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

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

vi.mock('../../../src/schemas/pantry-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 = () => ({
    lean: vi.fn().mockReturnThis(),
    exec: mockFindOneAndUpdate,
  });

  const deleteChain = () => ({
    exec: mockFindOneAndDelete,
  });

  const updateByIdChain = () => ({
    exec: mockFindByIdAndUpdate,
  });

  const updateManyChain = () => ({
    exec: mockUpdateMany,
  });

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

  class FakeModel {
    data: unknown;
    constructor(data: unknown) {
      this.data = data;
    }
    save() {
      mockSave(this.data);
      return Promise.resolve({ toObject: () => this.data });
    }
    static find = vi.fn(() => chain());
    static findOne = vi.fn(() => findOneChain());
    static findOneAndUpdate = vi.fn(() => updateChain());
    static findOneAndDelete = vi.fn(() => deleteChain());
    static findByIdAndUpdate = vi.fn(() => updateByIdChain());
    static updateMany = vi.fn(() => updateManyChain());
    static aggregate = vi.fn(() => aggChain());
  }

  return { PantryItemModel: FakeModel };
});

import { PantryRepository } from '../../../src/modules/pantry/pantry.repository.js';

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

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

  describe('findByHousehold', () => {
    it('returns paginated results', async () => {
      const items = [{ _id: { toString: () => 'id1' } }];
      mockFind.mockResolvedValue(items);

      const result = await repo.findByHousehold('hh1', { limit: 20 });
      expect(result.data).toHaveLength(1);
      expect(result.pagination.hasMore).toBe(false);
    });

    it('handles hasMore', async () => {
      const items = Array.from({ length: 3 }, (_, i) => ({
        _id: { toString: () => `id${i}` },
      }));
      mockFind.mockResolvedValue(items);

      const result = await repo.findByHousehold('hh1', { limit: 2 });
      expect(result.data).toHaveLength(2);
      expect(result.pagination.hasMore).toBe(true);
    });

    it('applies storageLocation filter', async () => {
      mockFind.mockResolvedValue([]);
      await repo.findByHousehold('hh1', { storageLocation: 'fridge', limit: 20 });
      expect(mockFind).toHaveBeenCalled();
    });

    it('applies status filter with single value', async () => {
      mockFind.mockResolvedValue([]);
      await repo.findByHousehold('hh1', { status: 'sealed', limit: 20 });
      expect(mockFind).toHaveBeenCalled();
    });

    it('applies status filter with multiple values', async () => {
      mockFind.mockResolvedValue([]);
      await repo.findByHousehold('hh1', { status: 'sealed,opened', limit: 20 });
      expect(mockFind).toHaveBeenCalled();
    });

    it('applies urgency filter', async () => {
      mockFind.mockResolvedValue([]);
      await repo.findByHousehold('hh1', { urgency: 'urgent,check', limit: 20 });
      expect(mockFind).toHaveBeenCalled();
    });

    it('applies single urgency filter', async () => {
      mockFind.mockResolvedValue([]);
      await repo.findByHousehold('hh1', { urgency: 'urgent', limit: 20 });
      expect(mockFind).toHaveBeenCalled();
    });

    it('applies productId filter', async () => {
      mockFind.mockResolvedValue([]);
      await repo.findByHousehold('hh1', { productId: 'p1', limit: 20 });
      expect(mockFind).toHaveBeenCalled();
    });

    it('applies cursor', async () => {
      const cursor = Buffer.from('abc').toString('base64');
      mockFind.mockResolvedValue([]);
      await repo.findByHousehold('hh1', { cursor, limit: 20 });
      expect(mockFind).toHaveBeenCalled();
    });
  });

  describe('findById', () => {
    it('returns item', async () => {
      mockFindOne.mockResolvedValue({ _id: 'id1' });
      const result = await repo.findById('id1', 'hh1');
      expect(result).toEqual({ _id: 'id1' });
    });
  });

  describe('findExpiringSoon', () => {
    it('returns items expiring within days', async () => {
      mockFind.mockResolvedValue([]);
      const result = await repo.findExpiringSoon('hh1', 7);
      expect(result.pagination.hasMore).toBe(false);
    });

    it('supports cursor', async () => {
      const cursor = Buffer.from('abc').toString('base64');
      mockFind.mockResolvedValue([]);
      const result = await repo.findExpiringSoon('hh1', 7, cursor, 20);
      expect(result.pagination.hasMore).toBe(false);
    });
  });

  describe('findActiveByHousehold', () => {
    it('returns active items', async () => {
      mockFind.mockResolvedValue([{ _id: 'id1' }]);
      const result = await repo.findActiveByHousehold('hh1');
      expect(result).toHaveLength(1);
    });
  });

  describe('create', () => {
    it('saves and returns item', async () => {
      const data = { name: 'test' };
      const result = await repo.create(data);
      expect(mockSave).toHaveBeenCalled();
      expect(result).toEqual(data);
    });
  });

  describe('update', () => {
    it('updates and returns item', async () => {
      mockFindOneAndUpdate.mockResolvedValue({ _id: 'id1' });
      const result = await repo.update('id1', 'hh1', { quantity: 3 });
      expect(result).toEqual({ _id: 'id1' });
    });
  });

  describe('updateFreshness', () => {
    it('updates freshness estimate', async () => {
      mockFindByIdAndUpdate.mockResolvedValue(undefined);
      await repo.updateFreshness('id1', { urgency: 'fresh' });
      expect(mockFindByIdAndUpdate).toHaveBeenCalled();
    });

    it('updates freshness and status', async () => {
      mockFindByIdAndUpdate.mockResolvedValue(undefined);
      await repo.updateFreshness('id1', { urgency: 'expired' }, 'expired');
      expect(mockFindByIdAndUpdate).toHaveBeenCalled();
    });
  });

  describe('delete', () => {
    it('deletes item', async () => {
      mockFindOneAndDelete.mockResolvedValue({ _id: 'id1' });
      await repo.delete('id1', 'hh1');
      expect(mockFindOneAndDelete).toHaveBeenCalled();
    });
  });

  describe('getWasteStats', () => {
    it('returns aggregation result', async () => {
      mockAggregate.mockResolvedValue([{ totalConsumed: 5, totalDiscarded: 2 }]);
      const result = await repo.getWasteStats('hh1', new Date(), new Date());
      expect(result).toHaveLength(1);
    });
  });

  describe('getTopWastedProducts', () => {
    it('returns top wasted products', async () => {
      mockAggregate.mockResolvedValue([{ productId: 'p1', productName: 'Milk', count: 3 }]);
      const result = await repo.getTopWastedProducts('hh1', new Date(), new Date());
      expect(result).toHaveLength(1);
    });
  });

  describe('findByIds', () => {
    it('returns items by ids', async () => {
      mockFind.mockResolvedValue([{ _id: 'id1' }]);
      const result = await repo.findByIds(['id1'], 'hh1');
      expect(result).toHaveLength(1);
    });
  });

  describe('bulkUpdateStatus', () => {
    it('returns modified count', async () => {
      mockUpdateMany.mockResolvedValue({ modifiedCount: 2 });
      const result = await repo.bulkUpdateStatus(['id1', 'id2'], 'hh1', 'consumed' as never);
      expect(result).toBe(2);
    });
  });
});
