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

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

vi.mock('../../../src/schemas/purchase.schema.js', () => {
  const findChain = () => ({
    sort: vi.fn().mockReturnThis(),
    limit: vi.fn().mockReturnThis(),
    lean: vi.fn().mockReturnThis(),
    exec: mockFind,
  });
  const findOneChain = () => ({ lean: vi.fn().mockReturnThis(), exec: mockFindOne });
  const updateChain = () => ({ exec: mockFindOneAndUpdate });
  const aggregateChain = () => ({ exec: mockAggregate });

  class FakeModel {
    data: unknown;
    constructor(data: unknown) {
      this.data = data;
    }
    save = mockSave;
    toObject() {
      return this.data;
    }
    static find = vi.fn(() => findChain());
    static findOne = vi.fn(() => findOneChain());
    static findOneAndUpdate = vi.fn(() => updateChain());
    static aggregate = vi.fn(() => aggregateChain());
  }
  return { PurchaseModel: FakeModel };
});

import { PurchasesRepository } from '../../../src/modules/purchases/purchases.repository.js';

const makeItem = (overrides = {}) => ({
  medicineProductId: 'mp-1',
  medicineId: 'med-1',
  name: 'Tylenol',
  quantity: 30,
  unit: 'tablet',
  addedToCabinet: false,
  ...overrides,
});

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

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

  describe('create', () => {
    it('saves and returns plain object', async () => {
      const data = {
        householdId: 'hh1',
        storeId: 'st-1',
        storeName: 'CVS',
        status: 'in_cabinet',
        items: [makeItem()],
        purchasedAt: new Date(),
        createdBy: 'u-1',
      };
      mockSave.mockResolvedValue({ toObject: () => data });

      const result = await repo.create(data);

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

  describe('findByHousehold', () => {
    it('returns paginated items without hasMore', async () => {
      const items = [{ _id: { toString: () => 'p-1' }, householdId: 'hh1' }];
      mockFind.mockResolvedValue(items);

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

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

    it('returns hasMore and cursor when results exceed limit', async () => {
      const items = Array.from({ length: 21 }, (_, i) => ({ _id: { toString: () => `p-${i}` } }));
      mockFind.mockResolvedValue(items);

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

      expect(result.data).toHaveLength(20);
      expect(result.pagination.hasMore).toBe(true);
      expect(result.pagination.cursor).not.toBeNull();
    });

    it('filters by status when provided', async () => {
      mockFind.mockResolvedValue([]);
      const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js');

      await repo.findByHousehold('hh1', { limit: 20, status: 'ordered' });

      expect(PurchaseModel.find).toHaveBeenCalledWith(
        expect.objectContaining({ status: 'ordered' }),
      );
    });

    it('filters by storeId when provided', async () => {
      mockFind.mockResolvedValue([]);
      const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js');

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

      expect(PurchaseModel.find).toHaveBeenCalledWith(expect.objectContaining({ storeId: 'st-1' }));
    });

    it('applies cursor filter when provided', async () => {
      mockFind.mockResolvedValue([]);
      const cursor = Buffer.from('p-1').toString('base64');
      const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js');

      await repo.findByHousehold('hh1', { limit: 20, cursor });

      expect(PurchaseModel.find).toHaveBeenCalledWith(
        expect.objectContaining({ _id: { $lt: 'p-1' } }),
      );
    });
  });

  describe('findById', () => {
    it('returns purchase when found', async () => {
      const purchase = { _id: 'p-1', householdId: 'hh1' };
      mockFindOne.mockResolvedValue(purchase);

      const result = await repo.findById('p-1', 'hh1');

      expect(result).toEqual(purchase);
    });

    it('returns null when not found', async () => {
      mockFindOne.mockResolvedValue(null);

      const result = await repo.findById('missing', 'hh1');

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

  describe('update', () => {
    it('updates notes and returns updated doc', async () => {
      const updated = { _id: 'p-1', notes: 'new note' };
      mockFindOneAndUpdate.mockResolvedValue(updated);

      const result = await repo.update('p-1', 'hh1', { notes: 'new note' });

      expect(result).toEqual(updated);
    });

    it('returns null when purchase not found', async () => {
      mockFindOneAndUpdate.mockResolvedValue(null);

      const result = await repo.update('missing', 'hh1', {});

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

    it('includes items in update set when provided', async () => {
      const updated = { _id: 'p-1' };
      mockFindOneAndUpdate.mockResolvedValue(updated);
      const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js');
      const items = [{ name: 'X', quantity: 1, unit: 'tablet', addedToCabinet: false }];

      await repo.update('p-1', 'hh1', { items } as never);

      expect(PurchaseModel.findOneAndUpdate).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({ $set: expect.objectContaining({ items }) }),
        expect.anything(),
      );
    });
  });

  describe('receiveAll', () => {
    it('sets status to in_cabinet and all items addedToCabinet', async () => {
      const updated = { _id: 'p-1', status: 'in_cabinet' };
      mockFindOneAndUpdate.mockResolvedValue(updated);
      const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js');

      const result = await repo.receiveAll('p-1', 'hh1');

      expect(PurchaseModel.findOneAndUpdate).toHaveBeenCalledWith(
        { _id: 'p-1', householdId: 'hh1', isDeleted: false },
        expect.objectContaining({
          $set: expect.objectContaining({ status: 'in_cabinet' }),
        }),
        expect.anything(),
      );
      expect(result).toEqual(updated);
    });
  });

  describe('markItemsAddedToCabinet', () => {
    it('builds per-index update set and calls findOneAndUpdate', async () => {
      const updated = { _id: 'p-1', items: [] };
      mockFindOneAndUpdate.mockResolvedValue(updated);
      const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js');

      const result = await repo.markItemsAddedToCabinet('p-1', 'hh1', [0, 2]);

      expect(PurchaseModel.findOneAndUpdate).toHaveBeenCalledWith(
        { _id: 'p-1', householdId: 'hh1', isDeleted: false },
        expect.objectContaining({
          $set: expect.objectContaining({
            'items.0.addedToCabinet': true,
            'items.2.addedToCabinet': true,
          }),
        }),
        expect.anything(),
      );
      expect(result).toEqual(updated);
    });
  });

  describe('softDelete', () => {
    it('sets isDeleted to true', async () => {
      mockFindOneAndUpdate.mockResolvedValue({ _id: 'p-1', isDeleted: true });

      const result = await repo.softDelete('p-1', 'hh1');

      expect(result).toBeTruthy();
    });
  });

  describe('getPendingMedicineStock', () => {
    it('returns aggregated stock by medicineId', async () => {
      const rows = [{ medicineId: 'med-1', totalUnits: 60 }];
      mockAggregate.mockResolvedValue(rows);

      const result = await repo.getPendingMedicineStock('hh1');

      expect(result).toEqual(rows);
    });

    it('returns empty array when no pending purchases', async () => {
      mockAggregate.mockResolvedValue([]);

      const result = await repo.getPendingMedicineStock('hh1');

      expect(result).toEqual([]);
    });
  });
});
