import { describe, it, expect, vi, beforeEach } from 'vitest';
import { PricesRepository } from '../../../src/modules/prices/prices.repository.js';

const { mockSave, MockPriceRecordModel } = vi.hoisted(() => {
  const mockSave = vi.fn();
  function MockModel(this: { save: typeof mockSave }, data: unknown) {
    Object.assign(this, data);
    this.save = mockSave;
  }
  Object.assign(MockModel, {
    findOne: vi.fn(),
    find: vi.fn(),
    findOneAndUpdate: vi.fn(),
    insertMany: vi.fn(),
    aggregate: vi.fn(),
  });
  return { mockSave, MockPriceRecordModel: MockModel };
});

vi.mock('../../../src/schemas/price-record.schema.js', () => ({
  PriceRecordModel: MockPriceRecordModel,
}));

const { PriceRecordModel } = await import('../../../src/schemas/price-record.schema.js');

function makeChain(result: unknown = null) {
  return {
    sort: vi.fn().mockReturnThis(),
    limit: vi.fn().mockReturnThis(),
    lean: vi.fn().mockReturnThis(),
    exec: vi.fn().mockResolvedValue(result),
  };
}

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

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

  describe('create', () => {
    it('saves and returns new document toObject', async () => {
      const data = { householdId: 'h1', productId: 'p1', productName: 'Apple', storeId: 's1', storeName: 'Store', price: 1, currency: 'USD', quantity: 1, unit: 'g', pricePerUnit: 1, date: new Date(), createdBy: 'u1' };
      mockSave.mockResolvedValue({ toObject: () => ({ ...data, _id: 'id1' }) });

      const result = await repo.create(data);
      expect(mockSave).toHaveBeenCalled();
      expect(result._id).toBe('id1');
    });
  });

  describe('createMany', () => {
    it('inserts multiple records and returns mapped toObjects', async () => {
      const inputs = [{ price: 1 }, { price: 2 }];
      const returns = inputs.map((x, idx) => ({ ...x, _id: `id${idx}`, toObject: function() { return this; } }));
      vi.mocked(PriceRecordModel.insertMany).mockResolvedValue(returns as any);

      const result = await repo.createMany(inputs as any);
      expect(PriceRecordModel.insertMany).toHaveBeenCalledWith(inputs);
      expect(result).toHaveLength(2);
      expect(result[0]._id).toBe('id0');
    });
  });

  describe('findByProduct', () => {
    it('applies complex filters and pagination cursor decoding/encoding', async () => {
      const baseFilter = { householdId: 'h1', productId: 'prod1' };
      const startDate = new Date('2026-01-01').toISOString();
      const endDate = new Date('2026-01-10').toISOString();
      const cursorId = '507f1f77bcf86cd799439011';
      const cursorStr = Buffer.from(cursorId).toString('base64');

      const mockItems = [
        { _id: '607f1f77bcf86cd799439012', price: 10 },
        { _id: '607f1f77bcf86cd799439013', price: 12 }
      ];

      const chain = makeChain(mockItems);
      vi.mocked(PriceRecordModel.find).mockReturnValue(chain as any);

      const result = await repo.findByProduct('h1', 'prod1', {
        storeId: 'st1',
        startDate,
        endDate,
        cursor: cursorStr,
        limit: 2
      });

      expect(PriceRecordModel.find).toHaveBeenCalledWith({
        householdId: 'h1',
        productId: 'prod1',
        storeId: 'st1',
        date: {
          $gte: new Date(startDate),
          $lte: new Date(endDate),
        },
        _id: { $lt: cursorId }
      });
      expect(result.data).toHaveLength(2);
      expect(result.pagination.hasMore).toBe(false);
    });

    it('correctly indicates hasMore and generates next base64 cursor', async () => {
      const mockItems = [
        { _id: '607f1f77bcf86cd799439011', price: 10 },
        { _id: '607f1f77bcf86cd799439012', price: 11 },
        { _id: '607f1f77bcf86cd799439013', price: 12 }
      ];

      const chain = makeChain(mockItems);
      vi.mocked(PriceRecordModel.find).mockReturnValue(chain as any);

      const result = await repo.findByProduct('h1', 'prod1', { limit: 2 });
      expect(result.data).toHaveLength(2);
      expect(result.pagination.hasMore).toBe(true);
      expect(result.pagination.cursor).toBe(Buffer.from('607f1f77bcf86cd799439012').toString('base64'));
    });
  });

  describe('compareStores', () => {
    it('runs group/aggregate queries ordered by deviance', async () => {
      const mockAggResult = [
        { _id: 's1', storeName: 'Cheap', latestPrice: 10, latestPricePerUnit: 1, currency: 'USD', date: new Date() }
      ];
      vi.mocked(PriceRecordModel.aggregate).mockReturnValue({
        exec: vi.fn().mockResolvedValue(mockAggResult)
      } as any);

      const result = await repo.compareStores('h1', 'p1');
      expect(PriceRecordModel.aggregate).toHaveBeenCalled();
      expect(result).toHaveLength(1);
      expect(result[0].storeId).toBe('s1');
      expect(result[0].latestPricePerUnit).toBe(1);
    });
  });

  describe('getLatestForProduct', () => {
    it('queries latest pricing document ordered by date descending', async () => {
      const chain = makeChain({ _id: 'pr1' });
      vi.mocked(PriceRecordModel.findOne).mockReturnValue(chain as any);

      await repo.getLatestForProduct('h1', 'p1', 's1');
      expect(PriceRecordModel.findOne).toHaveBeenCalledWith({ householdId: 'h1', productId: 'p1', storeId: 's1' });
      expect(chain.sort).toHaveBeenCalledWith({ date: -1 });
    });
  });

  describe('getAnalytics', () => {
    it('executes Promise.all parallel pipeline aggregations for periods, buckets, categories, and inflation', async () => {
      const mockExec = vi.fn().mockResolvedValue([]);
      vi.mocked(PriceRecordModel.aggregate).mockReturnValue({
        exec: mockExec
      } as any);

      await repo.getAnalytics('h1');
      // 4 explicit pipeline calls should have fired in Promise.all + inflation alert
      expect(PriceRecordModel.aggregate).toHaveBeenCalledTimes(4);
    });
  });
});
