// src/routes/upc.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import type { Request, Response, NextFunction } from 'express';
import { createMockUserProfile } from '../tests/utils/mockFactories';
import { createTestApp } from '../tests/utils/createTestApp';
import { NotFoundError } from '../services/db/errors.db';
import type { UpcScanSource } from '../types/upc';

// Mock the upcService module
vi.mock('../services/upcService.server', () => ({
  scanUpc: vi.fn(),
  lookupUpc: vi.fn(),
  getScanHistory: vi.fn(),
  getScanById: vi.fn(),
  getScanStats: vi.fn(),
  linkUpcToProduct: vi.fn(),
}));

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

// Import the router and mocked service AFTER all mocks are defined.
import upcRouter from './upc.routes';
import * as upcService from '../services/upcService.server';

const mockUser = createMockUserProfile({
  user: { user_id: 'user-123', email: 'test@test.com' },
});

const _mockAdminUser = createMockUserProfile({
  user: { user_id: 'admin-123', email: 'admin@test.com' },
  role: 'admin',
});

// Standardized mock for passport
// Note: createTestApp sets req.user before the router runs, so we preserve it here
vi.mock('../config/passport', () => ({
  default: {
    authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
      // Preserve the user set by createTestApp if already present
      if (!req.user) {
        req.user = mockUser;
      }
      next();
    }),
    initialize: () => (req: Request, res: Response, next: NextFunction) => next(),
  },
  isAdmin: (req: Request, res: Response, next: NextFunction) => {
    const user = req.user as typeof _mockAdminUser;
    if (user?.role === 'admin') {
      next();
    } else {
      res.status(403).json({ success: false, error: { message: 'Forbidden' } });
    }
  },
}));

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

