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

/**
 * End-to-end tests for the item search feature
 * Validates all acceptance criteria from the task:
 * - Endpoint returns items matching query in code OR name OR description (case-insensitive)
 * - Results limited to 10 items
 * - Returns empty array for queries < 2 characters
 * - Handles special characters safely (no regex injection)
 */
describe('Item Search - E2E Acceptance Criteria', () => {
  let service: ItemsService;
  let controller: ItemsController;
  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 and controller instances
    service = new ItemsService(
      itemModel as any,
      {} as any, // batchModel
      {} as any, // balanceModel
      {} as any, // sheetModel
      {} as any, // stockTxModel
    );

    controller = new ItemsController(service);
  });

  describe('AC1: Endpoint returns items matching query in code OR name OR description (case-insensitive)', () => {
    it('should match items by itemCode', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'RICE-BASMATI-001',
          itemName: 'Premium Rice',
          description: 'High quality product',
          bagWeightKg: 25,
        },
      ];

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

      const result = await controller.searchItems('BASMATI');
      
      expect(result).toHaveLength(1);
      expect(result[0].itemCode).toContain('BASMATI');
    });

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

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

      const result = await controller.searchItems('Premium');
      
      expect(result).toHaveLength(1);
      expect(result[0].itemName).toContain('Premium');
    });

    it('should match items by description', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'ITEM-002',
          itemName: 'Rice Product',
          description: 'Organic Basmati Rice from India',
          bagWeightKg: 25,
        },
      ];

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

      const result = await controller.searchItems('Organic');
      
      expect(result).toHaveLength(1);
      expect(result[0].description).toContain('Organic');
    });

    it('should be case-insensitive for itemCode', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'RICE-001',
          itemName: 'Rice',
          description: 'Product',
          bagWeightKg: 25,
        },
      ];

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

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

    it('should be case-insensitive for itemName', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'ITEM-001',
          itemName: 'Basmati Rice',
          description: 'Product',
          bagWeightKg: 25,
        },
      ];

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

      const result1 = await controller.searchItems('basmati');
      const result2 = await controller.searchItems('BASMATI');
      const result3 = await controller.searchItems('BaSmAtI');
      
      expect(result1).toHaveLength(1);
      expect(result2).toHaveLength(1);
      expect(result3).toHaveLength(1);
    });

    it('should be case-insensitive for description', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'ITEM-001',
          itemName: 'Rice',
          description: 'Premium Quality Product',
          bagWeightKg: 25,
        },
      ];

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

      const result1 = await controller.searchItems('premium');
      const result2 = await controller.searchItems('PREMIUM');
      const result3 = await controller.searchItems('PrEmIuM');
      
      expect(result1).toHaveLength(1);
      expect(result2).toHaveLength(1);
      expect(result3).toHaveLength(1);
    });

    it('should match partial strings', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'RICE-BASMATI-PREMIUM-001',
          itemName: 'Basmati Premium Rice',
          description: 'High quality 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 controller.searchItems('bas');
      
      expect(result).toHaveLength(1);
      expect(result[0].itemCode).toContain('BASMATI');
    });
  });

  describe('AC2: Results limited to 10 items', () => {
    it('should return maximum 10 items even if more match', async () => {
      const mockItems = Array.from({ length: 10 }, (_, i) => ({
        _id: new Types.ObjectId(),
        itemCode: `RICE-${i.toString().padStart(3, '0')}`,
        itemName: `Rice Product ${i}`,
        description: `Rice description ${i}`,
        bagWeightKg: 25,
      }));

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

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

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

    it('should return fewer than 10 items if fewer match', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'RICE-001',
          itemName: 'Basmati Rice',
          description: 'Premium rice',
          bagWeightKg: 25,
        },
        {
          _id: new Types.ObjectId(),
          itemCode: 'RICE-002',
          itemName: 'Jasmine Rice',
          description: 'Fragrant rice',
          bagWeightKg: 40,
        },
      ];

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

      const result = await controller.searchItems('rice');
      
      expect(result).toHaveLength(2);
    });
  });

  describe('AC3: Returns empty array for queries < 2 characters', () => {
    it('should return empty array for 1 character query', async () => {
      const result = await controller.searchItems('a');
      expect(result).toEqual([]);
    });

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

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

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

    it('should process queries with exactly 2 characters', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'RICE-001',
          itemName: 'Rice',
          description: 'Product',
          bagWeightKg: 25,
        },
      ];

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

      const result = await controller.searchItems('ri');
      
      expect(itemModel.find).toHaveBeenCalled();
      expect(result).toHaveLength(1);
    });
  });

  describe('AC4: Handles special characters safely (no regex injection)', () => {
    const specialCharacters = [
      { char: '.', name: 'dot' },
      { char: '*', name: 'asterisk' },
      { char: '+', name: 'plus' },
      { char: '?', name: 'question mark' },
      { char: '^', name: 'caret' },
      { char: '$', name: 'dollar sign' },
      { char: '{', name: 'left brace' },
      { char: '}', name: 'right brace' },
      { char: '(', name: 'left parenthesis' },
      { char: ')', name: 'right parenthesis' },
      { char: '|', name: 'pipe' },
      { char: '[', name: 'left bracket' },
      { char: ']', name: 'right bracket' },
      { char: '\\', name: 'backslash' },
    ];

    specialCharacters.forEach(({ char, name }) => {
      it(`should safely handle ${name} (${char}) without regex error`, async () => {
        itemModel.find = vi.fn().mockReturnValue({
          limit: vi.fn().mockReturnValue({
            lean: vi.fn().mockReturnValue({
              exec: vi.fn().mockResolvedValue([]),
            }),
          }),
        });

        // Should not throw an error
        await expect(controller.searchItems(`test${char}query`)).resolves.toEqual([]);
        expect(itemModel.find).toHaveBeenCalled();
      });
    });

    it('should handle multiple special characters in one query', async () => {
      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue([]),
          }),
        }),
      });

      await expect(controller.searchItems('test.*+?^${}()|[]\\query')).resolves.toEqual([]);
      expect(itemModel.find).toHaveBeenCalled();
    });

    it('should treat special characters as literal characters, not regex operators', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'ITEM-001',
          itemName: 'Test Item (Special)',
          description: 'Product with special chars',
          bagWeightKg: 25,
        },
      ];

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

      // Search for literal parentheses
      const result = await controller.searchItems('(Special)');
      
      expect(itemModel.find).toHaveBeenCalled();
      expect(result).toHaveLength(1);
    });

    it('should handle regex injection attempt: .*', async () => {
      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue([]),
          }),
        }),
      });

      // .* would match everything if not escaped
      await expect(controller.searchItems('.*')).resolves.toEqual([]);
      expect(itemModel.find).toHaveBeenCalled();
    });

    it('should handle regex injection attempt: ^.*$', async () => {
      itemModel.find = vi.fn().mockReturnValue({
        limit: vi.fn().mockReturnValue({
          lean: vi.fn().mockReturnValue({
            exec: vi.fn().mockResolvedValue([]),
          }),
        }),
      });

      // ^.*$ would match everything if not escaped
      await expect(controller.searchItems('^.*$')).resolves.toEqual([]);
      expect(itemModel.find).toHaveBeenCalled();
    });
  });

  describe('Integration: Multiple acceptance criteria together', () => {
    it('should handle case-insensitive partial match with special characters', async () => {
      const mockItems = [
        {
          _id: new Types.ObjectId(),
          itemCode: 'RICE-001',
          itemName: 'Rice (Premium)',
          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 controller.searchItems('rice (prem');
      
      expect(result).toHaveLength(1);
      expect(result[0].itemName).toContain('Premium');
    });

    it('should return empty for special character query < 2 chars', async () => {
      const result = await controller.searchItems('*');
      expect(result).toEqual([]);
    });

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

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

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

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