import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ShoppingListsService } from './shopping-lists.service.js';
import { NotFoundError } from '../../common/errors.js';

describe('ShoppingListsService', () => {
  let service: ShoppingListsService;

  const mockListsRepo = {
    list: vi.fn(),
    findById: vi.fn(),
    create: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
    addItem: vi.fn(),
    updateItem: vi.fn(),
    removeItem: vi.fn(),
  };

  const mockGapService = {
    calculateGap: vi.fn(),
  };

  const mockPantryService = {
    create: vi.fn(),
  };

  const mockProductsRepo = {
    findById: vi.fn(),
  };

  const mockPricesService = {
    estimatePrice: vi.fn(),
    recordPrice: vi.fn(),
    compareStores: vi.fn(),
  };

  const mockMealPlanRepo = {
    findById: vi.fn(),
    update: vi.fn(),
  };

  beforeEach(() => {
    vi.clearAllMocks();
    service = new ShoppingListsService({
      shoppingListsRepository: mockListsRepo as any,
      shoppingGapService: mockGapService as any,
      pantryService: mockPantryService as any,
      productsRepository: mockProductsRepo as any,
      pricesService: mockPricesService as any,
      mealPlanRepository: mockMealPlanRepo as any,
    });
  });

  describe('create', () => {
    it('populates initial estimates and auto-generates internal tracking UUIDs', async () => {
      mockProductsRepo.findById.mockResolvedValue({ category: 'produce' });
      mockPricesService.estimatePrice.mockResolvedValue(5);
      mockListsRepo.create.mockImplementation(arg => arg);

      const result = await service.create(
        {
          name: 'Weekly run',
          items: [{ productId: 'p1', quantity: 1, unit: 'g' as any }],
        },
        'hh1',
        'u1'
      );

      expect(result.items).toHaveLength(1);
      expect(result.items[0].id).toBeDefined();
      expect(result.items[0].estimatedPrice).toBe(5);
      expect(result.totalEstimatedCost).toBe(5);
    });
  });

  describe('addItem', () => {
    it('hydrates single product pricing and pushes to list repository', async () => {
      mockListsRepo.findById.mockResolvedValue({ _id: 'list1' });
      mockProductsRepo.findById.mockResolvedValue({ category: 'meat' });
      mockPricesService.estimatePrice.mockResolvedValue(10);
      mockListsRepo.addItem.mockResolvedValue({ _id: 'list1' });

      const res = await service.addItem('list1', 'hh1', {
        productId: 'prodA',
        quantity: 1,
        unit: 'g' as any,
      });

      expect(mockListsRepo.addItem).toHaveBeenCalledWith(
        'list1',
        'hh1',
        expect.objectContaining({
          productId: 'prodA',
          estimatedPrice: 10,
          category: 'meat',
        })
      );
      expect(res.addedItem.id).toBeDefined();
    });
  });

  describe('updateItem', () => {
    it('injects correct checked timestamps on check-off state mutations', async () => {
      mockListsRepo.updateItem.mockResolvedValue({});

      await service.updateItem('list1', 'hh1', 'itemA', { checked: true }, 'userIdX');

      expect(mockListsRepo.updateItem).toHaveBeenCalledWith(
        'list1',
        'hh1',
        'itemA',
        expect.objectContaining({
          checked: true,
          checkedBy: 'userIdX',
          checkedAt: expect.any(Date),
        })
      );
    });
  });

  describe('createFromMealPlan', () => {
    it('runs shopping gap report and populates distinct grocery array linked back to source plan', async () => {
      mockMealPlanRepo.findById.mockResolvedValue({ _id: 'mp1', weekStartDate: '2026-05-18' });
      mockGapService.calculateGap.mockResolvedValue({
        missingItems: [
          { productId: 'gapProd', missingQuantity: 5, unit: 'g', category: 'dairy' }
        ]
      });
      mockPricesService.estimatePrice.mockResolvedValue(2);
      mockListsRepo.create.mockResolvedValue({ _id: 'newList1' });

      const res = await service.createFromMealPlan('mp1', 'hh1', 'userIdZ');

      expect(mockGapService.calculateGap).toHaveBeenCalledWith('hh1', 'mp1');
      expect(mockListsRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({
          mealPlanId: 'mp1',
          items: [
            expect.objectContaining({
              productId: 'gapProd',
              quantity: 5,
              estimatedPrice: 2,
            })
          ]
        })
      );
      // Assert link-back invocation
      expect(mockMealPlanRepo.update).toHaveBeenCalledWith('mp1', 'hh1', {
        shoppingListId: 'newList1',
      });
    });
  });

  describe('syncCheckedToPantry', () => {
    it('iterates checked items, creating pantry items and recording actual prices in ledger', async () => {
      const mockList = {
        _id: 'list1',
        preferredStoreId: 'storeA',
        items: [
          {
            id: 'itmA',
            productId: 'p1',
            checked: true,
            addedToPantry: false,
            quantity: 2,
            unit: 'g',
            actualPrice: 15.50,
          }
        ]
      };
      mockListsRepo.findById.mockResolvedValue(mockList);

      const summary = await service.syncCheckedToPantry('list1', 'hh1', 'userAlpha');

      // 1. Verify pantry promotion
      expect(mockPantryService.create).toHaveBeenCalledWith(
        expect.objectContaining({
          productId: 'p1',
          quantity: 2,
          purchasePrice: 15.50,
          storeId: 'storeA',
        }),
        'hh1',
        'userAlpha'
      );

      // 2. Verify point-in-time ledger price logging
      expect(mockPricesService.recordPrice).toHaveBeenCalledWith(
        expect.objectContaining({
          productId: 'p1',
          price: 15.50,
          storeId: 'storeA',
        }),
        'hh1',
        'userAlpha'
      );

      // 3. Verify completion bit toggled in list subdocument
      expect(mockListsRepo.updateItem).toHaveBeenCalledWith('list1', 'hh1', 'itmA', {
        addedToPantry: true,
      });

      expect(summary.addedCount).toBe(1);
      expect(summary.pricesLogged).toBe(1);
    });
  });

  describe('getStoreComparison', () => {
    it('collates individual store deviation lists to rank optimized single store trips', async () => {
      mockListsRepo.findById.mockResolvedValue({
        items: [{ productId: 'p1' }]
      });
      mockPricesService.compareStores.mockResolvedValue([
        { storeId: 'sA', storeName: 'Walmart', latestPrice: 10 },
        { storeId: 'sB', storeName: 'Whole Foods', latestPrice: 18 },
      ]);

      const comparison = await service.getStoreComparison('list1', 'hh1');
      expect(comparison.singleStoreOptions).toHaveLength(2);
      expect(comparison.singleStoreOptions[0].storeName).toBe('Walmart');
      expect(comparison.singleStoreOptions[0].estimatedTotal).toBe(10);
    });
  });
});
