import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ShoppingListsRepository } from '../../../src/modules/shopping-lists/shopping-lists.repository.js';

const { mockSave, MockShoppingListModel } = 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, MockShoppingListModel: MockModel };
});

vi.mock('../../../src/schemas/shopping-list.schema.js', () => ({
  ShoppingListModel: MockShoppingListModel,
}));

const { ShoppingListModel } = await import('../../../src/schemas/shopping-list.schema.js');

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

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

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

  describe('create', () => {
    it('saves new shopping list model and returns simple object', async () => {
      const doc = { _id: 'list1', name: 'Test List' };
      mockSave.mockResolvedValue({ toObject: () => doc });

      const result = await repo.create({ name: 'Test List', householdId: 'h1', createdBy: 'u1', status: 'active' });
      expect(mockSave).toHaveBeenCalled();
      expect(result._id).toBe('list1');
    });
  });

  describe('list', () => {
    it('queries lists for household ordered newest first', async () => {
      const chain = makeChain([{ _id: 'list1' }]);
      vi.mocked(ShoppingListModel.find).mockReturnValue(chain as any);

      await repo.list('h1');
      expect(ShoppingListModel.find).toHaveBeenCalledWith({ householdId: 'h1' });
      expect(chain.sort).toHaveBeenCalledWith({ createdAt: -1 });
    });
  });

  describe('findById', () => {
    it('queries distinct document by ID and householdId', async () => {
      const chain = makeChain({ _id: 'list1' });
      vi.mocked(ShoppingListModel.findOne).mockReturnValue(chain as any);

      const result = await repo.findById('list1', 'h1');
      expect(ShoppingListModel.findOne).toHaveBeenCalledWith({ _id: 'list1', householdId: 'h1' });
      expect(result?._id).toBe('list1');
    });
  });

  describe('findActiveByHousehold', () => {
    it('queries specifically active/shopping lists sorted by update recency', async () => {
      const chain = makeChain([{ _id: 'list1' }]);
      vi.mocked(ShoppingListModel.find).mockReturnValue(chain as any);

      await repo.findActiveByHousehold('h1');
      expect(ShoppingListModel.find).toHaveBeenCalledWith({
        householdId: 'h1',
        status: { $in: ['active', 'shopping'] }
      });
      expect(chain.sort).toHaveBeenCalledWith({ updatedAt: -1 });
    });
  });

  describe('update', () => {
    it('sets top level list variables atomically', async () => {
      const chain = makeChain({});
      vi.mocked(ShoppingListModel.findOneAndUpdate).mockReturnValue(chain as any);

      await repo.update('list1', 'h1', { name: 'New Name' });
      expect(ShoppingListModel.findOneAndUpdate).toHaveBeenCalledWith(
        { _id: 'list1', householdId: 'h1' },
        { $set: { name: 'New Name' } },
        { new: true }
      );
    });
  });

  describe('delete', () => {
    it('executes findOneAndDelete targeting target IDs', async () => {
      const chain = makeChain({});
      vi.mocked(ShoppingListModel.findOneAndDelete).mockReturnValue(chain as any);

      await repo.delete('list1', 'h1');
      expect(ShoppingListModel.findOneAndDelete).toHaveBeenCalledWith({ _id: 'list1', householdId: 'h1' });
    });
  });

  // --- Atomic Subdocument Array Actions Tests ---

  describe('addItem', () => {
    it('executes $push operator targeting list subdocuments', async () => {
      const chain = makeChain({});
      vi.mocked(ShoppingListModel.findOneAndUpdate).mockReturnValue(chain as any);

      const mockItem = { id: 'itm1', quantity: 1, unit: 'g', checked: false, addedToPantry: false };
      await repo.addItem('list1', 'h1', mockItem as any);

      expect(ShoppingListModel.findOneAndUpdate).toHaveBeenCalledWith(
        { _id: 'list1', householdId: 'h1' },
        { $push: { items: mockItem } },
        { new: true }
      );
    });
  });

  describe('updateItem', () => {
    it('maps partial payload to flattened positional $ keys', async () => {
      const chain = makeChain({});
      vi.mocked(ShoppingListModel.findOneAndUpdate).mockReturnValue(chain as any);

      await repo.updateItem('list1', 'h1', 'itm1', { checked: true, actualPrice: 5.5 });

      expect(ShoppingListModel.findOneAndUpdate).toHaveBeenCalledWith(
        { _id: 'list1', householdId: 'h1', 'items.id': 'itm1' },
        {
          $set: {
            'items.$.checked': true,
            'items.$.actualPrice': 5.5
          }
        },
        { new: true }
      );
    });
  });

  describe('removeItem', () => {
    it('executes $pull operator matching inner item tracking id', async () => {
      const chain = makeChain({});
      vi.mocked(ShoppingListModel.findOneAndUpdate).mockReturnValue(chain as any);

      await repo.removeItem('list1', 'h1', 'itm1');

      expect(ShoppingListModel.findOneAndUpdate).toHaveBeenCalledWith(
        { _id: 'list1', householdId: 'h1' },
        { $pull: { items: { id: 'itm1' } } },
        { new: true }
      );
    });
  });
  describe('sortItems logic', () => {
    it('sorts items by checked status, category, name, and id', async () => {
      const items = [
        { id: '6', customName: 'Zebra', category: 'Animal', checked: true },
        { id: '1', customName: 'Apple', category: 'Fruit', checked: false },
        { id: '2', customName: 'Banana', category: 'Fruit', checked: false },
        { id: '3', customName: 'Aardvark', category: 'Animal', checked: false },
        { id: '5', customName: 'Apple', category: 'Fruit', checked: true },
        { id: '4', customName: 'Bread', checked: false }, // No category (should come first in category sort)
        { id: '0', checked: false }, // No name, no category (should come first in all)
      ];
      
      const chain = makeChain({ _id: 'list1', items });
      vi.mocked(ShoppingListModel.findOne).mockReturnValue(chain as any);

      const result = await repo.findById('list1', 'h1');
      
      // Expected order:
      // 1. Unchecked, No category, No name (0)
      // 2. Unchecked, No category, Bread (4)
      // 3. Unchecked, Animal, Aardvark (3)
      // 4. Unchecked, Fruit, Apple (1)
      // 5. Unchecked, Fruit, Banana (2)
      // 6. Checked, Animal, Zebra (6)
      // 7. Checked, Fruit, Apple (5)
      
      expect(result.items[0].id).toBe('0');
      expect(result.items[1].id).toBe('4');
      expect(result.items[2].id).toBe('3');
      expect(result.items[3].id).toBe('1');
      expect(result.items[4].id).toBe('2');
      expect(result.items[5].id).toBe('6');
      expect(result.items[6].id).toBe('5');
    });

    it('handles mixed missing/present categories and names during sorting', async () => {
      const items = [
        { id: '1', customName: 'Apple', checked: false }, // category missing
        { id: '2', category: 'Fruit', checked: false }, // customName missing
      ];
      
      const chain = makeChain({ _id: 'list1', items });
      vi.mocked(ShoppingListModel.findOne).mockReturnValue(chain as any);

      const result = await repo.findById('list1', 'h1');
      
      // Expected order:
      // 1. (id:1) - empty category comes before 'Fruit'
      // 2. (id:2) - 'Fruit' category
      expect(result.items[0].id).toBe('1');
      expect(result.items[1].id).toBe('2');
    });

    it('sorts items by id as a final tie-breaker', async () => {
      const items = [
        { id: 'B', customName: 'Apple', category: 'Fruit', checked: false },
        { id: 'A', customName: 'Apple', category: 'Fruit', checked: false },
      ];
      
      const chain = makeChain({ _id: 'list1', items });
      vi.mocked(ShoppingListModel.findOne).mockReturnValue(chain as any);

      const result = await repo.findById('list1', 'h1');
      
      expect(result.items[0].id).toBe('A');
      expect(result.items[1].id).toBe('B');
    });

    it('handles null items or list gracefully', () => {
      expect((repo as any).sortItems(null)).toBeNull();
      expect((repo as any).sortItems({ name: 'foo' })).toEqual({ name: 'foo' });
    });
  });
});
