// src/hooks/useAiAnalysis.test.ts
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, Mocked } from 'vitest';
import { useAiAnalysis, aiAnalysisReducer } from './useAiAnalysis';
import { AnalysisType, Flyer, FlyerItem, MasterGroceryItem } from '../types';
import {
  createMockFlyer,
  createMockFlyerItem,
  createMockMasterGroceryItem,
  createMockSource,
} from '../tests/utils/mockFactories';
import { logger } from '../services/logger.client';
import { AiAnalysisService } from '../services/aiAnalysisService';

// 1. Mock dependencies
vi.mock('../services/aiAnalysisService');

vi.mock('../services/logger.client', () => ({
  // We use a real object here to allow spying on individual methods
  // while still getting console output.
  logger: {
    info: vi.fn((...args) => console.log('[LOG INFO]', ...args)),
    warn: vi.fn((...args) => console.log('[LOG WARN]', ...args)),
    error: vi.fn((...args) => console.log('[LOG ERROR]', ...args)),
  },
}));

// 2. Mock data
const mockFlyerItems: FlyerItem[] = [
  createMockFlyerItem({
    flyer_item_id: 1,
    item: 'Apples',
    price_display: '$1.99',
    price_in_cents: 199,
    quantity: '1lb',
    flyer_id: 1,
  }),
];
const mockWatchedItems: MasterGroceryItem[] = [
  createMockMasterGroceryItem({ master_grocery_item_id: 101, name: 'Bananas' }),
];
const mockSelectedFlyer: Flyer = createMockFlyer({
  flyer_id: 1,
  store: { store_id: 1, name: 'SuperMart' },
});

