import { describe, it, expect, vi, beforeEach } from 'vitest';

const { mockFind, mockSave, mockInsertMany, mockAggregate } = vi.hoisted(() => ({
  mockFind: vi.fn(),
  mockSave: vi.fn(),
  mockInsertMany: vi.fn(),
  mockAggregate: vi.fn(),
}));

vi.mock('../../../src/schemas/cabinet-event.schema.js', () => {
  const chain = () => ({
    sort: vi.fn().mockReturnThis(),
    limit: vi.fn().mockReturnThis(),
    lean: vi.fn().mockReturnThis(),
    exec: mockFind,
  });

  const aggChain = () => ({
    exec: mockAggregate,
  });

  class FakeModel {
    data: unknown;
    constructor(data: unknown) {
      this.data = data;
    }
    save = mockSave;
    toObject() {
      return this.data;
    }
    static find = vi.fn(() => chain());
    static insertMany = mockInsertMany;
    static aggregate = vi.fn(() => aggChain());
  }

  return { CabinetEventModel: FakeModel };
});

import { CabinetEventsRepository } from '../../../src/modules/cabinet-events/cabinet-events.repository.js';

describe(CabinetEventsRepository.name, () => {
  let repo: CabinetEventsRepository;

  const baseEventData = {
    householdId: 'hh1',
    userId: 'user-1',
    cabinetItemId: 'ci-1',
    medicineId: 'med-1',
    medicineName: 'Metformin',
    eventType: 'purchased' as const,
    quantity: 10,
    quantityBefore: 0,
    quantityAfter: 10,
    sourceType: 'manual' as const,
  };

  beforeEach(() => {
    vi.clearAllMocks();
    repo = new CabinetEventsRepository();
  });

  describe('create', () => {
    it('creates and returns a cabinet event', async () => {
      mockSave.mockImplementation(function (this: { toObject: () => unknown }) {
        return Promise.resolve(this);
      });

      const result = await repo.create(baseEventData);

      expect(result).toEqual(baseEventData);
      expect(mockSave).toHaveBeenCalled();
    });
  });

  describe('createMany', () => {
    it('inserts multiple events', async () => {
      const events = [baseEventData, { ...baseEventData, quantity: 5 }];
      mockInsertMany.mockResolvedValue(events);

      const result = await repo.createMany(events);

      expect(result).toEqual(events);
      expect(mockInsertMany).toHaveBeenCalledWith(events);
    });
  });

  describe('findByHousehold', () => {
    it('returns paginated events with no filters', async () => {
      const items = [{ _id: 'ev-1', quantity: 10 }];
      mockFind.mockResolvedValue(items);

      const result = await repo.findByHousehold('hh1', { limit: 20 });

      expect(result.data).toEqual(items);
      expect(result.pagination.hasMore).toBe(false);
    });

    it('handles cursor-based pagination', async () => {
      const items = [{ _id: 'ev-2', quantity: 5 }];
      mockFind.mockResolvedValue(items);

      const cursor = Buffer.from('ev-1').toString('base64');
      const result = await repo.findByHousehold('hh1', { cursor, limit: 20 });

      expect(result.data).toEqual(items);
      expect(result.pagination.hasMore).toBe(false);
    });

    it('sets hasMore when more items exist', async () => {
      const items = Array.from({ length: 3 }, (_, i) => ({ _id: `ev-${i}`, quantity: i }));
      mockFind.mockResolvedValue(items);

      const result = await repo.findByHousehold('hh1', { limit: 2 });

      expect(result.data).toHaveLength(2);
      expect(result.pagination.hasMore).toBe(true);
      expect(result.pagination.cursor).toBeTruthy();
    });

    it('returns null cursor when no data', async () => {
      mockFind.mockResolvedValue([]);

      const result = await repo.findByHousehold('hh1', { limit: 20 });

      expect(result.pagination.cursor).toBeNull();
      expect(result.pagination.hasMore).toBe(false);
    });

    it('filters by medicineId', async () => {
      mockFind.mockResolvedValue([]);

      await repo.findByHousehold('hh1', { medicineId: 'med-1', limit: 20 });

      expect(mockFind).toHaveBeenCalled();
    });

    it('filters by eventType', async () => {
      mockFind.mockResolvedValue([]);

      await repo.findByHousehold('hh1', { eventType: 'purchased' as never, limit: 20 });

      expect(mockFind).toHaveBeenCalled();
    });

    it('filters by startDate only', async () => {
      mockFind.mockResolvedValue([]);

      await repo.findByHousehold('hh1', {
        startDate: '2024-01-01T00:00:00.000Z',
        limit: 20,
      });

      expect(mockFind).toHaveBeenCalled();
    });

    it('filters by endDate only', async () => {
      mockFind.mockResolvedValue([]);

      await repo.findByHousehold('hh1', {
        endDate: '2024-12-31T00:00:00.000Z',
        limit: 20,
      });

      expect(mockFind).toHaveBeenCalled();
    });

    it('filters by both startDate and endDate', async () => {
      mockFind.mockResolvedValue([]);

      await repo.findByHousehold('hh1', {
        startDate: '2024-01-01T00:00:00.000Z',
        endDate: '2024-12-31T00:00:00.000Z',
        limit: 20,
      });

      expect(mockFind).toHaveBeenCalled();
    });
  });

  describe('findByCabinetItem', () => {
    it('returns paginated events for a cabinet item', async () => {
      const items = [{ _id: 'ev-1', cabinetItemId: 'ci-1' }];
      mockFind.mockResolvedValue(items);

      const result = await repo.findByCabinetItem('hh1', 'ci-1', { limit: 20 });

      expect(result.data).toEqual(items);
      expect(result.pagination.hasMore).toBe(false);
    });

    it('handles cursor-based pagination', async () => {
      const items = [{ _id: 'ev-2' }];
      mockFind.mockResolvedValue(items);

      const cursor = Buffer.from('ev-1').toString('base64');
      const result = await repo.findByCabinetItem('hh1', 'ci-1', { cursor, limit: 20 });

      expect(result.data).toEqual(items);
      expect(result.pagination.hasMore).toBe(false);
    });

    it('sets hasMore when more items exist', async () => {
      const items = Array.from({ length: 3 }, (_, i) => ({ _id: `ev-${i}` }));
      mockFind.mockResolvedValue(items);

      const result = await repo.findByCabinetItem('hh1', 'ci-1', { limit: 2 });

      expect(result.data).toHaveLength(2);
      expect(result.pagination.hasMore).toBe(true);
      expect(result.pagination.cursor).toBeTruthy();
    });

    it('returns null cursor when no data', async () => {
      mockFind.mockResolvedValue([]);

      const result = await repo.findByCabinetItem('hh1', 'ci-1', { limit: 20 });

      expect(result.pagination.cursor).toBeNull();
      expect(result.pagination.hasMore).toBe(false);
    });
  });

  describe('getSpendingSummary', () => {
    it('returns spending summary with default month period', async () => {
      const byMedicine = [
        {
          _id: 'med-1',
          medicineName: 'Metformin',
          totalSpent: 100,
          totalQuantity: 10,
          avgUnitPrice: 10,
          purchaseCount: 2,
          currency: 'USD',
        },
      ];
      const byPeriod = [{ _id: '2024-01', totalSpent: 100 }];

      mockAggregate.mockResolvedValueOnce(byMedicine).mockResolvedValueOnce(byPeriod);

      const result = await repo.getSpendingSummary('hh1', { period: 'month' });

      expect(result.totalSpent).toBe(100);
      expect(result.currency).toBe('USD');
      expect(result.byMedicine).toHaveLength(1);
      expect(result.byMedicine[0].medicineId).toBe('med-1');
      expect(result.byMedicine[0].medicineName).toBe('Metformin');
      expect(result.byMedicine[0].totalSpent).toBe(100);
      expect(result.byMedicine[0].totalQuantity).toBe(10);
      expect(result.byMedicine[0].avgUnitPrice).toBe(10);
      expect(result.byMedicine[0].purchaseCount).toBe(2);
      expect(result.byPeriod).toHaveLength(1);
      expect(result.byPeriod[0].period).toBe('2024-01');
      expect(result.byPeriod[0].totalSpent).toBe(100);
    });

    it('returns null currency when no medicine data', async () => {
      mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]);

      const result = await repo.getSpendingSummary('hh1', { period: 'month' });

      expect(result.totalSpent).toBe(0);
      expect(result.currency).toBeNull();
      expect(result.byMedicine).toHaveLength(0);
      expect(result.byPeriod).toHaveLength(0);
    });

    it('filters by medicineId', async () => {
      mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]);

      await repo.getSpendingSummary('hh1', { period: 'month', medicineId: 'med-1' });

      expect(mockAggregate).toHaveBeenCalledTimes(2);
    });

    it('filters by startDate only', async () => {
      mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]);

      await repo.getSpendingSummary('hh1', {
        period: 'month',
        startDate: '2024-01-01T00:00:00.000Z',
      });

      expect(mockAggregate).toHaveBeenCalledTimes(2);
    });

    it('filters by endDate only', async () => {
      mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]);

      await repo.getSpendingSummary('hh1', {
        period: 'month',
        endDate: '2024-12-31T00:00:00.000Z',
      });

      expect(mockAggregate).toHaveBeenCalledTimes(2);
    });

    it('filters by both startDate and endDate', async () => {
      mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]);

      await repo.getSpendingSummary('hh1', {
        period: 'month',
        startDate: '2024-01-01T00:00:00.000Z',
        endDate: '2024-12-31T00:00:00.000Z',
      });

      expect(mockAggregate).toHaveBeenCalledTimes(2);
    });

    it('uses quarter date format', async () => {
      mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]);

      await repo.getSpendingSummary('hh1', { period: 'quarter' });

      expect(mockAggregate).toHaveBeenCalledTimes(2);
    });

    it('uses year date format', async () => {
      mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]);

      await repo.getSpendingSummary('hh1', { period: 'year' });

      expect(mockAggregate).toHaveBeenCalledTimes(2);
    });

    it('handles null currency in first medicine entry', async () => {
      const byMedicine = [
        {
          _id: 'med-1',
          medicineName: 'Metformin',
          totalSpent: 50,
          totalQuantity: 5,
          avgUnitPrice: 10,
          purchaseCount: 1,
          currency: null,
        },
      ];
      mockAggregate.mockResolvedValueOnce(byMedicine).mockResolvedValueOnce([]);

      const result = await repo.getSpendingSummary('hh1', { period: 'month' });

      expect(result.currency).toBeNull();
    });
  });

  describe('getAvgUnitPriceByMedicine', () => {
    it('returns empty map when no medicine ids provided', async () => {
      const result = await repo.getAvgUnitPriceByMedicine('hh1', []);

      expect(result).toEqual(new Map());
      expect(mockAggregate).not.toHaveBeenCalled();
    });

    it('returns map of avg unit prices', async () => {
      const results = [
        { _id: 'med-1', avgUnitPrice: 10.5, currency: 'USD', totalSpent: 105, totalQuantity: 10 },
        { _id: 'med-2', avgUnitPrice: 5.0, currency: 'EUR', totalSpent: 50, totalQuantity: 10 },
      ];
      mockAggregate.mockResolvedValue(results);

      const map = await repo.getAvgUnitPriceByMedicine('hh1', ['med-1', 'med-2']);

      expect(map.size).toBe(2);
      expect(map.get('med-1')).toEqual({ avgUnitPrice: 10.5, currency: 'USD' });
      expect(map.get('med-2')).toEqual({ avgUnitPrice: 5.0, currency: 'EUR' });
    });

    it('handles null currency in results', async () => {
      const results = [
        { _id: 'med-1', avgUnitPrice: 10, currency: null, totalSpent: 100, totalQuantity: 10 },
      ];
      mockAggregate.mockResolvedValue(results);

      const map = await repo.getAvgUnitPriceByMedicine('hh1', ['med-1']);

      expect(map.get('med-1')).toEqual({ avgUnitPrice: 10, currency: null });
    });
  });
});
