// src/routes/flyer.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import flyerRouter from './flyer.routes';
import { createMockFlyer, createMockFlyerItem } from '../tests/utils/mockFactories';
import { NotFoundError } from '../services/db/errors.db';
import { createTestApp } from '../tests/utils/createTestApp';

// 1. Mock the Service Layer directly.
vi.mock('../services/db/index.db', () => ({
  flyerRepo: {
    getFlyerById: vi.fn(),
    getFlyers: vi.fn(),
    getFlyerItems: vi.fn(),
    getFlyerItemsForFlyers: vi.fn(),
    countFlyerItemsForFlyers: vi.fn(),
    trackFlyerItemInteraction: vi.fn(),
  },
}));

import { mockLogger } from '../tests/utils/mockLogger';

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

// Import the mocked db module to control its functions in tests
import * as db from '../services/db/index.db';

// Define a reusable matcher for the logger object.
const expectLogger = expect.objectContaining({
  info: expect.any(Function),
  error: expect.any(Function),
});

describe('Flyer Routes (/api/flyers)', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });
  
  const app = createTestApp({ router: flyerRouter, basePath: '/api/flyers' });

  // Add a basic error handler to capture errors passed to next(err) and return JSON.
  // This prevents unhandled error crashes in tests and ensures we get the 500 response we expect.
  app.use((err: any, req: any, res: any, next: any) => {
    res.status(err.status || 500).json({ message: err.message, errors: err.errors });
  });

  describe('GET /', () => {
    it('should return a list of flyers on success', async () => {
      const mockFlyers = [createMockFlyer({ flyer_id: 1 }), createMockFlyer({ flyer_id: 2 })];
      vi.mocked(db.flyerRepo.getFlyers).mockResolvedValue(mockFlyers);

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

      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockFlyers);
    });

    it('should pass limit and offset query parameters to the db function', async () => {
      vi.mocked(db.flyerRepo.getFlyers).mockResolvedValue([]);
      await supertest(app).get('/api/flyers?limit=15&offset=30');
      expect(db.flyerRepo.getFlyers).toHaveBeenCalledWith(expectLogger, 15, 30);
    });

    it('should return 500 if the database call fails', async () => {
      vi.mocked(db.flyerRepo.getFlyers).mockRejectedValue(new Error('DB Error'));
      const response = await supertest(app).get('/api/flyers');
      expect(response.status).toBe(500);
      expect(response.body.message).toBe('DB Error');
    });

    it('should return 400 for invalid query parameters', async () => {
      const response = await supertest(app).get('/api/flyers?limit=abc&offset=-5');
      expect(response.status).toBe(400);
      expect(response.body.errors).toBeDefined();
      expect(response.body.errors.length).toBe(2);
    });
  });

  describe('GET /:id', () => {
    it('should return a single flyer on success', async () => {
      const mockFlyer = createMockFlyer({ flyer_id: 123 });
      vi.mocked(db.flyerRepo.getFlyerById).mockResolvedValue(mockFlyer);

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

      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockFlyer);
      expect(db.flyerRepo.getFlyerById).toHaveBeenCalledWith(123);
    });

    it('should return 404 if the flyer is not found', async () => {
      // FIX: Instead of mocking a rejection, we mock the *result* of the database query
      // to have zero rows. This allows the `getFlyerById` method's own internal logic
      // to correctly throw the `NotFoundError`, making the test more realistic.
      vi.mocked(db.flyerRepo.getFlyerById).mockRejectedValue(new NotFoundError(`Flyer with ID 999 not found.`));
      const response = await supertest(app).get('/api/flyers/999');

      expect(response.status).toBe(404);
      expect(response.body.message).toContain('not found');
    });

    it('should return 400 for an invalid flyer ID', async () => {
      const response = await supertest(app).get('/api/flyers/abc');
      expect(response.status).toBe(400);
      // Zod coercion results in NaN for "abc", which triggers a type error before our custom message
      expect(response.body.errors[0].message).toMatch(/Invalid flyer ID provided|expected number, received NaN/);
    });

    it('should return 500 if the database call fails', async () => {
      vi.mocked(db.flyerRepo.getFlyerById).mockRejectedValue(new Error('DB Error'));
      const response = await supertest(app).get('/api/flyers/123');
      expect(response.status).toBe(500);
      expect(response.body.message).toBe('DB Error');
    });
  });

  describe('GET /:id/items', () => {
    it('should return items for a specific flyer', async () => {
      const mockFlyerItems = [createMockFlyerItem({ flyer_item_id: 1, flyer_id: 123 })];
      vi.mocked(db.flyerRepo.getFlyerItems).mockResolvedValue(mockFlyerItems);

      const response = await supertest(app).get('/api/flyers/123/items');

      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockFlyerItems);
    });

    it('should return 400 for an invalid flyer ID', async () => {
      const response = await supertest(app).get('/api/flyers/abc/items');
      expect(response.status).toBe(400);
      expect(response.body.errors[0].message).toMatch(/Invalid flyer ID provided|expected number, received NaN/);
    });

    it('should return 500 if the database call fails', async () => {
      vi.mocked(db.flyerRepo.getFlyerItems).mockRejectedValue(new Error('DB Error'));
      const response = await supertest(app).get('/api/flyers/123/items');
      expect(response.status).toBe(500);
      expect(response.body.message).toBe('DB Error');
    });
  });

  describe('POST /items/batch-fetch', () => {
    it('should return items for multiple flyers', async () => {
      const mockFlyerItems = [createMockFlyerItem({ flyer_item_id: 1, flyer_id: 1 })];
      vi.mocked(db.flyerRepo.getFlyerItemsForFlyers).mockResolvedValue(mockFlyerItems);

      const response = await supertest(app)
        .post('/api/flyers/items/batch-fetch')
        .send({ flyerIds: [1, 2] });

      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockFlyerItems);
    });

    it('should return 400 if flyerIds is not an array', async () => {
      const response = await supertest(app)
        .post('/api/flyers/items/batch-fetch')
        .send({ flyerIds: 'not-an-array' });
      expect(response.status).toBe(400);
      expect(response.body.errors[0].message).toMatch(/expected array/);
    });

    it('should return 400 if flyerIds is an empty array, as per schema validation', async () => {
      const response = await supertest(app)
        .post('/api/flyers/items/batch-fetch')
        .send({ flyerIds: [] });
      expect(response.status).toBe(400);
      // Check for the specific Zod error message.
      expect(response.body.errors[0].message).toBe('flyerIds must be a non-empty array.');
    });

    it('should return 500 if the database call fails', async () => {
      vi.mocked(db.flyerRepo.getFlyerItemsForFlyers).mockRejectedValue(new Error('DB Error'));
      const response = await supertest(app).post('/api/flyers/items/batch-fetch').send({ flyerIds: [1] });
      expect(response.status).toBe(500);
      expect(response.body.message).toBe('DB Error');
    });
  });

  describe('POST /items/batch-count', () => {
    it('should return the count of items for multiple flyers', async () => {
      vi.mocked(db.flyerRepo.countFlyerItemsForFlyers).mockResolvedValueOnce(42);

      const response = await supertest(app)
        .post('/api/flyers/items/batch-count')
        .send({ flyerIds: [1, 2, 3] });

      expect(response.status).toBe(200);
      expect(response.body).toEqual({ count: 42 });
    });

    it('should return 400 if flyerIds is not an array', async () => {
      const response = await supertest(app)
        .post('/api/flyers/items/batch-count')
        .send({ flyerIds: 'not-an-array' });
      expect(response.status).toBe(400);
    });

    it('should return a count of 0 if flyerIds is empty', async () => {
      vi.mocked(db.flyerRepo.countFlyerItemsForFlyers).mockResolvedValueOnce(0);

      const response = await supertest(app)
        .post('/api/flyers/items/batch-count')
        .send({ flyerIds: [] });
      expect(response.status).toBe(200);
      expect(response.body).toEqual({ count: 0 });
    });

    it('should return 500 if the database call fails', async () => {
      vi.mocked(db.flyerRepo.countFlyerItemsForFlyers).mockRejectedValue(new Error('DB Error'));
      const response = await supertest(app).post('/api/flyers/items/batch-count').send({ flyerIds: [1] });
      expect(response.status).toBe(500);
      expect(response.body.message).toBe('DB Error');
    });
  });

  describe('POST /items/:itemId/track', () => {
    it('should return 202 Accepted and call the tracking function', async () => {
      const response = await supertest(app)
        .post('/api/flyers/items/99/track')
        .send({ type: 'click' });

      expect(response.status).toBe(202);
      expect(db.flyerRepo.trackFlyerItemInteraction).toHaveBeenCalledWith(99, 'click', expectLogger);
    });

    it('should return 400 for an invalid item ID', async () => {
      const response = await supertest(app)
        .post('/api/flyers/items/abc/track')
        .send({ type: 'click' });
      expect(response.status).toBe(400);
    });

    it('should return 400 for an invalid interaction type', async () => {
      const response = await supertest(app).post('/api/flyers/items/99/track').send({ type: 'invalid' });
      expect(response.status).toBe(400);
    });
  });
});