describe('UPC Routes (/api/upc)', () => {
  const mockUserProfile = createMockUserProfile({
    user: { user_id: 'user-123', email: 'test@test.com' },
  });

  const mockAdminProfile = createMockUserProfile({
    user: { user_id: 'admin-123', email: 'admin@test.com' },
    role: 'admin',
  });

  beforeEach(() => {
    vi.clearAllMocks();
    // Provide default mock implementations
    vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });
    vi.mocked(upcService.getScanStats).mockResolvedValue({
      total_scans: 0,
      successful_lookups: 0,
      unique_products: 0,
      scans_today: 0,
      scans_this_week: 0,
    });
  });

  const app = createTestApp({
    router: upcRouter,
    basePath: '/api/upc',
    authenticatedUser: mockUserProfile,
  });

  const adminApp = createTestApp({
    router: upcRouter,
    basePath: '/api/upc',
    authenticatedUser: mockAdminProfile,
  });

  describe('POST /scan', () => {
    it('should scan a manually entered UPC code successfully', async () => {
      const mockScanResult = {
        scan_id: 1,
        upc_code: '012345678905',
        product: {
          product_id: 1,
          name: 'Test Product',
          brand: 'Test Brand',
          category: 'Snacks',
          description: null,
          size: '500g',
          upc_code: '012345678905',
          image_url: null,
          master_item_id: null,
        },
        external_lookup: null,
        confidence: null,
        lookup_successful: true,
        is_new_product: false,
        scanned_at: new Date().toISOString(),
      };

      vi.mocked(upcService.scanUpc).mockResolvedValue(mockScanResult);

      const response = await supertest(app).post('/api/upc/scan').send({
        upc_code: '012345678905',
        scan_source: 'manual_entry',
      });

      expect(response.status).toBe(200);
      expect(response.body.data.scan_id).toBe(1);
      expect(response.body.data.upc_code).toBe('012345678905');
      expect(response.body.data.lookup_successful).toBe(true);
      expect(upcService.scanUpc).toHaveBeenCalledWith(
        mockUserProfile.user.user_id,
        { upc_code: '012345678905', scan_source: 'manual_entry' },
        expectLogger,
      );
    });

    it('should scan from base64 image', async () => {
      const mockScanResult = {
        scan_id: 2,
        upc_code: '987654321098',
        product: null,
        external_lookup: {
          name: 'External Product',
          brand: 'External Brand',
          category: null,
          description: null,
          image_url: null,
          source: 'openfoodfacts' as const,
        },
        confidence: 0.95,
        lookup_successful: true,
        is_new_product: true,
        scanned_at: new Date().toISOString(),
      };

      vi.mocked(upcService.scanUpc).mockResolvedValue(mockScanResult);

      const response = await supertest(app).post('/api/upc/scan').send({
        image_base64: 'SGVsbG8gV29ybGQ=',
        scan_source: 'image_upload',
      });

      expect(response.status).toBe(200);
      expect(response.body.data.confidence).toBe(0.95);
      expect(response.body.data.is_new_product).toBe(true);
    });

    it('should return 400 when neither upc_code nor image_base64 is provided', async () => {
      const response = await supertest(app).post('/api/upc/scan').send({
        scan_source: 'manual_entry',
      });

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

    it('should return 400 for invalid scan_source', async () => {
      const response = await supertest(app).post('/api/upc/scan').send({
        upc_code: '012345678905',
        scan_source: 'invalid_source',
      });

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

    it('should return 500 if the scan service fails', async () => {
      vi.mocked(upcService.scanUpc).mockRejectedValue(new Error('Scan service error'));

      const response = await supertest(app).post('/api/upc/scan').send({
        upc_code: '012345678905',
        scan_source: 'manual_entry',
      });

      expect(response.status).toBe(500);
      expect(response.body.error.message).toBe('Scan service error');
    });
  });

  describe('GET /lookup', () => {
    it('should look up a UPC code successfully', async () => {
      const mockLookupResult = {
        upc_code: '012345678905',
        product: {
          product_id: 1,
          name: 'Test Product',
          brand: 'Test Brand',
          category: 'Snacks',
          description: null,
          size: '500g',
          upc_code: '012345678905',
          image_url: null,
          master_item_id: null,
        },
        external_lookup: null,
        found: true,
        from_cache: false,
      };

      vi.mocked(upcService.lookupUpc).mockResolvedValue(mockLookupResult);

      const response = await supertest(app).get('/api/upc/lookup?upc_code=012345678905');

      expect(response.status).toBe(200);
      expect(response.body.data.upc_code).toBe('012345678905');
      expect(response.body.data.found).toBe(true);
    });

    it('should support include_external and force_refresh parameters', async () => {
      const mockLookupResult = {
        upc_code: '012345678905',
        product: null,
        external_lookup: {
          name: 'External Product',
          brand: 'External Brand',
          category: null,
          description: null,
          image_url: null,
          source: 'openfoodfacts' as const,
        },
        found: true,
        from_cache: false,
      };

      vi.mocked(upcService.lookupUpc).mockResolvedValue(mockLookupResult);

      const response = await supertest(app).get(
        '/api/upc/lookup?upc_code=012345678905&include_external=true&force_refresh=true',
      );

      expect(response.status).toBe(200);
      expect(upcService.lookupUpc).toHaveBeenCalledWith(
        expect.objectContaining({
          upc_code: '012345678905',
          force_refresh: true,
        }),
        expectLogger,
      );
    });

    it('should return 400 for invalid UPC code format', async () => {
      const response = await supertest(app).get('/api/upc/lookup?upc_code=123');

      expect(response.status).toBe(400);
      expect(response.body.error.details[0].message).toMatch(/8-14 digits/);
    });

    it('should return 400 when upc_code is missing', async () => {
      const response = await supertest(app).get('/api/upc/lookup');

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

    it('should return 500 if the lookup service fails', async () => {
      vi.mocked(upcService.lookupUpc).mockRejectedValue(new Error('Lookup error'));

      const response = await supertest(app).get('/api/upc/lookup?upc_code=012345678905');

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

  describe('GET /history', () => {
    it('should return scan history with pagination', async () => {
      const mockHistory = {
        scans: [
          {
            scan_id: 1,
            user_id: 'user-123',
            upc_code: '012345678905',
            product_id: 1,
            scan_source: 'manual_entry' as UpcScanSource,
            scan_confidence: null,
            raw_image_path: null,
            lookup_successful: true,
            created_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
          },
        ],
        total: 1,
      };

      vi.mocked(upcService.getScanHistory).mockResolvedValue(mockHistory);

      const response = await supertest(app).get('/api/upc/history?limit=10&offset=0');

      expect(response.status).toBe(200);
      expect(response.body.data.scans).toHaveLength(1);
      expect(response.body.data.total).toBe(1);
      expect(upcService.getScanHistory).toHaveBeenCalledWith(
        expect.objectContaining({
          user_id: mockUserProfile.user.user_id,
          limit: 10,
          offset: 0,
        }),
        expectLogger,
      );
    });

    it('should support filtering by lookup_successful', async () => {
      vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });

      const response = await supertest(app).get('/api/upc/history?lookup_successful=true');

      expect(response.status).toBe(200);
      expect(upcService.getScanHistory).toHaveBeenCalledWith(
        expect.objectContaining({
          lookup_successful: true,
        }),
        expectLogger,
      );
    });

    it('should support filtering by scan_source', async () => {
      vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });

      const response = await supertest(app).get('/api/upc/history?scan_source=image_upload');

      expect(response.status).toBe(200);
      expect(upcService.getScanHistory).toHaveBeenCalledWith(
        expect.objectContaining({
          scan_source: 'image_upload',
        }),
        expectLogger,
      );
    });

    it('should support filtering by date range', async () => {
      vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });

      const response = await supertest(app).get(
        '/api/upc/history?from_date=2024-01-01&to_date=2024-01-31',
      );

      expect(response.status).toBe(200);
      expect(upcService.getScanHistory).toHaveBeenCalledWith(
        expect.objectContaining({
          from_date: '2024-01-01',
          to_date: '2024-01-31',
        }),
        expectLogger,
      );
    });

    it('should return 400 for invalid date format', async () => {
      const response = await supertest(app).get('/api/upc/history?from_date=01-01-2024');

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

    it('should return 500 if the history service fails', async () => {
      vi.mocked(upcService.getScanHistory).mockRejectedValue(new Error('History error'));

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

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

  describe('GET /history/:scanId', () => {
    it('should return a specific scan by ID', async () => {
      const mockScan = {
        scan_id: 1,
        user_id: 'user-123',
        upc_code: '012345678905',
        product_id: 1,
        scan_source: 'manual_entry' as UpcScanSource,
        scan_confidence: null,
        raw_image_path: null,
        lookup_successful: true,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      vi.mocked(upcService.getScanById).mockResolvedValue(mockScan);

      const response = await supertest(app).get('/api/upc/history/1');

      expect(response.status).toBe(200);
      expect(response.body.data.scan_id).toBe(1);
      expect(upcService.getScanById).toHaveBeenCalledWith(
        1,
        mockUserProfile.user.user_id,
        expectLogger,
      );
    });

    it('should return 404 when scan not found', async () => {
      vi.mocked(upcService.getScanById).mockRejectedValue(new NotFoundError('Scan not found'));

      const response = await supertest(app).get('/api/upc/history/999');

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

    it('should return 400 for invalid scan ID', async () => {
      const response = await supertest(app).get('/api/upc/history/abc');

      expect(response.status).toBe(400);
      expect(response.body.error.details[0].message).toMatch(/Invalid ID|number/i);
    });
  });

  describe('GET /stats', () => {
    it('should return scan statistics', async () => {
      const mockStats = {
        total_scans: 100,
        successful_lookups: 80,
        unique_products: 50,
        scans_today: 5,
        scans_this_week: 25,
      };

      vi.mocked(upcService.getScanStats).mockResolvedValue(mockStats);

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

      expect(response.status).toBe(200);
      expect(response.body.data.total_scans).toBe(100);
      expect(response.body.data.successful_lookups).toBe(80);
      expect(upcService.getScanStats).toHaveBeenCalledWith(
        mockUserProfile.user.user_id,
        expectLogger,
      );
    });

    it('should return 500 if the stats service fails', async () => {
      vi.mocked(upcService.getScanStats).mockRejectedValue(new Error('Stats error'));

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

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

  describe('POST /link', () => {
    it('should link UPC to product (admin only)', async () => {
      vi.mocked(upcService.linkUpcToProduct).mockResolvedValue(undefined);

      const response = await supertest(adminApp).post('/api/upc/link').send({
        upc_code: '012345678905',
        product_id: 1,
      });

      expect(response.status).toBe(204);
      expect(upcService.linkUpcToProduct).toHaveBeenCalledWith(1, '012345678905', expectLogger);
    });

    it('should return 403 for non-admin users', async () => {
      const response = await supertest(app).post('/api/upc/link').send({
        upc_code: '012345678905',
        product_id: 1,
      });

      expect(response.status).toBe(403);
      expect(upcService.linkUpcToProduct).not.toHaveBeenCalled();
    });

    it('should return 400 for invalid UPC code format', async () => {
      const response = await supertest(adminApp).post('/api/upc/link').send({
        upc_code: '123',
        product_id: 1,
      });

      expect(response.status).toBe(400);
      expect(response.body.error.details[0].message).toMatch(/8-14 digits/);
    });

    it('should return 400 for invalid product_id', async () => {
      const response = await supertest(adminApp).post('/api/upc/link').send({
        upc_code: '012345678905',
        product_id: -1,
      });

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

    it('should return 404 when product not found', async () => {
      vi.mocked(upcService.linkUpcToProduct).mockRejectedValue(
        new NotFoundError('Product not found'),
      );

      const response = await supertest(adminApp).post('/api/upc/link').send({
        upc_code: '012345678905',
        product_id: 999,
      });

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

    it('should return 500 if the link service fails', async () => {
      vi.mocked(upcService.linkUpcToProduct).mockRejectedValue(new Error('Link error'));

      const response = await supertest(adminApp).post('/api/upc/link').send({
        upc_code: '012345678905',
        product_id: 1,
      });

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