import { describe, it, expect, vi, beforeEach } from 'vitest';
import { DosageUnit, MedicineProductSource } from '@meshitrack/shared';

const {
  mockExec,
  _mockLean,
  mockFind,
  mockFindOne,
  mockFindOneAndUpdate,
  mockSave,
  _mockSort,
  _mockLimit,
  mockCountDocuments,
} = vi.hoisted(() => {
  const mockExec = vi.fn();
  const mockLean = vi.fn(() => ({ exec: mockExec }));
  const mockLimit = vi.fn(() => ({ lean: mockLean }));
  const mockSort = vi.fn(() => ({ limit: mockLimit }));
  const mockCountDocuments = vi.fn();
  return {
    mockExec,
    mockLean,
    mockFind: vi.fn(() => ({ sort: mockSort })),
    mockFindOne: vi.fn(() => ({ lean: mockLean })),
    mockFindOneAndUpdate: vi.fn(() => ({ exec: mockExec })),
    mockSave: vi.fn(),
    mockSort,
    mockLimit,
    mockCountDocuments,
  };
});

vi.mock('../../../src/schemas/medicine-product.schema.js', () => {
  class MockMedicineProductModel {
    _data: Record<string, unknown>;
    constructor(data: Record<string, unknown>) {
      this._data = data;
      Object.assign(this, data);
    }
    save() {
      mockSave();
      return Promise.resolve(this);
    }
    toObject() {
      return { _id: 'mp-new', ...this._data };
    }
    static find = mockFind;
    static findOne = mockFindOne;
    static findOneAndUpdate = mockFindOneAndUpdate;
    static countDocuments = vi.fn(() => ({ exec: mockCountDocuments }));
  }
  return { MedicineProductModel: MockMedicineProductModel };
});

import { MedicineProductsRepository } from '../../../src/modules/medicine-products/medicine-products.repository.js';

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

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

  describe('findByMedicine', () => {
    it('returns paginated results', async () => {
      const items = [
        { _id: 'mp-1', brand: 'CVS' },
        { _id: 'mp-2', brand: 'Kirkland' },
      ];
      mockExec.mockResolvedValue(items);

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

      expect(mockFind).toHaveBeenCalledWith({
        householdId: 'hh1',
        medicineId: 'med-1',
        isDeleted: false,
      });
      expect(result.data).toHaveLength(2);
      expect(result.pagination.hasMore).toBe(false);
    });

    it('detects hasMore when extra item returned', async () => {
      const items = Array.from({ length: 3 }, (_, i) => ({ _id: `mp-${i}`, brand: `Brand ${i}` }));
      mockExec.mockResolvedValue(items);

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

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

    it('decodes cursor for pagination', async () => {
      mockExec.mockResolvedValue([]);
      const cursor = Buffer.from('mp-5').toString('base64');

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

      expect(mockFind).toHaveBeenCalledWith(expect.objectContaining({ _id: { $gt: 'mp-5' } }));
    });

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

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

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

  describe('findById', () => {
    it('finds by id and householdId', async () => {
      const product = { _id: 'mp-1', brand: 'CVS' };
      mockExec.mockResolvedValue(product);

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

      expect(mockFindOne).toHaveBeenCalledWith({
        _id: 'mp-1',
        householdId: 'hh1',
        isDeleted: false,
      });
      expect(result).toEqual(product);
    });
  });

  describe('create', () => {
    it('creates a medicine product', async () => {
      mockSave.mockResolvedValue({});

      const data = {
        brand: 'CVS Health',
        packageSize: 90,
        packageUnit: DosageUnit.TABLET,
        source: MedicineProductSource.MANUAL,
      };

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

      expect(mockSave).toHaveBeenCalled();
      expect(result).toMatchObject({
        brand: 'CVS Health',
        householdId: 'hh1',
        medicineId: 'med-1',
        medicineName: 'Metformin',
        createdBy: 'kc-1',
      });
    });
  });

  describe('update', () => {
    it('updates a medicine product', async () => {
      mockExec.mockResolvedValue({ _id: 'mp-1', brand: 'Updated' });

      const result = await repo.update('mp-1', 'hh1', { brand: 'Updated' });

      expect(mockFindOneAndUpdate).toHaveBeenCalledWith(
        { _id: 'mp-1', householdId: 'hh1', isDeleted: false },
        { $set: { brand: 'Updated' } },
        { new: true, lean: true },
      );
      expect(result).toEqual({ _id: 'mp-1', brand: 'Updated' });
    });
  });

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

      await repo.softDelete('mp-1', 'hh1');

      expect(mockFindOneAndUpdate).toHaveBeenCalledWith(
        { _id: 'mp-1', householdId: 'hh1', isDeleted: false },
        { $set: { isDeleted: true } },
        { new: true, lean: true },
      );
    });
  });

  describe('countByMedicineId', () => {
    it('returns count of non-deleted products for medicine', async () => {
      mockCountDocuments.mockResolvedValue(3);

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

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