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

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

vi.mock('../../../src/schemas/cabinet-item.schema.js', () => {
  const chain = () => ({
    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 countChain = () => ({
    exec: mockCountDocuments,
  });

  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 findOne = vi.fn(() => findOneChain());
    static findOneAndUpdate = vi.fn(() => updateChain());
    static countDocuments = vi.fn(() => countChain());
    static aggregate = vi.fn(() => aggChain());
  }

  return { CabinetItemModel: FakeModel };
});

import { CabinetRepository } from '../../../src/modules/cabinet/cabinet.repository.js';

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

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

  describe('findByHousehold', () => {
    it('returns paginated items', async () => {
      const items = [{ _id: 'ci-1', quantity: 30 }];
      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: 'ci-2', quantity: 10 }];
      mockFind.mockResolvedValue(items);

      const cursor = Buffer.from('ci-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: `ci-${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();
    });

    it('filters by medicineId', async () => {
      mockFind.mockResolvedValue([]);
      await repo.findByHousehold('hh1', { medicineId: 'med-1', limit: 20 });
      expect(mockFind).toHaveBeenCalled();
    });

    it('filters by status', async () => {
      mockFind.mockResolvedValue([]);
      await repo.findByHousehold('hh1', {
        status: 'active' as never,
        limit: 20,
      });
      expect(mockFind).toHaveBeenCalled();
    });

    it('filters by expiringWithin', async () => {
      mockFind.mockResolvedValue([]);
      await repo.findByHousehold('hh1', { expiringWithin: 30, limit: 20 });
      expect(mockFind).toHaveBeenCalled();
    });
  });

  describe('findById', () => {
    it('returns item by id and householdId', async () => {
      const item = { _id: 'ci-1', householdId: 'hh1', quantity: 30 };
      mockFindOne.mockResolvedValue(item);

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

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

  describe('getAggregateSummary', () => {
    it('returns aggregate data', async () => {
      const aggregated = [{ _id: 'med-1', totalQuantity: 60, itemCount: 2 }];
      mockAggregate.mockResolvedValue(aggregated);

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

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

  describe('create', () => {
    it('creates and returns cabinet item', async () => {
      const data = {
        medicineId: 'med-1',
        medicineName: 'Metformin',
        medicineStrength: 500,
        medicineStrengthUnit: 'mg',
        medicineForm: 'tablet',
        quantity: 30,
        unit: 'tablet' as const,
      };
      mockSave.mockImplementation(function (this: { toObject: () => unknown }) {
        return Promise.resolve(this);
      });

      const result = await repo.create(data, 'hh1', 'user-1');

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

  describe('update', () => {
    it('updates and returns item', async () => {
      const updated = { _id: 'ci-1', quantity: 25 };
      mockFindOneAndUpdate.mockResolvedValue(updated);

      const result = await repo.update('ci-1', 'hh1', { quantity: 25 });

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

  describe('adjustQuantity', () => {
    it('adjusts quantity and returns updated item', async () => {
      mockFindOne.mockResolvedValue({ _id: 'ci-1', quantity: 30, status: 'active' });
      const updated = { _id: 'ci-1', quantity: 27, status: 'active' };
      mockFindOneAndUpdate.mockResolvedValue(updated);

      const result = await repo.adjustQuantity('ci-1', 'hh1', -3);

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

    it('floors quantity at 0 and sets depleted status', async () => {
      mockFindOne.mockResolvedValue({ _id: 'ci-1', quantity: 2, status: 'active' });
      const updated = { _id: 'ci-1', quantity: 0, status: 'depleted' };
      mockFindOneAndUpdate.mockResolvedValue(updated);

      const result = await repo.adjustQuantity('ci-1', 'hh1', -5);

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

    it('re-activates depleted item when adding stock', async () => {
      mockFindOne.mockResolvedValue({ _id: 'ci-1', quantity: 0, status: 'depleted' });
      const updated = { _id: 'ci-1', quantity: 10, status: 'active' };
      mockFindOneAndUpdate.mockResolvedValue(updated);

      const result = await repo.adjustQuantity('ci-1', 'hh1', 10);

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

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

      const result = await repo.adjustQuantity('ci-missing', 'hh1', 5);

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

  describe('findExpiringSoon', () => {
    it('returns items expiring within N days', async () => {
      const items = [{ _id: 'ci-1', expirationDate: new Date() }];
      mockFind.mockResolvedValue(items);

      const result = await repo.findExpiringSoon('hh1', 30);

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

  describe('countByMedicineId', () => {
    it('returns count', async () => {
      mockCountDocuments.mockResolvedValue(3);

      const result = await repo.countByMedicineId('med-1');

      expect(result).toBe(3);
    });
  });

  describe('softDelete', () => {
    it('soft deletes and returns item', async () => {
      const deleted = { _id: 'ci-1', isDeleted: true };
      mockFindOneAndUpdate.mockResolvedValue(deleted);

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

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

  describe('discard', () => {
    it('zeros quantity, marks depleted and deleted', async () => {
      const discarded = { _id: 'ci-1', quantity: 0, status: 'depleted', isDeleted: true };
      mockFindOneAndUpdate.mockResolvedValue(discarded);

      const result = await repo.discard('ci-1', 'hh1');

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

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

      const result = await repo.discard('ci-missing', 'hh1');

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

  describe('findActiveByMedicineForFEFO', () => {
    it('returns active items sorted by expiration date', async () => {
      const items = [
        { _id: 'ci-1', quantity: 10, expirationDate: new Date('2025-06-01') },
        { _id: 'ci-2', quantity: 20, expirationDate: new Date('2025-12-01') },
      ];
      mockFind.mockResolvedValue(items);

      const result = await repo.findActiveByMedicineForFEFO('hh1', 'med-1');

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

    it('returns empty array when no active items exist', async () => {
      mockFind.mockResolvedValue([]);

      const result = await repo.findActiveByMedicineForFEFO('hh1', 'med-1');

      expect(result).toHaveLength(0);
    });
  });
});
