import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ShoppingGapService } from '../../../src/modules/meal-plans/shopping-gap.service.js';
import type { MealPlanRepository } from '../../../src/modules/meal-plans/meal-plans.repository.js';
import type { RecipesRepository } from '../../../src/modules/recipes/recipes.repository.js';
import type { PantryRepository } from '../../../src/modules/pantry/pantry.repository.js';
import type { ProductsRepository } from '../../../src/modules/products/products.repository.js';
import { NotFoundError } from '../../../src/common/errors.js';

describe(ShoppingGapService.name, () => {
  let service: ShoppingGapService;
  let mockMealRepo: { [K in keyof MealPlanRepository]: ReturnType<typeof vi.fn> };
  let mockRecipesRepo: { [K in keyof RecipesRepository]: ReturnType<typeof vi.fn> };
  let mockPantryRepo: { [K in keyof PantryRepository]: ReturnType<typeof vi.fn> };
  let mockProductsRepo: { [K in keyof ProductsRepository]: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockMealRepo = { findById: vi.fn() } as never;
    mockRecipesRepo = { findById: vi.fn() } as never;
    mockPantryRepo = { findActiveByHousehold: vi.fn() } as never;
    mockProductsRepo = { findByIds: vi.fn() } as never;

    service = new ShoppingGapService({
      mealPlanRepository: mockMealRepo as unknown as MealPlanRepository,
      recipesRepository: mockRecipesRepo as unknown as RecipesRepository,
      pantryRepository: mockPantryRepo as unknown as PantryRepository,
      productsRepository: mockProductsRepo as unknown as ProductsRepository,
    });
  });

  describe('calculateGap', () => {
    it('throws NotFoundError if plan is missing', async () => {
      mockMealRepo.findById.mockResolvedValue(null);
      await expect(service.calculateGap('hh1', 'p1')).rejects.toThrow(NotFoundError);
    });

    it('correctly scales recipe ingredients and contrasts against pantry', async () => {
      // 1. Setup Meal Plan with 1 meal
      // Recipe A planned for 4 servings.
      mockMealRepo.findById.mockResolvedValue({
        _id: 'plan1',
        days: [
          {
            meals: [
              { recipeId: 'recipe1', servings: 4 }
            ]
          }
        ]
      });

      // 2. Recipe 1: serves 2, needs 100g of ProdA (total needed = 200g for 4 servings)
      mockRecipesRepo.findById.mockResolvedValue({
        _id: 'recipe1',
        servings: 2,
        ingredients: [
          { productId: 'prodA', quantity: 100, unit: 'g', isOptional: false }
        ]
      });

      // 3. Products Info
      mockProductsRepo.findByIds.mockResolvedValue([
        { _id: 'prodA', name: 'Flour', category: 'baking' }
      ]);

      // 4. Pantry only has 50g of ProdA. Missing amount should be 150g!
      mockPantryRepo.findActiveByHousehold.mockResolvedValue([
        { productId: 'prodA', quantity: 50 }
      ]);

      const result = await service.calculateGap('hh1', 'plan1');

      expect(result.mealPlanId).toBe('plan1');
      expect(result.missingItems.length).toBe(1);

      const gap = result.missingItems[0]!;
      expect(gap.productId).toBe('prodA');
      expect(gap.productName).toBe('Flour');
      expect(gap.requiredQuantity).toBe(200); // 100g * (4 planned / 2 base)
      expect(gap.pantryQuantity).toBe(50);
      expect(gap.missingQuantity).toBe(150);
      expect(gap.unit).toBe('g');
    });

    it('does not include products that are fully stocked', async () => {
      mockMealRepo.findById.mockResolvedValue({
        _id: 'plan2',
        days: [
          {
            meals: [{ recipeId: 'recipe1', servings: 2 }]
          }
        ]
      });

      mockRecipesRepo.findById.mockResolvedValue({
        _id: 'recipe1',
        servings: 2,
        ingredients: [
          { productId: 'prodB', quantity: 50, unit: 'g', isOptional: false }
        ]
      });

      mockProductsRepo.findByIds.mockResolvedValue([{ _id: 'prodB', name: 'Salt' }]);

      // Pantry has 100g (more than enough)
      mockPantryRepo.findActiveByHousehold.mockResolvedValue([{ productId: 'prodB', quantity: 100 }]);

      const result = await service.calculateGap('hh1', 'plan2');
      expect(result.missingItems.length).toBe(0);
    });

    it('aggregates duplicate ingredients and sorts by product name', async () => {
      mockMealRepo.findById.mockResolvedValue({
        _id: 'plan-multi',
        days: [
          {
            meals: [
              { recipeId: 'recipeA', servings: 1 },
              { recipeId: 'recipeB', servings: 1 },
            ]
          }
        ]
      });

      mockRecipesRepo.findById.mockImplementation(async (id) => {
        if (id === 'recipeA') {
          return {
            _id: 'recipeA', servings: 1,
            ingredients: [{ productId: 'prod1', quantity: 10, isOptional: false }]
          };
        }
        return {
          _id: 'recipeB', servings: 1,
          ingredients: [
            { productId: 'prod1', quantity: 20, isOptional: false },
            { productId: 'prod2', quantity: 5, isOptional: false },
          ]
        };
      });

      mockProductsRepo.findByIds.mockResolvedValue([
        { _id: 'prod1', name: 'Banana' },
        { _id: 'prod2', name: 'Apple' },
      ]);

      mockPantryRepo.findActiveByHousehold.mockResolvedValue([]);

      const result = await service.calculateGap('hh1', 'plan-multi');

      expect(result.missingItems).toHaveLength(2);
      expect(result.missingItems[0].productName).toBe('Apple');
      expect(result.missingItems[1].productName).toBe('Banana');
      expect(result.missingItems[1].requiredQuantity).toBe(30);
    });

    it('covers fallback paths for missing list, recipe properties and pantry quantities', async () => {
      mockMealRepo.findById.mockResolvedValue({
        _id: 'plan-empty',
      });
      mockProductsRepo.findByIds.mockResolvedValue([]);
      mockPantryRepo.findActiveByHousehold.mockResolvedValue([]);

      let res = await service.calculateGap('hh1', 'plan-empty');
      expect(res.missingItems).toHaveLength(0);

      mockMealRepo.findById.mockResolvedValue({
        _id: 'plan-missing',
        days: [
          {
            meals: [{ recipeId: 'recipeC', servings: 1 }]
          }
        ]
      });

      mockRecipesRepo.findById.mockResolvedValue({
        _id: 'recipeC',
        servings: 1,
        ingredients: [
          { productId: 'prod3', quantity: 10, isOptional: false }
        ]
      });

      mockProductsRepo.findByIds.mockResolvedValue([
        { _id: 'prod3' }
      ]);

      mockPantryRepo.findActiveByHousehold.mockResolvedValue([
        { productId: 'prod3' }
      ]);

      res = await service.calculateGap('hh1', 'plan-missing');
      expect(res.missingItems).toHaveLength(1);
      const itm = res.missingItems[0]!;
      expect(itm.unit).toBe('g');
      expect(itm.productName).toBe('Unknown Ingredient');
      expect(itm.category).toBe('other');
      expect(itm.pantryQuantity).toBe(0);
    });

    it('skips optional ingredients, handles missing recipes and defaults servings to 1', async () => {
      mockMealRepo.findById.mockResolvedValue({
        _id: 'plan-edge',
        days: [
          {
            meals: [
              { recipeId: 'recipeExist', servings: 2 },
              { recipeId: 'recipeNotExist', servings: 1 },
            ]
          }
        ]
      });

      mockRecipesRepo.findById.mockImplementation(async (id) => {
        if (id === 'recipeExist') {
          return {
            _id: 'recipeExist',
            servings: 0,
            ingredients: [
              { productId: 'prodIng', quantity: 5, isOptional: false },
              { productId: 'prodOptional', quantity: 10, isOptional: true },
            ]
          };
        }
        return null;
      });

      mockProductsRepo.findByIds.mockResolvedValue([{ _id: 'prodIng', name: 'Ingredient' }]);
      mockPantryRepo.findActiveByHousehold.mockResolvedValue([]);

      const result = await service.calculateGap('hh1', 'plan-edge');
      expect(result.missingItems).toHaveLength(1);
      expect(result.missingItems[0].productId).toBe('prodIng');
      expect(result.missingItems[0].requiredQuantity).toBe(10);
    });
  });
});
