import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import type { Request, Response, NextFunction } from 'express';
import { createTestApp } from '../tests/utils/createTestApp';
import { createMockUserProfile } from '../tests/utils/mockFactories';
import type { UserReaction } from '../types';

// 1. Mock the Service Layer directly.
vi.mock('../services/db/index.db', () => ({
  reactionRepo: {
    getReactions: vi.fn(),
    getReactionSummary: vi.fn(),
    toggleReaction: vi.fn(),
  },
}));

// Mock the logger to keep test output clean
vi.mock('../services/logger.server', async () => ({
  logger: (await import('../tests/utils/mockLogger')).mockLogger,
}));

// Mock Passport middleware
vi.mock('../config/passport', () => ({
  default: {
    authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
      // If we are testing the unauthenticated state (no user injected), simulate 401.
      if (!req.user) {
        return res.status(401).json({ message: 'Unauthorized' });
      }
      next();
    }),
  },
}));

// Import the router and mocked DB AFTER all mocks are defined.
import reactionsRouter from './reactions.routes';
import { reactionRepo } from '../services/db/index.db';
import { mockLogger } from '../tests/utils/mockLogger';

const expectLogger = expect.objectContaining({
  info: expect.any(Function),
  error: expect.any(Function),
});

describe('Reaction Routes (/api/reactions)', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('GET /', () => {
    const app = createTestApp({ router: reactionsRouter, basePath: '/api/reactions' });

    it('should return a list of reactions', async () => {
      const mockReactions = [
        { reaction_id: 1, reaction_type: 'like', entity_id: '123' },
      ] as unknown as UserReaction[];
      vi.mocked(reactionRepo.getReactions).mockResolvedValue(mockReactions);

      const response = await supertest(app).get('/api/reactions');

      expect(response.status).toBe(200);
      expect(response.body.data).toEqual(mockReactions);
      expect(reactionRepo.getReactions).toHaveBeenCalledWith({}, expectLogger);
    });

    it('should filter by query parameters', async () => {
      const mockReactions = [
        { reaction_id: 1, reaction_type: 'like' },
      ] as unknown as UserReaction[];
      vi.mocked(reactionRepo.getReactions).mockResolvedValue(mockReactions);

      const validUuid = '123e4567-e89b-12d3-a456-426614174000';
      const query = { userId: validUuid, entityType: 'recipe', entityId: '1' };

      const response = await supertest(app).get('/api/reactions').query(query);

      expect(response.status).toBe(200);
      expect(reactionRepo.getReactions).toHaveBeenCalledWith(
        expect.objectContaining(query),
        expectLogger,
      );
    });

    it('should return 500 on database error', async () => {
      const error = new Error('DB Error');
      vi.mocked(reactionRepo.getReactions).mockRejectedValue(error);

      const response = await supertest(app).get('/api/reactions');

      expect(response.status).toBe(500);
      expect(mockLogger.error).toHaveBeenCalledWith({ error }, 'Error fetching user reactions');
    });
  });

  describe('GET /summary', () => {
    const app = createTestApp({ router: reactionsRouter, basePath: '/api/reactions' });

    it('should return reaction summary for an entity', async () => {
      const mockSummary = [
        { reaction_type: 'like', count: 10 },
        { reaction_type: 'love', count: 5 },
      ];
      vi.mocked(reactionRepo.getReactionSummary).mockResolvedValue(mockSummary);

      const response = await supertest(app)
        .get('/api/reactions/summary')
        .query({ entityType: 'recipe', entityId: '123' });

      expect(response.status).toBe(200);
      expect(response.body.data).toEqual(mockSummary);
      expect(reactionRepo.getReactionSummary).toHaveBeenCalledWith('recipe', '123', expectLogger);
    });

    it('should return 400 if required parameters are missing', async () => {
      const response = await supertest(app).get('/api/reactions/summary');
      expect(response.status).toBe(400);
      expect(response.body.error.details[0].message).toContain('required');
    });

    it('should return 500 on database error', async () => {
      const error = new Error('DB Error');
      vi.mocked(reactionRepo.getReactionSummary).mockRejectedValue(error);

      const response = await supertest(app)
        .get('/api/reactions/summary')
        .query({ entityType: 'recipe', entityId: '123' });

      expect(response.status).toBe(500);
      expect(mockLogger.error).toHaveBeenCalledWith({ error }, 'Error fetching reaction summary');
    });
  });

  describe('POST /toggle', () => {
    const mockUser = createMockUserProfile({ user: { user_id: 'user-123' } });
    const app = createTestApp({
      router: reactionsRouter,
      basePath: '/api/reactions',
      authenticatedUser: mockUser,
    });

    const validBody = {
      entity_type: 'recipe',
      entity_id: '123',
      reaction_type: 'like',
    };

    it('should return 201 when a reaction is added', async () => {
      const mockResult = {
        ...validBody,
        reaction_id: 1,
        user_id: 'user-123',
      } as unknown as UserReaction;
      vi.mocked(reactionRepo.toggleReaction).mockResolvedValue(mockResult);

      const response = await supertest(app).post('/api/reactions/toggle').send(validBody);

      expect(response.status).toBe(201);
      expect(response.body.data).toEqual({ message: 'Reaction added.', reaction: mockResult });
      expect(reactionRepo.toggleReaction).toHaveBeenCalledWith(
        { user_id: 'user-123', ...validBody },
        expectLogger,
      );
    });

    it('should return 200 when a reaction is removed', async () => {
      // Returning null/false from toggleReaction implies the reaction was removed
      vi.mocked(reactionRepo.toggleReaction).mockResolvedValue(null);

      const response = await supertest(app).post('/api/reactions/toggle').send(validBody);

      expect(response.status).toBe(200);
      expect(response.body.data).toEqual({ message: 'Reaction removed.' });
    });

    it('should return 400 if body is invalid', async () => {
      const response = await supertest(app)
        .post('/api/reactions/toggle')
        .send({ entity_type: 'recipe' }); // Missing other required fields

      expect(response.status).toBe(400);
      expect(response.body.error.details).toBeDefined();
    });

    it('should return 401 if not authenticated', async () => {
      const unauthApp = createTestApp({ router: reactionsRouter, basePath: '/api/reactions' });
      const response = await supertest(unauthApp).post('/api/reactions/toggle').send(validBody);

      expect(response.status).toBe(401);
    });

    it('should return 500 on database error', async () => {
      const error = new Error('DB Error');
      vi.mocked(reactionRepo.toggleReaction).mockRejectedValue(error);

      const response = await supertest(app).post('/api/reactions/toggle').send(validBody);

      expect(response.status).toBe(500);
      expect(mockLogger.error).toHaveBeenCalledWith(
        { error, body: validBody },
        'Error toggling user reaction',
      );
    });
  });

  describe('Rate Limiting', () => {
    it('should apply publicReadLimiter to GET /', async () => {
      const app = createTestApp({ router: reactionsRouter, basePath: '/api/reactions' });
      vi.mocked(reactionRepo.getReactions).mockResolvedValue([]);
      const response = await supertest(app)
        .get('/api/reactions')
        .set('X-Test-Rate-Limit-Enable', 'true');

      expect(response.status).toBe(200);
      expect(response.headers).toHaveProperty('ratelimit-limit');
    });

    it('should apply userUpdateLimiter to POST /toggle', async () => {
      const mockUser = createMockUserProfile({ user: { user_id: 'user-123' } });
      const app = createTestApp({
        router: reactionsRouter,
        basePath: '/api/reactions',
        authenticatedUser: mockUser,
      });
      vi.mocked(reactionRepo.toggleReaction).mockResolvedValue(null);

      const response = await supertest(app)
        .post('/api/reactions/toggle')
        .set('X-Test-Rate-Limit-Enable', 'true')
        .send({ entity_type: 'recipe', entity_id: '1', reaction_type: 'like' });

      expect(response.status).toBe(200);
      expect(response.headers).toHaveProperty('ratelimit-limit');
      expect(parseInt(response.headers['ratelimit-limit'])).toBe(150);
    });
  });
});
