import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SuggestionEngineService } from '../../../src/modules/meal-plans/suggestion-engine.service.js';
import type { RecipesRepository } from '../../../src/modules/recipes/recipes.repository.js';
import type { PantryRepository } from '../../../src/modules/pantry/pantry.repository.js';
import type { MealPlanRepository } from '../../../src/modules/meal-plans/meal-plans.repository.js';
import type { NutritionTargetRepository } from '../../../src/modules/nutrition-targets/nutrition-target.repository.js';

describe(SuggestionEngineService.name, () => {
  let service: SuggestionEngineService;
  let mockRecipesRepo: { [K in keyof RecipesRepository]: ReturnType<typeof vi.fn> };
  let mockPantryRepo: { [K in keyof PantryRepository]: ReturnType<typeof vi.fn> };
  let mockMealPlanRepo: { [K in keyof MealPlanRepository]: ReturnType<typeof vi.fn> };
  let mockNutritionRepo: { [K in keyof NutritionTargetRepository]: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-05-20T00:00:00Z'));

    mockRecipesRepo = {
      findByHousehold: vi.fn(),
    } as never;

    mockPantryRepo = {
      findActiveByHousehold: vi.fn(),
    } as never;

    mockMealPlanRepo = {
      findByHousehold: vi.fn(),
    } as never;

    mockNutritionRepo = {
      findByUser: vi.fn(),
    } as never;

    service = new SuggestionEngineService({
      recipesRepository: mockRecipesRepo as unknown as RecipesRepository,
      pantryRepository: mockPantryRepo as unknown as PantryRepository,
      mealPlanRepository: mockMealPlanRepo as unknown as MealPlanRepository,
      nutritionTargetRepository: mockNutritionRepo as unknown as NutritionTargetRepository,
    });
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  describe('getSuggestions', () => {
    it('correctly ranks recipes based on inventory coverage and freshness', async () => {
      // 1. Set up recipes:
      // - Recipe A: Needs Product 1 (2 units) and Product 2 (1 unit)
      // - Recipe B: Needs Product 3 (1 unit)
      const recipeA = {
        _id: 'recipeA',
        name: 'Recipe A',
        ingredients: [
          { productId: 'prod1', quantity: 2, isOptional: false },
          { productId: 'prod2', quantity: 1, isOptional: false },
        ],
        perServingNutrition: { calories: 400, protein: 30, carbs: 40, fat: 10 }, // balanced
      };

      const recipeB = {
        _id: 'recipeB',
        name: 'Recipe B',
        ingredients: [
          { productId: 'prod3', quantity: 1, isOptional: false },
        ],
        perServingNutrition: { calories: 600, protein: 10, carbs: 100, fat: 15 }, // high carb
      };

      mockRecipesRepo.findByHousehold.mockResolvedValue({
        data: [recipeA, recipeB],
        pagination: { hasMore: false },
      });

      // 2. Set up Pantry inventory:
      // We have Product 1 in abundance (expiringSoon).
      // We have Product 2 (fresh).
      // Product 3 is NOT in pantry.
      mockPantryRepo.findActiveByHousehold.mockResolvedValue([
        {
          productId: 'prod1',
          quantity: 10,
          freshnessEstimate: { daysRemaining: 2, urgency: 'expiringSoon' },
        },
        {
          productId: 'prod2',
          quantity: 5,
          freshnessEstimate: { daysRemaining: 15, urgency: 'fresh' },
        },
      ]);

      // 3. Set up standard nutrition target (Maintenance: 30p/40c/30f split)
      // Macro split match logic:
      // Recipe A: 400cals, 30g Protein(120cals=30%), 40g Carbs(160cals=40%), 10g Fat(90cals=22.5%) -> highly aligned!
      mockNutritionRepo.findByUser.mockResolvedValue({
        dailyCalories: 2000,
        proteinG: 150, // (150 * 4) = 600cals (30%)
        carbsG: 200, // (200 * 4) = 800cals (40%)
        fatG: 67, // (67 * 9) = 603cals (30%)
      });

      // 4. Set up recent meal plans (empty history -> 100% Variety for all)
      mockMealPlanRepo.findByHousehold.mockResolvedValue({
        data: [],
      });

      // Run suggestion fetch
      const suggestions = await service.getSuggestions('hh1', 'user1');

      // Assertions:
      expect(suggestions.length).toBe(2);

      // Recipe A should clearly rank #1 (100% Coverage, using urgent items, highly nutritious match)
      const top = suggestions[0]!;
      expect(top.recipeId).toBe('recipeA');
      expect(top.scores.coverage).toBe(1); // full coverage
      // Urgency: (expiringSoon[0.7] + fresh[0.1]) / 2 = 0.4
      expect(top.scores.urgency).toBeGreaterThan(0.3);
      expect(top.scores.variety).toBe(1); // never eaten

      // Recipe B should have 0 coverage and thus lower totalScore
      const bottom = suggestions[1]!;
      expect(bottom.recipeId).toBe('recipeB');
      expect(bottom.scores.coverage).toBe(0);
      expect(bottom.totalScore).toBeLessThan(top.totalScore);
    });

    it('penalizes recipes eaten recently (Variety score)', async () => {
      const recipeX = {
        _id: 'recipeX',
        name: 'Recipe X',
        ingredients: [],
        perServingNutrition: { calories: 100, protein: 5, carbs: 10, fat: 2 },
      };

      mockRecipesRepo.findByHousehold.mockResolvedValue({ data: [recipeX] });
      mockPantryRepo.findActiveByHousehold.mockResolvedValue([]);
      mockNutritionRepo.findByUser.mockResolvedValue(null);

      // Fake history: Recipe X was eaten 7 days ago
      const date7DaysAgo = new Date();
      date7DaysAgo.setDate(date7DaysAgo.getDate() - 7);
      const dateStr = date7DaysAgo.toISOString().split('T')[0];

      mockMealPlanRepo.findByHousehold.mockResolvedValue({
        data: [
          {
            days: [
              {
                date: dateStr,
                meals: [{ recipeId: 'recipeX' }],
              },
            ],
          },
        ],
      });

      const suggestions = await service.getSuggestions('hh1', 'user1');
      
      // Variety calculation: 7 days ago / 14 days = 0.5
      expect(suggestions[0]!.scores.variety).toBeCloseTo(0.5, 1);
    });

    it('triggers reasoning branches for partial coverage and urgent items', async () => {
      const recipeC = {
        _id: 'recipeC',
        name: 'Recipe C',
        ingredients: [
          { productId: 'prod1', quantity: 10, isOptional: false },
          { productId: 'prod2', quantity: 10, isOptional: false },
        ],
      };

      mockRecipesRepo.findByHousehold.mockResolvedValue({ data: [recipeC] });
      
      // 1. Coverage: (10/10 + 5/10)/2 = 0.75 (hits >0.5)
      // 2. Urgency: both set to urgent = 1.0 (hits >0.7)
      mockPantryRepo.findActiveByHousehold.mockResolvedValue([
        { productId: 'prod1', quantity: 10, freshnessEstimate: { urgency: 'urgent' } },
        { productId: 'prod2', quantity: 5, freshnessEstimate: { urgency: 'urgent' } },
      ]);
      mockNutritionRepo.findByUser.mockResolvedValue(null);
      mockMealPlanRepo.findByHousehold.mockResolvedValue({ data: [] });

      const suggestions = await service.getSuggestions('hh1', 'user1');
      expect(suggestions[0]!.scores.coverage).toBe(0.75);
      expect(suggestions[0]!.scores.urgency).toBe(1);
      expect(suggestions[0]!.reasoning).toContain('Uses several ingredients already stocked in your pantry.');
      expect(suggestions[0]!.reasoning).toContain('High priority: Saves expiring pantry items from going to waste!');
    });

    it('triggers reasoning for moderately soon-to-expire items', async () => {
      const recipeD = {
        _id: 'recipeD',
        name: 'Recipe D',
        ingredients: [{ productId: 'prod1', quantity: 5, isOptional: false }],
      };

      mockRecipesRepo.findByHousehold.mockResolvedValue({ data: [recipeD] });
      
      // Urgency soon/expiringSoon has weight 0.7 (hits >0.4 branch)
      mockPantryRepo.findActiveByHousehold.mockResolvedValue([
        { productId: 'prod1', quantity: 5, freshnessEstimate: { urgency: 'expiringSoon' } },
      ]);
      mockNutritionRepo.findByUser.mockResolvedValue(null);
      mockMealPlanRepo.findByHousehold.mockResolvedValue({ data: [] });

      const suggestions = await service.getSuggestions('hh1', 'user1');
      expect(suggestions[0]!.scores.urgency).toBe(0.7);
      expect(suggestions[0]!.reasoning).toContain('Helps use up items that should be consumed soon.');
    });

    it('aggregates duplicate pantry items and handles normal/default urgencies', async () => {
      const recipeE = {
        _id: 'recipeE',
        name: 'Recipe E',
        ingredients: [
          { productId: 'prod1', quantity: 5, isOptional: false },
        ],
      };

      mockRecipesRepo.findByHousehold.mockResolvedValue({ data: [recipeE] });
      
      mockPantryRepo.findActiveByHousehold.mockResolvedValue([
        { productId: 'prod1', quantity: 2, freshnessEstimate: { daysRemaining: 5, urgency: 'normal' } },
        { productId: 'prod1', quantity: 3, freshnessEstimate: { daysRemaining: 10, urgency: 'unknown-type' } },
      ]);
      mockNutritionRepo.findByUser.mockResolvedValue(null);
      mockMealPlanRepo.findByHousehold.mockResolvedValue({ data: [] });

      const suggestions = await service.getSuggestions('hh1', 'user1');
      expect(suggestions[0]!.scores.coverage).toBe(1);
      expect(suggestions[0]!.scores.urgency).toBe(0.3);
    });

    it('covers boundary logic for nameless recipes, custom meals, default targets and private weights', async () => {
      // 1. Nameless recipe and recipe without ingredients
      const rawRecipe = { _id: 'recipeMissingProps' }; 
      mockRecipesRepo.findByHousehold.mockResolvedValue({ data: [rawRecipe] });

      // 2. Active target with partial/falsy info
      mockNutritionRepo.findByUser.mockResolvedValue({ dailyCalories: 0, proteinG: 0 });

      // 3. Last eaten containing a custom meal without recipeId (should continue)
      mockMealPlanRepo.findByHousehold.mockResolvedValue({
        data: [
          {
            days: [
              {
                date: '2026-05-19',
                meals: [
                  { customName: 'Snack' }, // no recipeId!
                ],
              },
            ],
          },
        ],
      });

      mockPantryRepo.findActiveByHousehold.mockResolvedValue([]);

      const suggestions = await service.getSuggestions('hh1', 'user1');
      
      expect(suggestions).toHaveLength(1);

      // 4. Direct call to getUrgencyWeight default branch
      const defaultWeight = (service as any).getUrgencyWeight('mystery-status');
      expect(defaultWeight).toBe(0);
    });
  });
});