describe('useAiAnalysis Hook', () => {
  let mockService: Mocked<AiAnalysisService>;

  const defaultParams = {
    flyerItems: mockFlyerItems,
    selectedFlyer: mockSelectedFlyer,
    watchedItems: mockWatchedItems,
    service: {} as Mocked<AiAnalysisService>,
  };

  beforeEach(() => {
    console.log('\n\n\n--- TEST CASE START ---');
    vi.clearAllMocks();

    // Create a fresh mock of the service for each test to ensure isolation.
    mockService = new AiAnalysisService() as Mocked<AiAnalysisService>;
    mockService.getQuickInsights = vi.fn();
    mockService.getDeepDiveAnalysis = vi.fn();
    mockService.searchWeb = vi.fn();
    mockService.planTripWithMaps = vi.fn();
    mockService.compareWatchedItemPrices = vi.fn();
    mockService.generateImageFromText = vi.fn();

    // This replaces the placeholder from the initial object definition.
    defaultParams.service = mockService;
  });

  it('should initialize with correct default states', () => {
    console.log('TEST: should initialize with correct default states');
    const { result } = renderHook(() => useAiAnalysis(defaultParams));

    console.log('Asserting initial state...');
    expect(result.current.results).toEqual({});
    expect(result.current.sources).toEqual({});
    expect(result.current.loadingAnalysis).toBeNull();
    expect(result.current.error).toBeNull();
    expect(result.current.generatedImageUrl).toBeNull();
    console.log('Initial state assertions passed.');
  });

  describe('runAnalysis', () => {
    it('should call the correct service method for QUICK_INSIGHTS and update state on success', async () => {
      console.log('TEST: should handle QUICK_INSIGHTS success');
      const mockResult = 'Quick insights text';
      mockService.getQuickInsights.mockResolvedValue(mockResult);
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      // Act: Call the action exposed by the hook.
      await act(async () => {
        await result.current.runAnalysis(AnalysisType.QUICK_INSIGHTS);
      });

      expect(mockService.getQuickInsights).toHaveBeenCalledWith(mockFlyerItems);
      expect(result.current.results[AnalysisType.QUICK_INSIGHTS]).toBe(mockResult);
      expect(result.current.loadingAnalysis).toBeNull();
      expect(result.current.error).toBeNull();
    });

    it('should call the correct service method for DEEP_DIVE', async () => {
      console.log('TEST: should call execute for DEEP_DIVE');
      const mockResult = 'Deep dive text';
      mockService.getDeepDiveAnalysis.mockResolvedValue(mockResult);
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      await act(async () => {
        await result.current.runAnalysis(AnalysisType.DEEP_DIVE);
      });

      expect(mockService.getDeepDiveAnalysis).toHaveBeenCalledWith(mockFlyerItems);
      expect(result.current.results[AnalysisType.DEEP_DIVE]).toBe(mockResult);
    });

    it('should handle grounded responses for WEB_SEARCH', async () => {
      console.log('TEST: should handle grounded responses for WEB_SEARCH');
      const mockResult = {
        text: 'Web search text',
        sources: [createMockSource({ uri: 'http://a.com', title: 'Source A' })],
      };
      mockService.searchWeb.mockResolvedValue(mockResult);
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      await act(async () => {
        await result.current.runAnalysis(AnalysisType.WEB_SEARCH);
      });

      expect(mockService.searchWeb).toHaveBeenCalledWith(mockFlyerItems);
      expect(result.current.results[AnalysisType.WEB_SEARCH]).toBe(mockResult.text);
      expect(result.current.sources[AnalysisType.WEB_SEARCH]).toEqual(mockResult.sources);
    });

    it('should handle PLAN_TRIP and its specific arguments', async () => {
      console.log('TEST: should handle PLAN_TRIP');
      const mockResult = {
        text: 'Trip plan text',
        sources: [createMockSource({ uri: 'http://maps.com', title: 'Map' })],
      };
      mockService.planTripWithMaps.mockResolvedValue(mockResult);
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      await act(async () => {
        await result.current.runAnalysis(AnalysisType.PLAN_TRIP);
      });

      expect(mockService.planTripWithMaps).toHaveBeenCalledWith(
        mockFlyerItems,
        mockSelectedFlyer.store,
      );
      expect(result.current.results[AnalysisType.PLAN_TRIP]).toBe(mockResult.text);
      expect(result.current.sources[AnalysisType.PLAN_TRIP]).toEqual(mockResult.sources);
    });

    it('should handle COMPARE_PRICES and its specific arguments', async () => {
      console.log('TEST: should handle COMPARE_PRICES');
      const mockResult = {
        text: 'Price comparison text',
        sources: [createMockSource({ uri: 'http://prices.com', title: 'Prices' })],
      };
      mockService.compareWatchedItemPrices.mockResolvedValue(mockResult);
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      await act(async () => {
        await result.current.runAnalysis(AnalysisType.COMPARE_PRICES);
      });

      expect(mockService.compareWatchedItemPrices).toHaveBeenCalledWith(mockWatchedItems);
      expect(result.current.results[AnalysisType.COMPARE_PRICES]).toBe(mockResult.text);
      expect(result.current.sources[AnalysisType.COMPARE_PRICES]).toEqual(mockResult.sources);
    });

    it('should set error if PLAN_TRIP is called without a store', async () => {
      console.log('TEST: should set error if PLAN_TRIP is called without a store');
      // This test requires creating a malformed Flyer object to test a runtime guard clause.
      // We create a valid flyer, then delete the 'store' property to simulate this scenario.
      // The cast is necessary because TypeScript correctly identifies that a valid Flyer must have a store.
      const flyerWithoutStore = createMockFlyer();
      delete (flyerWithoutStore as Partial<Flyer>).store;
      const paramsWithoutStore = { ...defaultParams, selectedFlyer: flyerWithoutStore as Flyer };
      const { result } = renderHook(() => useAiAnalysis(paramsWithoutStore));

      await act(async () => {
        await result.current.runAnalysis(AnalysisType.PLAN_TRIP);
      });

      expect(result.current.error).toBe('Store information is not available for trip planning.');
      expect(mockService.planTripWithMaps).not.toHaveBeenCalled();
    });

    it('should set the error state if a service call fails', async () => {
      console.log('TEST: should set error state on failure');
      const apiError = new Error('API is down');
      mockService.getQuickInsights.mockRejectedValue(apiError);
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      await act(async () => {
        await result.current.runAnalysis(AnalysisType.QUICK_INSIGHTS);
      });

      expect(mockService.getQuickInsights).toHaveBeenCalled();
      expect(result.current.error).toBe('API is down');
      expect(result.current.loadingAnalysis).toBeNull();
    });

    it('should clear previous error when starting a new analysis', async () => {
      console.log('TEST: should clear previous error when starting a new analysis');
      const apiError = new Error('First failure');
      mockService.getQuickInsights.mockRejectedValueOnce(apiError);

      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      // 1. Trigger error
      await act(async () => {
        await result.current.runAnalysis(AnalysisType.QUICK_INSIGHTS);
      });
      expect(result.current.error).toBe('First failure');

      // 2. Trigger success
      mockService.getQuickInsights.mockResolvedValueOnce('Success');
      await act(async () => {
        await result.current.runAnalysis(AnalysisType.QUICK_INSIGHTS);
      });

      expect(result.current.error).toBeNull();
      expect(result.current.results[AnalysisType.QUICK_INSIGHTS]).toBe('Success');
    });
  });

  describe('generateImage', () => {
    it('should not call the service if there is no DEEP_DIVE result', async () => {
      console.log('TEST: should not run generateImage if DEEP_DIVE results are missing');
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      await act(async () => {
        await result.current.generateImage();
      });

      expect(logger.warn).toHaveBeenCalledWith(
        'generateImage called but no meal plan text available.',
      );
      expect(mockService.generateImageFromText).not.toHaveBeenCalled();
    });

    it('should call the service and update state on successful image generation', async () => {
      console.log('TEST: should generate image on success');
      const mockBase64 = 'base64string';
      mockService.getDeepDiveAnalysis.mockResolvedValue('A great meal plan');
      mockService.generateImageFromText.mockResolvedValue(mockBase64);
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      // First, run the deep dive to populate the required state.
      await act(async () => {
        await result.current.runAnalysis(AnalysisType.DEEP_DIVE);
      });

      // Now, generate the image.
      await act(async () => {
        await result.current.generateImage();
      });

      expect(mockService.generateImageFromText).toHaveBeenCalledWith('A great meal plan');
      expect(result.current.generatedImageUrl).toBe(`data:image/png;base64,${mockBase64}`);
      expect(result.current.loadingAnalysis).toBeNull();
    });

    it('should set an error if image generation fails', async () => {
      console.log('TEST: should handle image generation failure');
      const apiError = new Error('Image model failed');
      mockService.getDeepDiveAnalysis.mockResolvedValue('A great meal plan');
      mockService.generateImageFromText.mockRejectedValue(apiError);
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      await act(async () => {
        await result.current.runAnalysis(AnalysisType.DEEP_DIVE);
      });

      await act(async () => {
        await result.current.generateImage();
      });

      expect(result.current.error).toBe('Image model failed');
      expect(result.current.loadingAnalysis).toBeNull();
    });
  });

  describe('State Management and Logging', () => {
    it('should preserve existing results when running a new analysis', async () => {
      console.log('TEST: should preserve existing results');
      mockService.getQuickInsights.mockResolvedValue('Insight 1');
      mockService.getDeepDiveAnalysis.mockResolvedValue('Insight 2');

      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      // 1. Run first analysis
      await act(async () => {
        await result.current.runAnalysis(AnalysisType.QUICK_INSIGHTS);
      });

      // 2. Run second analysis
      await act(async () => {
        await result.current.runAnalysis(AnalysisType.DEEP_DIVE);
      });

      // 3. Verify both are present
      expect(result.current.results[AnalysisType.QUICK_INSIGHTS]).toBe('Insight 1');
      expect(result.current.results[AnalysisType.DEEP_DIVE]).toBe('Insight 2');
    });

    it('should log reducer actions', async () => {
      console.log('TEST: should log reducer actions');
      mockService.getQuickInsights.mockResolvedValue('Insight');
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      await act(async () => {
        await result.current.runAnalysis(AnalysisType.QUICK_INSIGHTS);
      });

      expect(logger.info).toHaveBeenCalledWith(
        expect.stringContaining('[aiAnalysisReducer] Dispatched action: FETCH_START'),
        expect.objectContaining({ payload: { analysisType: AnalysisType.QUICK_INSIGHTS } }),
      );
    });

    it('should set loading state but not call service for unhandled analysis types in runAnalysis', async () => {
      console.log('TEST: unhandled analysis type');
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      await act(async () => {
        // GENERATE_IMAGE is handled by generateImage(), not runAnalysis()
        await result.current.runAnalysis(AnalysisType.GENERATE_IMAGE);
      });

      // It sets loading
      expect(result.current.loadingAnalysis).toBe(AnalysisType.GENERATE_IMAGE);
      // But calls no service methods
      expect(mockService.getQuickInsights).not.toHaveBeenCalled();
      expect(mockService.generateImageFromText).not.toHaveBeenCalled();
    });
  });

  describe('Helper Functions', () => {
    it('should clear error when clearError is called', async () => {
      const apiError = new Error('Some error');
      mockService.getQuickInsights.mockRejectedValue(apiError);
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      // Trigger error
      await act(async () => {
        await result.current.runAnalysis(AnalysisType.QUICK_INSIGHTS);
      });
      expect(result.current.error).toBe('Some error');

      // Clear error
      act(() => {
        result.current.clearError();
      });
      expect(result.current.error).toBeNull();
    });

    it('should reset state when resetAnalysis is called', async () => {
      mockService.getQuickInsights.mockResolvedValue('Insight');
      const { result } = renderHook(() => useAiAnalysis(defaultParams));

      // Populate state
      await act(async () => {
        await result.current.runAnalysis(AnalysisType.QUICK_INSIGHTS);
      });
      expect(result.current.results[AnalysisType.QUICK_INSIGHTS]).toBe('Insight');

      // Reset
      act(() => {
        result.current.resetAnalysis();
      });

      expect(result.current.results).toEqual({});
      expect(result.current.loadingAnalysis).toBeNull();
      expect(result.current.error).toBeNull();
    });
  });

  describe('aiAnalysisReducer', () => {
    it('should return current state for unknown action', () => {
      const initialState = {
        loadingAnalysis: null,
        error: null,
        results: {},
        sources: {},
        generatedImageUrl: null,
      };
      const action = { type: 'UNKNOWN_ACTION' } as any;
      const newState = aiAnalysisReducer(initialState, action);
      expect(newState).toBe(initialState);
    });
  });
});
