import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MealPlanRepository } from '../../../src/modules/meal-plans/meal-plans.repository.js';
import { MealPlanStatus } from '@meshitrack/shared';

const { mockSave, MockMealPlanModel } = vi.hoisted(() => {
  const mockSave = vi.fn();
  function MockModel(this: { save: typeof mockSave }, data: unknown) {
    Object.assign(this, data);
    this.save = mockSave;
  }
  Object.assign(MockModel, {
    findOne: vi.fn(),
    find: vi.fn(),
    findOneAndUpdate: vi.fn(),
    findOneAndDelete: vi.fn(),
  });
  return { mockSave, MockMealPlanModel: MockModel };
});

vi.mock('../../../src/schemas/meal-plan.schema.js', () => ({
  MealPlanModel: MockMealPlanModel,
}));

const { MealPlanModel } = await import('../../../src/schemas/meal-plan.schema.js');

function makeChain(result: unknown = null) {
  return {
    sort: vi.fn().mockReturnThis(),
    limit: vi.fn().mockReturnThis(),
    lean: vi.fn().mockReturnThis(),
    exec: vi.fn().mockResolvedValue(result),
  };
}

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

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

  describe('findByHousehold', () => {
    it('applies householdId filter', async () => {
      const chain = makeChain([]);
      vi.mocked(MealPlanModel.find).mockReturnValue(chain as never);

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

      expect(MealPlanModel.find).toHaveBeenCalledWith(
        expect.objectContaining({ householdId: 'hh1' }),
      );
    });
  });

  describe('findByWeek', () => {
    it('queries by householdId and weekStartDate', async () => {
      const mockPlan = { _id: 'mp1', weekStartDate: '2026-05-18' };
      vi.mocked(MealPlanModel.findOne).mockReturnValue(makeChain(mockPlan) as never);

      const result = await repo.findByWeek('hh1', '2026-05-18');
      expect(MealPlanModel.findOne).toHaveBeenCalledWith({
        householdId: 'hh1',
        weekStartDate: '2026-05-18',
      });
      expect(result).toEqual(mockPlan);
    });
  });

  describe('create', () => {
    it('saves and returns new document', async () => {
      const plainDoc = { _id: 'new-id', weekStartDate: '2026-05-18' };
      mockSave.mockResolvedValue({ toObject: () => plainDoc });

      const result = await repo.create({ weekStartDate: '2026-05-18' });
      expect(mockSave).toHaveBeenCalled();
      expect(result).toEqual(plainDoc);
    });
  });

  describe('updateStatus', () => {
    it('updates status only', async () => {
      vi.mocked(MealPlanModel.findOneAndUpdate).mockReturnValue(makeChain({ _id: 'mp1' }) as never);

      await repo.updateStatus('mp1', 'hh1', MealPlanStatus.ACTIVE);
      expect(MealPlanModel.findOneAndUpdate).toHaveBeenCalledWith(
        { _id: 'mp1', householdId: 'hh1' },
        { $set: { status: MealPlanStatus.ACTIVE } },
        { new: true, lean: true },
      );
    });
  });

  describe('findById', () => {
    it('finds meal plan by id and householdId', async () => {
      const mockPlan = { _id: 'mp1', weekStartDate: '2026-05-18' };
      vi.mocked(MealPlanModel.findOne).mockReturnValue(makeChain(mockPlan) as never);

      const result = await repo.findById('mp1', 'hh1');
      expect(MealPlanModel.findOne).toHaveBeenCalledWith({
        _id: 'mp1',
        householdId: 'hh1',
      });
      expect(result).toEqual(mockPlan);
    });
  });

  describe('update', () => {
    it('updates meal plan using findOneAndUpdate', async () => {
      vi.mocked(MealPlanModel.findOneAndUpdate).mockReturnValue(makeChain({ _id: 'mp1' }) as never);

      await repo.update('mp1', 'hh1', { status: MealPlanStatus.ACTIVE });
      expect(MealPlanModel.findOneAndUpdate).toHaveBeenCalledWith(
        { _id: 'mp1', householdId: 'hh1' },
        { $set: { status: MealPlanStatus.ACTIVE } },
        { new: true, lean: true },
      );
    });
  });

  describe('delete', () => {
    it('deletes meal plan using findOneAndDelete', async () => {
      vi.mocked(MealPlanModel.findOneAndDelete).mockReturnValue(makeChain({ _id: 'mp1' }) as never);

      const result = await repo.delete('mp1', 'hh1');
      expect(MealPlanModel.findOneAndDelete).toHaveBeenCalledWith({
        _id: 'mp1',
        householdId: 'hh1',
      });
      expect(result).toEqual({ _id: 'mp1' });
    });
  });

  describe('findByHousehold pagination cursor', () => {
    it('applies pagination filter when cursor is provided', async () => {
      const chain = makeChain([]);
      vi.mocked(MealPlanModel.find).mockReturnValue(chain as never);

      const cursor = Buffer.from('some-mongo-id').toString('base64');
      await repo.findByHousehold('hh1', { limit: 20, cursor });

      expect(MealPlanModel.find).toHaveBeenCalledWith({
        householdId: 'hh1',
        _id: { $gt: 'some-mongo-id' }
      });
    });
  });
});
