// src/services/aiService.server.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { createMockLogger } from '../tests/utils/mockLogger';
import type { Logger } from 'pino';
import type { MasterGroceryItem } from '../types';
// Import the class, not the singleton instance, so we can instantiate it with mocks.
import { AIService } from './aiService.server';

// Mock the logger to prevent the real pino instance from being created, which causes issues with 'pino-pretty' in tests.
vi.mock('./logger.server', () => ({
  logger: createMockLogger(),
}));

// Import the mocked logger instance to pass to the service constructor.
import { logger as mockLoggerInstance } from './logger.server';

// Explicitly unmock the service under test to ensure we import the real implementation.
vi.unmock('./aiService.server');

// Mock sharp, as it's a direct dependency of the service.
const mockToBuffer = vi.fn();
const mockExtract = vi.fn(() => ({ toBuffer: mockToBuffer }));
const mockSharp = vi.fn(() => ({ extract: mockExtract }));
vi.mock('sharp', () => ({
  __esModule: true,
  default: mockSharp,
}));


describe('AI Service (Server)', () => {
  // Create mock dependencies that will be injected into the service
  const mockAiClient = { generateContent: vi.fn() };
  const mockFileSystem = { readFile: vi.fn() };

  // Instantiate the service with our mock dependencies
  const aiServiceInstance = new AIService(mockLoggerInstance, mockAiClient, mockFileSystem);

  beforeEach(() => {
    vi.clearAllMocks();
    // Reset modules to ensure the service re-initializes with the mocks

   mockAiClient.generateContent.mockResolvedValue({ 
      text: '[]', 
      candidates: [] 
    });
  });

  describe('Constructor', () => {
    const originalEnv = process.env;

    beforeEach(() => {
      // Reset process.env before each test in this block
      vi.resetModules(); // Important to re-evaluate the service file
      process.env = { ...originalEnv };
    });

    afterEach(() => {
      // Restore original environment variables
      process.env = originalEnv;
    });

    it('should throw an error if GEMINI_API_KEY is not set in a non-test environment', async () => {
      // Simulate a non-test environment
      process.env.NODE_ENV = 'production';
      delete process.env.GEMINI_API_KEY;

      // Dynamically import the class to re-evaluate the constructor logic
      const { AIService } = await import('./aiService.server');
      expect(() => new AIService(mockLoggerInstance)).toThrow('GEMINI_API_KEY environment variable not set for server-side AI calls.');
    });
  });

  describe('extractItemsFromReceiptImage', () => {
    it('should extract items from a valid AI response', async () => {
      const mockAiResponseText = `[
          { "raw_item_description": "ORGANIC BANANAS", "price_paid_cents": 129 },
          { "raw_item_description": "AVOCADO", "price_paid_cents": 299 }
        ]`;
      
      mockAiClient.generateContent.mockResolvedValue({ text: mockAiResponseText, candidates: [] });
      mockFileSystem.readFile.mockResolvedValue(Buffer.from('mock-image-data'));

      const result = await aiServiceInstance.extractItemsFromReceiptImage('path/to/image.jpg', 'image/jpeg', mockLoggerInstance);

      expect(mockAiClient.generateContent).toHaveBeenCalledTimes(1);
      expect(result).toEqual([
        { raw_item_description: 'ORGANIC BANANAS', price_paid_cents: 129 },
        { raw_item_description: 'AVOCADO', price_paid_cents: 299 },
      ]);
    });

    it('should throw an error if the AI response is not valid JSON', async () => {
      mockAiClient.generateContent.mockResolvedValue({ text: 'This is not JSON.', candidates: [] });
      mockFileSystem.readFile.mockResolvedValue(Buffer.from('mock-image-data'));

      await expect(aiServiceInstance.extractItemsFromReceiptImage('path/to/image.jpg', 'image/jpeg', mockLoggerInstance)).rejects.toThrow(
        'AI response did not contain a valid JSON array.'
      );
    });

    it('should throw an error if the AI API call fails', async () => {
      const apiError = new Error('API limit reached');
      mockAiClient.generateContent.mockRejectedValue(apiError);
      mockFileSystem.readFile.mockResolvedValue(Buffer.from('mock-image-data'));

      await expect(aiServiceInstance.extractItemsFromReceiptImage('path/to/image.jpg', 'image/jpeg', mockLoggerInstance))
        .rejects.toThrow(apiError);
      expect(mockLoggerInstance.error).toHaveBeenCalledWith(
        { err: apiError }, "Google GenAI API call failed in extractItemsFromReceiptImage"
      );
    });
  });

  describe('extractCoreDataFromFlyerImage', () => {
    const mockMasterItems: MasterGroceryItem[] = [{ master_grocery_item_id: 1, name: 'Apples', created_at: '' }];

    it('should extract and post-process flyer data correctly', async () => {
      const mockAiResponse = {
        store_name: 'Test Store',
        valid_from: '2024-01-01',
        valid_to: '2024-01-07',
        items: [
          { item: 'Apples', price_display: '$1.99', price_in_cents: 199, quantity: '1lb', category_name: 'Produce', master_item_id: 1 },
          { item: 'Oranges', price_display: null, price_in_cents: null, quantity: undefined, category_name: null, master_item_id: null },
        ],
      };
      mockAiClient.generateContent.mockResolvedValue({ text: JSON.stringify(mockAiResponse), candidates: [] });
      mockFileSystem.readFile.mockResolvedValue(Buffer.from('mock-image-data'));

      const result = await aiServiceInstance.extractCoreDataFromFlyerImage([{ path: 'path/to/image.jpg', mimetype: 'image/jpeg' }], mockMasterItems, undefined, undefined, mockLoggerInstance);

      expect(mockAiClient.generateContent).toHaveBeenCalledTimes(1);
      expect(result.store_name).toBe('Test Store');
      expect(result.items).toHaveLength(2);
      expect(result.items[1].price_display).toBe('');
      expect(result.items[1].quantity).toBe('');
      expect(result.items[1].category_name).toBe('Other/Miscellaneous');
    });

    it('should throw an error if the AI response is not a valid JSON object', async () => {
      mockAiClient.generateContent.mockResolvedValue({ text: 'not a json object', candidates: [] });
      mockFileSystem.readFile.mockResolvedValue(Buffer.from('mock-image-data'));

      await expect(aiServiceInstance.extractCoreDataFromFlyerImage([], mockMasterItems, undefined, undefined, mockLoggerInstance)).rejects.toThrow(
        'AI response did not contain a valid JSON object.'
      );
    });

    it('should throw an error if the AI response contains malformed JSON', async () => {
      // Arrange: AI returns a string that looks like JSON but is invalid
      mockAiClient.generateContent.mockResolvedValue({ text: '{ "store_name": "Incomplete, }', candidates: [] });
      mockFileSystem.readFile.mockResolvedValue(Buffer.from('mock-image-data'));

      // Act & Assert
      await expect(aiServiceInstance.extractCoreDataFromFlyerImage([], mockMasterItems, undefined, undefined, mockLoggerInstance))
        .rejects.toThrow('AI response did not contain a valid JSON object.');
    });

    it('should throw an error if the AI API call fails', async () => {
      // Arrange: AI client's method rejects
      const apiError = new Error('API call failed');
      mockAiClient.generateContent.mockRejectedValue(apiError);
      mockFileSystem.readFile.mockResolvedValue(Buffer.from('mock-image-data'));

      // Act & Assert
      await expect(aiServiceInstance.extractCoreDataFromFlyerImage([], mockMasterItems, undefined, undefined, mockLoggerInstance)).rejects.toThrow(apiError);
      expect(mockLoggerInstance.error).toHaveBeenCalledWith(
        { err: apiError }, "Google GenAI API call failed in extractCoreDataFromFlyerImage"
      );
    });
  });

  describe('_buildFlyerExtractionPrompt (private method)', () => {
    it('should include a strong hint for userProfileAddress', () => {
      const prompt = (aiServiceInstance as unknown as { _buildFlyerExtractionPrompt: (masterItems: [], submitterIp: undefined, userProfileAddress: string) => string })._buildFlyerExtractionPrompt([], undefined, '123 Main St, Anytown');
      expect(prompt).toContain('The user who uploaded this flyer has a profile address of "123 Main St, Anytown". Use this as a strong hint for the store\'s location.');
    });

    it('should include a general hint for submitterIp when no address is present', () => {
      const prompt = (aiServiceInstance as unknown as { _buildFlyerExtractionPrompt: (masterItems: [], submitterIp: string) => string })._buildFlyerExtractionPrompt([], '123.45.67.89');
      expect(prompt).toContain('The user uploaded this flyer from an IP address that suggests a location. Use this as a general hint for the store\'s region.');
    });

    it('should not include any location hint if no IP or address is provided', () => {
      const prompt = (aiServiceInstance as unknown as { _buildFlyerExtractionPrompt: (masterItems: []) => string })._buildFlyerExtractionPrompt([]);
      expect(prompt).not.toContain('Use this as a strong hint');
      expect(prompt).not.toContain('Use this as a general hint');
    });
  });

  describe('_parseJsonFromAiResponse (private method)', () => {
    it('should return null for undefined or empty input', () => { // This was a duplicate, fixed.
      expect((aiServiceInstance as unknown as { _parseJsonFromAiResponse: (text: undefined, logger: typeof mockLoggerInstance) => null })._parseJsonFromAiResponse(undefined, mockLoggerInstance)).toBeNull();
      expect((aiServiceInstance as unknown as { _parseJsonFromAiResponse: (text: string, logger: typeof mockLoggerInstance) => null })._parseJsonFromAiResponse('', mockLoggerInstance)).toBeNull();
    });

    it('should correctly parse a clean JSON string', () => {
      const json = '{ "key": "value" }';
      // Use a type-safe assertion to access the private method for testing.
      const result = (aiServiceInstance as unknown as { _parseJsonFromAiResponse: <T>(text: string, logger: Logger) => T | null })._parseJsonFromAiResponse<{ key: string }>(json, mockLoggerInstance);
      expect(result).toEqual({ key: 'value' });
    });

    it('should extract and parse JSON wrapped in markdown and other text', () => { // This was a duplicate, fixed.
      const responseText = 'Here is the data you requested:\n```json\n{ "data": true }\n```\nLet me know if you need more.';
      expect((aiServiceInstance as unknown as { _parseJsonFromAiResponse: (text: string, logger: typeof mockLoggerInstance) => { data: boolean } })._parseJsonFromAiResponse(responseText, mockLoggerInstance)).toEqual({ data: true });
    });

    it('should handle JSON arrays correctly', () => {
      const responseText = '```json\n```';
      expect((aiServiceInstance as unknown as { _parseJsonFromAiResponse: (text: string, logger: typeof mockLoggerInstance) => number[] })._parseJsonFromAiResponse(responseText, mockLoggerInstance)).toEqual([1, 2, 3]);
    });

    it('should return null for strings without valid JSON', () => {
      const responseText = 'This is just plain text.';
      expect((aiServiceInstance as unknown as { _parseJsonFromAiResponse: (text: string, logger: typeof mockLoggerInstance) => null })._parseJsonFromAiResponse(responseText, mockLoggerInstance)).toBeNull();
    });

    it('should return null for incomplete JSON and log an error', async () => {
      const logger = createMockLogger();
      const responseText = '```json\n{ "key": "value"'; // Missing closing brace;
      expect((aiServiceInstance as unknown as { _parseJsonFromAiResponse: (text: string, logger: Logger) => null })._parseJsonFromAiResponse(responseText, logger)).toBeNull();
      expect(logger.error).toHaveBeenCalledWith({ jsonString: '{ "key": "value"', error: expect.any(SyntaxError) }, 'Failed to parse JSON from AI response slice');
    });
  });

  describe('_normalizeExtractedItems (private method)', () => {
    it('should replace null or undefined fields with default values', () => {
      const rawItems: { item: string; price_display: null; quantity: undefined; category_name: null; master_item_id: null; }[] = [{ item: 'Test', price_display: null, quantity: undefined, category_name: null, master_item_id: null }];
      const [normalized] = (aiServiceInstance as unknown as { _normalizeExtractedItems: (items: typeof rawItems) => { price_display: string, quantity: string, category_name: string, master_item_id: undefined }[] })._normalizeExtractedItems(rawItems);
      expect(normalized.price_display).toBe('');
      expect(normalized.quantity).toBe('');
      expect(normalized.category_name).toBe('Other/Miscellaneous');
      expect(normalized.master_item_id).toBeUndefined();
    });
  });

  describe('extractTextFromImageArea', () => {
    it('should call sharp to crop the image and call the AI with the correct prompt', async () => {
      const imagePath = 'path/to/image.jpg';
      const cropArea = { x: 10, y: 20, width: 100, height: 50 };
      const extractionType = 'store_name';

      // Mock sharp's output
      const mockCroppedBuffer = Buffer.from('cropped-image-data');
      mockToBuffer.mockResolvedValue(mockCroppedBuffer);
      // Mock AI response
      mockAiClient.generateContent.mockResolvedValue({ text: 'Super Store', candidates: [] });

      const result = await aiServiceInstance.extractTextFromImageArea(imagePath, 'image/jpeg', cropArea, extractionType, mockLoggerInstance);

      expect(mockSharp).toHaveBeenCalledWith(imagePath);
      expect(mockExtract).toHaveBeenCalledWith({
        left: 10,
        top: 20,
        width: 100,
        height: 50,
      });

      expect(mockAiClient.generateContent).toHaveBeenCalledTimes(1);
      
      interface AiCallArgs {
        contents: {
          parts: {
            text?: string;
            inlineData?: unknown;
          }[];
        }[];
      }
      const aiCallArgs = mockAiClient.generateContent.mock.calls[0][0] as AiCallArgs;
      expect(aiCallArgs.contents[0].parts[0].text).toContain('What is the store name in this image?');
      expect(result.text).toBe('Super Store');
    });

    it('should throw an error if the AI API call fails', async () => {
      const apiError = new Error('API Error');
      mockAiClient.generateContent.mockRejectedValue(apiError);
      mockToBuffer.mockResolvedValue(Buffer.from('cropped-image-data'));

      await expect(aiServiceInstance.extractTextFromImageArea('path', 'image/jpeg', { x: 0, y: 0, width: 10, height: 10 }, 'dates', mockLoggerInstance))
        .rejects.toThrow(apiError);
      expect(mockLoggerInstance.error).toHaveBeenCalledWith(
        { err: apiError }, "Google GenAI API call failed in extractTextFromImageArea for type dates"
      );
    });
  });

  describe('planTripWithMaps', () => {
    const mockUserLocation: GeolocationCoordinates = { latitude: 45, longitude: -75, accuracy: 10, altitude: null, altitudeAccuracy: null, heading: null, speed: null, toJSON: () => ({}) };
    const mockStore = { name: 'Test Store' };

    it('should throw a "feature disabled" error', async () => {
      // This test verifies the current implementation which has the feature disabled.
      await expect(aiServiceInstance.planTripWithMaps([], mockStore, mockUserLocation, mockLoggerInstance))
        .rejects.toThrow("The 'planTripWithMaps' feature is currently disabled due to API costs.");
    });
  });
});