import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ItemsService } from './items.service';
import { Model, Types } from 'mongoose';

describe('ItemsService - searchItems', () => {
  let service: ItemsService;
  let itemModel: any;

  beforeEach(() => {
    // Mock the item model
    itemModel = {
      find: vi.fn(),
      findOne: vi.fn(),
      findById: vi.fn(),
      create: vi.fn(),
      findByIdAndUpdate: vi.fn(),
    };

    // Create service instance with mocked dependencies
    service = new ItemsService(
      itemModel as any,
      {} as Model<any>, // batchModel
      {} as Model<any>, // balanceModel
      {} as Model<any>, // sheetModel
      {} as Model<any>, // stockTxModel
    );
  });

  describe('Query validation', () => {
    it('should return empty array for queries < 2 characters', async () => {
      const result = await service.searchItems('a');
      expect(result).toEqual([]);
    });

    it('should return empty array for empty query', async () => {
      const result = await service.searchItems('');
      expect(result).toEqual([]);
    });

    it('should return empty array for null/undefined query', async () => {
      const result1 = await service.searchItems(null as any);
      const result2 = await service.searchItems(undefined as any);
      expect(result1).toEqual([]);
      expect(result2).toEqual([]);
    });
  });

  describe('Search functionality', () => {
    it('should search by itemCode (partial match, case-insensitive)', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'RICE-001',
          itemName: 'Basmati Rice',
          description: 'Premium Basmati Rice',
          bagWeightKg: 25,
        },
      ];

      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue(mockItems),
          }),
        }),
      });

      const result = await service.searchItems('rice');
      
      expect(itemModel.find).toHaveBeenCalledWith({
        $or: [
          { itemCode: expect.any(RegExp) },
          { itemName: expect.any(RegExp) },
          { description: expect.any(RegExp) },
        ],
      });
      expect(result).toHaveLength(1);
      expect(result[0].itemCode).toBe('RICE-001');
    });

    it('should search by itemName (partial match, case-insensitive)', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'ITEM-001',
          itemName: 'Premium Basmati Rice',
          description: 'High quality rice',
          bagWeightKg: 25,
        },
      ];

      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue(mockItems),
          }),
        }),
      });

      const result = await service.searchItems('basmati');
      
      expect(result).toHaveLength(1);
      expect(result[0].itemName).toContain('Basmati');
    });

    it('should search by description (partial match, case-insensitive)', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'ITEM-002',
          itemName: 'Rice Product',
          description: 'Premium quality grain',
          bagWeightKg: 25,
        },
      ];

      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue(mockItems),
          }),
        }),
      });

      const result = await service.searchItems('premium');
      
      expect(result).toHaveLength(1);
      expect(result[0].description).toContain('Premium');
    });

    it('should limit results to 10 items', async () => {
      const mockItems = Array.from({ length: 15 }, (_, i) => ({
        _id: new Types.ObjectId(),
        itemCode: `ITEM-${i.toString().padStart(3, '0')}`,
        itemName: `Item ${i}`,
        description: `Description ${i}`,
        bagWeightKg: 25,
      }));

      const limitMock = vi.fn().mockReturnValue({
        lean: vi.fn().mockReturnValue({
          exec: vi.fn().mockResolvedValue(mockItems.slice(0, 10)),
        }),
      });

      itemModel.find = vi.fn().mockReturnValue({
        limit: limitMock,
      });

      const result = await service.searchItems('item');
      
      expect(limitMock).toHaveBeenCalledWith(10);
      expect(result).toHaveLength(10);
    });
  });

  describe('Special character handling (regex injection prevention)', () => {
    it('should escape special regex characters: . * + ? ^ $ { } ( ) | [ ] \\', async () => {
      const specialChars = ['.', '*', '+', '?', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\'];
      
      for (const char of specialChars) {
        itemModel.find = vi.fn().mockReturnValue({
          limit: vi.fn().mockReturnValue({
            lean: vi.fn().mockReturnValue({
              exec: vi.fn().mockResolvedValue([]),
            }),
          }),
        });

        await service.searchItems(`test${char}query`);
        
        // Verify that find was called (meaning no regex error occurred)
        expect(itemModel.find).toHaveBeenCalled();
      }
    });

    it('should handle query with parentheses without regex error', async () => {
      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue([]),
          }),
        }),
      });

      await service.searchItems('item(test)');
      
      expect(itemModel.find).toHaveBeenCalled();
    });

    it('should handle query with brackets without regex error', async () => {
      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue([]),
          }),
        }),
      });

      await service.searchItems('item[test]');
      
      expect(itemModel.find).toHaveBeenCalled();
    });

    it('should handle query with asterisk without regex error', async () => {
      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue([]),
          }),
        }),
      });

      await service.searchItems('item*test');
      
      expect(itemModel.find).toHaveBeenCalled();
    });
  });

  describe('Case-insensitive search', () => {
    it('should find items regardless of query case', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'RICE-001',
          itemName: 'Basmati Rice',
          description: 'Premium rice',
          bagWeightKg: 25,
        },
      ];

      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue(mockItems),
          }),
        }),
      });

      const result1 = await service.searchItems('RICE');
      const result2 = await service.searchItems('rice');
      const result3 = await service.searchItems('RiCe');
      
      expect(result1).toHaveLength(1);
      expect(result2).toHaveLength(1);
      expect(result3).toHaveLength(1);
    });
  });

  describe('Empty results', () => {
    it('should return empty array when no items match', async () => {
      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue([]),
          }),
        }),
      });

      const result = await service.searchItems('nonexistent');
      
      expect(result).toEqual([]);
    });
  });

  describe('Item mapping', () => {
    it('should map items with bagWeightKg to include unit field', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'RICE-001',
          itemName: 'Basmati Rice',
          description: 'Premium rice',
          bagWeightKg: 40,
        },
      ];

      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue(mockItems),
          }),
        }),
      });

      const result = await service.searchItems('rice');
      
      expect(result[0].unit).toBe('BAG/1x40kg');
    });

    it('should handle items without bagWeightKg', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'ITEM-001',
          itemName: 'Test Item',
          description: 'Test description',
          bagWeightKg: null,
        },
      ];

      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue(mockItems),
          }),
        }),
      });

      const result = await service.searchItems('test');
      
      expect(result[0].unit).toBeNull();
    });
  });
});
