// src/routes/inventory.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 { UserInventoryItem, ExpiringItemsResponse } from '../types/expiry';

// Mock the expiryService module
vi.mock('../services/expiryService.server', () => ({
  getInventory: vi.fn(),
  addInventoryItem: vi.fn(),
  getInventoryItemById: vi.fn(),
  updateInventoryItem: vi.fn(),
  deleteInventoryItem: vi.fn(),
  markItemConsumed: vi.fn(),
  getExpiringItemsGrouped: vi.fn(),
  getExpiringItems: vi.fn(),
  getExpiredItems: vi.fn(),
  getAlertSettings: vi.fn(),
  updateAlertSettings: vi.fn(),
  getRecipeSuggestionsForExpiringItems: 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 inventoryRouter from './inventory.routes';
import * as expiryService from '../services/expiryService.server';

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

// Standardized mock for passport
vi.mock('../config/passport', () => ({
  default: {
    authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
      req.user = mockUser;
      next();
    }),
    initialize: () => (req: Request, res: Response, next: NextFunction) => next(),
  },
}));

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

// Helper to create mock inventory item
function createMockInventoryItem(overrides: Partial<UserInventoryItem> = {}): UserInventoryItem {
  return {
    inventory_id: 1,
    user_id: 'user-123',
    product_id: null,
    master_item_id: 100,
    item_name: 'Milk',
    quantity: 1,
    unit: 'liters',
    purchase_date: '2024-01-10',
    expiry_date: '2024-02-10',
    source: 'manual',
    location: 'fridge',
    notes: null,
    is_consumed: false,
    consumed_at: null,
    expiry_source: 'manual',
    receipt_item_id: null,
    pantry_location_id: 1,
    notification_sent_at: null,
    created_at: new Date().toISOString(),
    updated_at: new Date().toISOString(),
    days_until_expiry: 10,
    expiry_status: 'fresh',
    ...overrides,
  };
}

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

  beforeEach(() => {
    vi.clearAllMocks();
    // Provide default mock implementations
    vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });
    vi.mocked(expiryService.getExpiringItems).mockResolvedValue([]);
    vi.mocked(expiryService.getExpiredItems).mockResolvedValue([]);
    vi.mocked(expiryService.getAlertSettings).mockResolvedValue([]);
  });

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

  // ============================================================================
  // INVENTORY ITEM ENDPOINTS
  // ============================================================================

  describe('GET /', () => {
    it('should return paginated inventory items', async () => {
      const mockItems = [createMockInventoryItem()];
      vi.mocked(expiryService.getInventory).mockResolvedValue({
        items: mockItems,
        total: 1,
      });

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

      expect(response.status).toBe(200);
      expect(response.body.data.items).toHaveLength(1);
      expect(response.body.data.total).toBe(1);
    });

    it('should support filtering by location', async () => {
      vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });

      const response = await supertest(app).get('/api/inventory?location=fridge');

      expect(response.status).toBe(200);
      expect(expiryService.getInventory).toHaveBeenCalledWith(
        expect.objectContaining({ location: 'fridge' }),
        expectLogger,
      );
    });

    it('should support filtering by expiring_within_days', async () => {
      vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });

      const response = await supertest(app).get('/api/inventory?expiring_within_days=7');

      expect(response.status).toBe(200);
      expect(expiryService.getInventory).toHaveBeenCalledWith(
        expect.objectContaining({ expiring_within_days: 7 }),
        expectLogger,
      );
    });

    it('should support search filter', async () => {
      vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });

      const response = await supertest(app).get('/api/inventory?search=milk');

      expect(response.status).toBe(200);
      expect(expiryService.getInventory).toHaveBeenCalledWith(
        expect.objectContaining({ search: 'milk' }),
        expectLogger,
      );
    });

    it('should support sorting', async () => {
      vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });

      const response = await supertest(app).get(
        '/api/inventory?sort_by=expiry_date&sort_order=asc',
      );

      expect(response.status).toBe(200);
      expect(expiryService.getInventory).toHaveBeenCalledWith(
        expect.objectContaining({
          sort_by: 'expiry_date',
          sort_order: 'asc',
        }),
        expectLogger,
      );
    });

    it('should return 400 for invalid location', async () => {
      const response = await supertest(app).get('/api/inventory?location=invalid');

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

    it('should return 500 if service fails', async () => {
      vi.mocked(expiryService.getInventory).mockRejectedValue(new Error('DB Error'));

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

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

  describe('POST /', () => {
    it('should add a new inventory item', async () => {
      const mockItem = createMockInventoryItem();
      vi.mocked(expiryService.addInventoryItem).mockResolvedValue(mockItem);

      const response = await supertest(app).post('/api/inventory').send({
        item_name: 'Milk',
        source: 'manual',
        quantity: 1,
        location: 'fridge',
        expiry_date: '2024-02-10',
      });

      expect(response.status).toBe(201);
      expect(response.body.data.item_name).toBe('Milk');
      expect(expiryService.addInventoryItem).toHaveBeenCalledWith(
        mockUserProfile.user.user_id,
        expect.objectContaining({
          item_name: 'Milk',
          source: 'manual',
        }),
        expectLogger,
      );
    });

    it('should return 400 if item_name is missing', async () => {
      const response = await supertest(app).post('/api/inventory').send({
        source: 'manual',
      });

      expect(response.status).toBe(400);
      // Zod returns a type error message when a required field is undefined
      expect(response.body.error.details[0].message).toMatch(/expected string|required/i);
    });

    it('should return 400 for invalid source', async () => {
      const response = await supertest(app).post('/api/inventory').send({
        item_name: 'Milk',
        source: 'invalid_source',
      });

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

    it('should return 400 for invalid expiry_date format', async () => {
      const response = await supertest(app).post('/api/inventory').send({
        item_name: 'Milk',
        source: 'manual',
        expiry_date: '01-10-2024',
      });

      expect(response.status).toBe(400);
      expect(response.body.error.details[0].message).toMatch(/YYYY-MM-DD/);
    });

    it('should return 500 if service fails', async () => {
      vi.mocked(expiryService.addInventoryItem).mockRejectedValue(new Error('DB Error'));

      const response = await supertest(app).post('/api/inventory').send({
        item_name: 'Milk',
        source: 'manual',
      });

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

  describe('GET /:inventoryId', () => {
    it('should return a specific inventory item', async () => {
      const mockItem = createMockInventoryItem();
      vi.mocked(expiryService.getInventoryItemById).mockResolvedValue(mockItem);

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

      expect(response.status).toBe(200);
      expect(response.body.data.inventory_id).toBe(1);
      expect(expiryService.getInventoryItemById).toHaveBeenCalledWith(
        1,
        mockUserProfile.user.user_id,
        expectLogger,
      );
    });

    it('should return 404 when item not found', async () => {
      vi.mocked(expiryService.getInventoryItemById).mockRejectedValue(
        new NotFoundError('Item not found'),
      );

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

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

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

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

  describe('PUT /:inventoryId', () => {
    it('should update an inventory item', async () => {
      const mockItem = createMockInventoryItem({ quantity: 2 });
      vi.mocked(expiryService.updateInventoryItem).mockResolvedValue(mockItem);

      const response = await supertest(app).put('/api/inventory/1').send({
        quantity: 2,
      });

      expect(response.status).toBe(200);
      expect(response.body.data.quantity).toBe(2);
    });

    it('should update expiry_date', async () => {
      const mockItem = createMockInventoryItem({ expiry_date: '2024-03-01' });
      vi.mocked(expiryService.updateInventoryItem).mockResolvedValue(mockItem);

      const response = await supertest(app).put('/api/inventory/1').send({
        expiry_date: '2024-03-01',
      });

      expect(response.status).toBe(200);
      expect(expiryService.updateInventoryItem).toHaveBeenCalledWith(
        1,
        mockUserProfile.user.user_id,
        expect.objectContaining({ expiry_date: '2024-03-01' }),
        expectLogger,
      );
    });

    it('should return 400 if no update fields provided', async () => {
      const response = await supertest(app).put('/api/inventory/1').send({});

      expect(response.status).toBe(400);
      expect(response.body.error.details[0].message).toMatch(/At least one field/);
    });

    it('should return 404 when item not found', async () => {
      vi.mocked(expiryService.updateInventoryItem).mockRejectedValue(
        new NotFoundError('Item not found'),
      );

      const response = await supertest(app).put('/api/inventory/999').send({
        quantity: 2,
      });

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

  describe('DELETE /:inventoryId', () => {
    it('should delete an inventory item', async () => {
      vi.mocked(expiryService.deleteInventoryItem).mockResolvedValue(undefined);

      const response = await supertest(app).delete('/api/inventory/1');

      expect(response.status).toBe(204);
      expect(expiryService.deleteInventoryItem).toHaveBeenCalledWith(
        1,
        mockUserProfile.user.user_id,
        expectLogger,
      );
    });

    it('should return 404 when item not found', async () => {
      vi.mocked(expiryService.deleteInventoryItem).mockRejectedValue(
        new NotFoundError('Item not found'),
      );

      const response = await supertest(app).delete('/api/inventory/999');

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

  describe('POST /:inventoryId/consume', () => {
    it('should mark item as consumed', async () => {
      vi.mocked(expiryService.markItemConsumed).mockResolvedValue(undefined);

      const response = await supertest(app).post('/api/inventory/1/consume');

      expect(response.status).toBe(204);
      expect(expiryService.markItemConsumed).toHaveBeenCalledWith(
        1,
        mockUserProfile.user.user_id,
        expectLogger,
      );
    });

    it('should return 404 when item not found', async () => {
      vi.mocked(expiryService.markItemConsumed).mockRejectedValue(
        new NotFoundError('Item not found'),
      );

      const response = await supertest(app).post('/api/inventory/999/consume');

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

  // ============================================================================
  // EXPIRING ITEMS ENDPOINTS
  // ============================================================================

  describe('GET /expiring/summary', () => {
    it('should return expiring items grouped by urgency', async () => {
      const mockSummary: ExpiringItemsResponse = {
        expiring_today: [createMockInventoryItem({ days_until_expiry: 0 })],
        expiring_this_week: [createMockInventoryItem({ days_until_expiry: 3 })],
        expiring_this_month: [createMockInventoryItem({ days_until_expiry: 20 })],
        already_expired: [createMockInventoryItem({ days_until_expiry: -5 })],
        counts: {
          today: 1,
          this_week: 1,
          this_month: 1,
          expired: 1,
          total: 4,
        },
      };

      vi.mocked(expiryService.getExpiringItemsGrouped).mockResolvedValue(mockSummary);

      const response = await supertest(app).get('/api/inventory/expiring/summary');

      expect(response.status).toBe(200);
      expect(response.body.data.counts.total).toBe(4);
    });

    it('should return 500 if service fails', async () => {
      vi.mocked(expiryService.getExpiringItemsGrouped).mockRejectedValue(new Error('DB Error'));

      const response = await supertest(app).get('/api/inventory/expiring/summary');

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

  describe('GET /expiring', () => {
    it('should return items expiring within default 7 days', async () => {
      const mockItems = [createMockInventoryItem({ days_until_expiry: 5 })];
      vi.mocked(expiryService.getExpiringItems).mockResolvedValue(mockItems);

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

      expect(response.status).toBe(200);
      expect(response.body.data.items).toHaveLength(1);
      expect(expiryService.getExpiringItems).toHaveBeenCalledWith(
        mockUserProfile.user.user_id,
        7,
        expectLogger,
      );
    });

    it('should accept custom days parameter', async () => {
      vi.mocked(expiryService.getExpiringItems).mockResolvedValue([]);

      const response = await supertest(app).get('/api/inventory/expiring?days=14');

      expect(response.status).toBe(200);
      expect(expiryService.getExpiringItems).toHaveBeenCalledWith(
        mockUserProfile.user.user_id,
        14,
        expectLogger,
      );
    });

    it('should return 400 for invalid days parameter', async () => {
      const response = await supertest(app).get('/api/inventory/expiring?days=100');

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

  describe('GET /expired', () => {
    it('should return already expired items', async () => {
      const mockItems = [
        createMockInventoryItem({ days_until_expiry: -3, expiry_status: 'expired' }),
      ];
      vi.mocked(expiryService.getExpiredItems).mockResolvedValue(mockItems);

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

      expect(response.status).toBe(200);
      expect(response.body.data.items).toHaveLength(1);
      expect(expiryService.getExpiredItems).toHaveBeenCalledWith(
        mockUserProfile.user.user_id,
        expectLogger,
      );
    });

    it('should return 500 if service fails', async () => {
      vi.mocked(expiryService.getExpiredItems).mockRejectedValue(new Error('DB Error'));

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

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

  // ============================================================================
  // ALERT SETTINGS ENDPOINTS
  // ============================================================================

  describe('GET /alerts', () => {
    it('should return user alert settings', async () => {
      const mockSettings = [
        {
          expiry_alert_id: 1,
          user_id: 'user-123',
          alert_method: 'email' as const,
          days_before_expiry: 3,
          is_enabled: true,
          last_alert_sent_at: null,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ];

      vi.mocked(expiryService.getAlertSettings).mockResolvedValue(mockSettings);

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

      expect(response.status).toBe(200);
      expect(response.body.data).toHaveLength(1);
      expect(response.body.data[0].alert_method).toBe('email');
    });

    it('should return 500 if service fails', async () => {
      vi.mocked(expiryService.getAlertSettings).mockRejectedValue(new Error('DB Error'));

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

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

  describe('PUT /alerts/:alertMethod', () => {
    it('should update alert settings for email', async () => {
      const mockSettings = {
        expiry_alert_id: 1,
        user_id: 'user-123',
        alert_method: 'email' as const,
        days_before_expiry: 5,
        is_enabled: true,
        last_alert_sent_at: null,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      vi.mocked(expiryService.updateAlertSettings).mockResolvedValue(mockSettings);

      const response = await supertest(app).put('/api/inventory/alerts/email').send({
        days_before_expiry: 5,
        is_enabled: true,
      });

      expect(response.status).toBe(200);
      expect(response.body.data.days_before_expiry).toBe(5);
      expect(expiryService.updateAlertSettings).toHaveBeenCalledWith(
        mockUserProfile.user.user_id,
        'email',
        { days_before_expiry: 5, is_enabled: true },
        expectLogger,
      );
    });

    it('should return 400 for invalid alert method', async () => {
      const response = await supertest(app).put('/api/inventory/alerts/sms').send({
        is_enabled: true,
      });

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

    it('should return 400 for invalid days_before_expiry', async () => {
      const response = await supertest(app).put('/api/inventory/alerts/email').send({
        days_before_expiry: 0,
      });

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

    it('should return 400 if days_before_expiry exceeds maximum', async () => {
      const response = await supertest(app).put('/api/inventory/alerts/email').send({
        days_before_expiry: 31,
      });

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

    it('should return 500 if service fails', async () => {
      vi.mocked(expiryService.updateAlertSettings).mockRejectedValue(new Error('DB Error'));

      const response = await supertest(app).put('/api/inventory/alerts/email').send({
        is_enabled: false,
      });

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

  // ============================================================================
  // RECIPE SUGGESTIONS ENDPOINT
  // ============================================================================

  describe('GET /recipes/suggestions', () => {
    it('should return recipe suggestions for expiring items', async () => {
      const mockInventoryItem = createMockInventoryItem({ inventory_id: 1, item_name: 'Milk' });
      const mockResult = {
        recipes: [
          {
            recipe_id: 1,
            recipe_name: 'Milk Smoothie',
            description: 'A healthy smoothie',
            prep_time_minutes: 5,
            cook_time_minutes: 0,
            servings: 2,
            photo_url: null,
            matching_items: [mockInventoryItem],
            match_count: 1,
          },
        ],
        total: 1,
        considered_items: [mockInventoryItem],
      };

      vi.mocked(expiryService.getRecipeSuggestionsForExpiringItems).mockResolvedValue(
        mockResult as any,
      );

      const response = await supertest(app).get('/api/inventory/recipes/suggestions');

      expect(response.status).toBe(200);
      expect(response.body.data.recipes).toHaveLength(1);
      expect(response.body.data.total).toBe(1);
    });

    it('should accept days, limit, and offset parameters', async () => {
      vi.mocked(expiryService.getRecipeSuggestionsForExpiringItems).mockResolvedValue({
        recipes: [],
        total: 0,
        considered_items: [],
      });

      const response = await supertest(app).get(
        '/api/inventory/recipes/suggestions?days=14&limit=5&offset=10',
      );

      expect(response.status).toBe(200);
      expect(expiryService.getRecipeSuggestionsForExpiringItems).toHaveBeenCalledWith(
        mockUserProfile.user.user_id,
        14,
        expectLogger,
        { limit: 5, offset: 10 },
      );
    });

    it('should return 400 for invalid days parameter', async () => {
      const response = await supertest(app).get('/api/inventory/recipes/suggestions?days=100');

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

    it('should return 500 if service fails', async () => {
      vi.mocked(expiryService.getRecipeSuggestionsForExpiringItems).mockRejectedValue(
        new Error('DB Error'),
      );

      const response = await supertest(app).get('/api/inventory/recipes/suggestions');

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