import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MedicineForm, StrengthUnit, MedicineCategory } from '@meshitrack/shared';

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

vi.mock('../../../src/schemas/medicine.schema.js', () => {
  class MockMedicineModel {
    _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: 'med-new', ...this._data };
    }
    static find = mockFind;
    static findOne = mockFindOne;
    static findOneAndUpdate = mockFindOneAndUpdate;
  }
  return { MedicineModel: MockMedicineModel };
});

import { MedicinesRepository } from '../../../src/modules/medicines/medicines.repository.js';

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

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

  describe('findByHousehold', () => {
    it('returns paginated results', async () => {
      const items = [
        { _id: 'med-1', name: 'Aspirin' },
        { _id: 'med-2', name: 'Ibuprofen' },
      ];
      mockExec.mockResolvedValue(items);

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

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

    it('applies partial name search when q is provided', async () => {
      mockExec.mockResolvedValue([]);

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

      expect(mockFind).toHaveBeenCalledWith(
        expect.objectContaining({ name: { $regex: 'asp', $options: 'i' } }),
      );
    });

    it('applies category filter', async () => {
      mockExec.mockResolvedValue([]);

      await repo.findByHousehold('hh1', { category: MedicineCategory.PRESCRIPTION, limit: 20 });

      expect(mockFind).toHaveBeenCalledWith(
        expect.objectContaining({ category: MedicineCategory.PRESCRIPTION }),
      );
    });

    it('applies form filter', async () => {
      mockExec.mockResolvedValue([]);

      await repo.findByHousehold('hh1', { form: MedicineForm.TABLET, limit: 20 });

      expect(mockFind).toHaveBeenCalledWith(expect.objectContaining({ form: MedicineForm.TABLET }));
    });

    it('detects hasMore when extra item returned', async () => {
      const items = Array.from({ length: 3 }, (_, i) => ({ _id: `med-${i}`, name: `Med ${i}` }));
      mockExec.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('decodes cursor for pagination', async () => {
      mockExec.mockResolvedValue([]);
      const cursor = Buffer.from('med-5').toString('base64');

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

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

  describe('findById', () => {
    it('finds by id and householdId', async () => {
      const medicine = { _id: 'med-1', name: 'Aspirin' };
      mockExec.mockResolvedValue(medicine);

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

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

  describe('findDuplicate', () => {
    it('finds medicine with matching fields', async () => {
      mockExec.mockResolvedValue({ _id: 'med-1' });

      const result = await repo.findDuplicate('hh1', 'Aspirin', 500, 'mg', 'tablet');

      expect(mockFindOne).toHaveBeenCalledWith({
        householdId: 'hh1',
        name: 'Aspirin',
        strength: 500,
        strengthUnit: 'mg',
        form: 'tablet',
        isDeleted: false,
      });
      expect(result).toBeTruthy();
    });

    it('excludes specified id', async () => {
      mockExec.mockResolvedValue(null);

      await repo.findDuplicate('hh1', 'Aspirin', 500, 'mg', 'tablet', 'med-1');

      expect(mockFindOne).toHaveBeenCalledWith(expect.objectContaining({ _id: { $ne: 'med-1' } }));
    });
  });

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

      const data = {
        name: 'Aspirin',
        form: MedicineForm.TABLET,
        strength: 500,
        strengthUnit: StrengthUnit.MG,
        category: MedicineCategory.OTC,
        tags: [],
      };

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

      expect(mockSave).toHaveBeenCalled();
      expect(result).toMatchObject({ name: 'Aspirin', householdId: 'hh1', createdBy: 'kc-1' });
    });
  });

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

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

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

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

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

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