// src/hooks/useAiAnalysis.test.ts
import React, { ReactNode } from 'react';
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useAiAnalysis } from './useAiAnalysis';
import { useApi } from './useApi';
import { AnalysisType } from '../types';
import type { Flyer, FlyerItem, MasterGroceryItem } from '../types'; // Removed ApiProvider import
import { ApiProvider } from '../providers/ApiProvider'; // Updated import path for ApiProvider

// 1. Mock dependencies
vi.mock('./useApi');
vi.mock('../services/logger.client', () => ({
  logger: {
    info: vi.fn(),
    error: vi.fn(),
  },
}));

const mockedUseApi = vi.mocked(useApi);

// Mock the AI API client functions that are passed to useApi
vi.mock('../services/aiApiClient', () => ({
  getQuickInsights: vi.fn(),
  getDeepDiveAnalysis: vi.fn(),
  searchWeb: vi.fn(),
  planTripWithMaps: vi.fn(),
  compareWatchedItemPrices: vi.fn(),
  generateImageFromText: vi.fn(),
}));

// Create a wrapper component that includes the required provider
const wrapper = ({ children }: { children: ReactNode }) => React.createElement(ApiProvider, null, children);

// --- Mocks for each useApi instance ---
const mockGetQuickInsights = { execute: vi.fn(), data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() };
const mockGetDeepDive = { execute: vi.fn(), data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() };
const mockSearchWeb = { execute: vi.fn(), data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() };
const mockPlanTrip = { execute: vi.fn(), data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() };
const mockComparePrices = { execute: vi.fn(), data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() };
const mockGenerateImage = { execute: vi.fn(), data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() };

// 2. Mock data
const mockFlyerItems: FlyerItem[] = [{ flyer_item_id: 1, item: 'Apples', price_display: '$1.99', price_in_cents: 199, quantity: '1lb', flyer_id: 1, created_at: '', view_count: 0, click_count: 0, updated_at: '' }];
const mockWatchedItems: MasterGroceryItem[] = [{ master_grocery_item_id: 101, name: 'Bananas', created_at: '' }];
const mockSelectedFlyer: Flyer = { flyer_id: 1, store: { store_id: 1, name: 'SuperMart', created_at: '' } } as Flyer;

describe('useAiAnalysis Hook', () => {
  const defaultParams = {
    flyerItems: mockFlyerItems,
    selectedFlyer: mockSelectedFlyer,
    watchedItems: mockWatchedItems,
  };

  beforeEach(() => {
    vi.clearAllMocks();
    
    // Set a default return value for any call to useApi.
    // This prevents the "cannot destructure property 'execute' of undefined" error
    // if the hook adds more useApi calls than we explicitly mock with mockReturnValueOnce.
    mockedUseApi.mockReturnValue({
      execute: vi.fn(),
      data: null,
      loading: false,
      error: null,
      isRefetching: false,
      reset: vi.fn(),
    });

    // Reset the mock implementations for specific calls if needed, 
    // or rely on the default and override specific ones in individual tests.
    // However, to keep existing test logic intact where specific mocks are expected:
    mockedUseApi
      .mockReturnValueOnce(mockGetQuickInsights)
      .mockReturnValueOnce(mockGetDeepDive)
      .mockReturnValueOnce(mockSearchWeb)
      .mockReturnValueOnce(mockPlanTrip)
      .mockReturnValueOnce(mockComparePrices)
      .mockReturnValueOnce(mockGenerateImage);

    // Mock Geolocation API
    Object.defineProperty(navigator, 'geolocation', {
      writable: true,
      value: {
        getCurrentPosition: vi.fn().mockImplementation((success) =>
          success({ coords: { latitude: 50, longitude: 50 } })
        ),
      },
    });
  });

  it('should initialize with correct default states', () => {
    const { result } = renderHook(() => useAiAnalysis(defaultParams), { wrapper });

    expect(result.current.results).toEqual({});
    expect(result.current.sources).toEqual({});
    expect(result.current.loadingStates).toEqual({
      [AnalysisType.QUICK_INSIGHTS]: false,
      [AnalysisType.DEEP_DIVE]: false,
      [AnalysisType.WEB_SEARCH]: false,
      [AnalysisType.PLAN_TRIP]: false,
      [AnalysisType.COMPARE_PRICES]: false,
    });
    expect(result.current.error).toBeNull();
    expect(result.current.generatedImageUrl).toBeNull();
    expect(result.current.isGeneratingImage).toBe(false);
  });

  describe('runAnalysis', () => {
    it('should call the correct execute function for QUICK_INSIGHTS', async () => {
      mockGetQuickInsights.execute.mockResolvedValue('Quick insights text');
      const { result } = renderHook(() => useAiAnalysis(defaultParams), { wrapper });

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

      expect(mockGetQuickInsights.execute).toHaveBeenCalledWith(mockFlyerItems);
    });

    it('should update results when quickInsightsData changes', () => {
      const { result, rerender } = renderHook(() => useAiAnalysis(defaultParams), { wrapper });
      
      // Simulate useApi returning new data by re-rendering with a new mock value
      mockedUseApi.mockReset()
        .mockReturnValueOnce({ ...mockGetQuickInsights, data: 'New insights', reset: vi.fn() })
        .mockReturnValue(mockGetDeepDive); // provide defaults for others

      rerender();

      expect(result.current.results[AnalysisType.QUICK_INSIGHTS]).toBe('New insights');
    });

    it('should call the correct execute function for DEEP_DIVE', async () => {
      mockGetDeepDive.execute.mockResolvedValue('Deep dive text');
      const { result } = renderHook(() => useAiAnalysis(defaultParams), { wrapper });

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

      expect(mockGetDeepDive.execute).toHaveBeenCalledWith(mockFlyerItems);
    });

    it('should update results and sources when webSearchData changes', () => {
      const mockResponse = { text: 'Web search text', sources: [{ web: { uri: 'http://a.com', title: 'Source A' } }] };
      
      const { result, rerender } = renderHook(() => useAiAnalysis(defaultParams), { wrapper });

      // Prepare mocks for the re-render. We must provide the full sequence of 6 calls.
      mockedUseApi.mockReset();
      // Set default fallback first
      mockedUseApi.mockReturnValue({ execute: vi.fn(), data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() });
      
      // Override specific sequence for re-render
      mockedUseApi
        .mockReturnValueOnce(mockGetQuickInsights)
        .mockReturnValueOnce(mockGetDeepDive)
        .mockReturnValueOnce({ ...mockSearchWeb, data: mockResponse, reset: vi.fn() }) // searchWeb has data now
        .mockReturnValueOnce(mockPlanTrip)
        .mockReturnValueOnce(mockComparePrices)
        .mockReturnValueOnce(mockGenerateImage);

      rerender();

      expect(result.current.results[AnalysisType.WEB_SEARCH]).toBe('Web search text');
      expect(result.current.sources[AnalysisType.WEB_SEARCH]).toEqual([{ uri: 'http://a.com', title: 'Source A' }]);
    });

    it('should call the correct execute function for COMPARE_PRICES', async () => {
      mockComparePrices.execute.mockResolvedValue({ text: 'Price comparison text', sources: [] }); // This was a duplicate, fixed.
      const { result } = renderHook(() => useAiAnalysis(defaultParams), { wrapper });

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

      expect(mockComparePrices.execute).toHaveBeenCalledWith(mockWatchedItems);
    });

    it('should call the correct execute function for PLAN_TRIP with geolocation', async () => {
      mockPlanTrip.execute.mockResolvedValue({ text: 'Trip plan text', sources: [{ uri: 'http://maps.com', title: 'Map' }] });
      const { result } = renderHook(() => useAiAnalysis(defaultParams), { wrapper });

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

      expect(navigator.geolocation.getCurrentPosition).toHaveBeenCalled();
      expect(mockPlanTrip.execute).toHaveBeenCalledWith(
        mockFlyerItems,
        mockSelectedFlyer.store,
        { latitude: 50, longitude: 50 }
      );
    });

    it('should derive a generic error message if an API call fails', () => {
      const apiError = new Error('API is down');
      
      // Simulate useApi returning an error
      // Reset and provide full sequence
      mockedUseApi.mockReset();
      mockedUseApi.mockReturnValue({ execute: vi.fn(), data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() });

      mockedUseApi
        .mockReturnValueOnce({ ...mockGetQuickInsights, error: apiError, reset: vi.fn() }) // Quick insights failed
        .mockReturnValueOnce(mockGetDeepDive)
        .mockReturnValueOnce(mockSearchWeb)
        .mockReturnValueOnce(mockPlanTrip)
        .mockReturnValueOnce(mockComparePrices)
        .mockReturnValueOnce(mockGenerateImage);

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

      expect(result.current.error).toBe('API is down');
    });

    it('should log an error for geolocation permission denial', async () => {
      const geoError = new GeolocationPositionError();
      Object.defineProperty(geoError, 'code', { value: GeolocationPositionError.PERMISSION_DENIED });
      vi.mocked(navigator.geolocation.getCurrentPosition).mockImplementation((success, error) => {
        if (error) error(geoError);
      });

      // The execute function will reject, and useApi will set the error state
      const rejectionError = new Error("Geolocation permission denied.");
      mockPlanTrip.execute.mockRejectedValue(rejectionError);

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

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

      // The test now verifies that the error from the failed execute call is propagated.
      // The specific user-friendly message is now part of the component that consumes the hook.
      expect(result.current.error).toBe(rejectionError.message);
    });
  });

  describe('generateImage', () => {
    it('should not run if there are no DEEP_DIVE results', async () => {
      const { result } = renderHook(() => useAiAnalysis(defaultParams), { wrapper });

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

      expect(mockGenerateImage.execute).not.toHaveBeenCalled();
    });

    it('should call the API and set the image URL on success', async () => {
      const { result } = renderHook(() => useAiAnalysis(defaultParams), { wrapper });

      // First, simulate having a deep dive result
      act(() => {
        // This state is internal, so we test the effect by calling the function
        result.current.results[AnalysisType.DEEP_DIVE] = 'A great meal plan';
      });

      mockGenerateImage.execute.mockResolvedValue('base64string');

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

      expect(mockGenerateImage.execute).toHaveBeenCalledWith('A great meal plan');
    });

    it('should set an error if image generation fails', async () => {
      const { result, rerender } = renderHook(() => useAiAnalysis(defaultParams), { wrapper });

      // Initial state setup (simulated via internal mutation for test speed, 
      // or by re-mocking UseApi to return result, but that requires a rerender)
      // Since we want to test generateImage which reads from state, we must ensure state is set.
      // The best way is to simulate a successful deep dive first.
      
      // Let's just bypass the state check by ensuring results is populated or mocking results differently?
      // No, we can just mutate the result.current.results for testing purposes 
      // IF the hook doesn't memoize generateImage based on it deeply.
      // generateImage depends on [results].
      
      // Better: Re-render with deep dive data present.
      mockedUseApi.mockReset();
      mockedUseApi.mockReturnValue({ execute: vi.fn(), data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() });
      mockedUseApi
        .mockReturnValueOnce(mockGetQuickInsights)
        .mockReturnValueOnce({ ...mockGetDeepDive, data: 'A great meal plan' }) // Deep dive data
        .mockReturnValueOnce(mockSearchWeb)
        .mockReturnValueOnce(mockPlanTrip)
        .mockReturnValueOnce(mockComparePrices)
        .mockReturnValueOnce(mockGenerateImage);
      
      rerender();

      // Now set up the failure for the next render/call
      const apiError = new Error('Image model failed');
      // We mock the execute function to reject.
      // AND we need the hook to re-render with the error state *after* the rejection.
      // But useApi internal state updates trigger the re-render.
      
      // Ideally, we test that the hook *reacts* to useApi returning an error.
      // But here we are testing the *action* calling the API.
      
      // Let's just mock the execute to reject, and assume the hook handles the rejection (caught).
      // Then re-mock useApi to show the error state, simulate a re-render, and check error.
      
      // For this test, we just want to ensure it tries to call it and handles error.
      // Actually, checking result.current.error relies on useApi returning the error.
      
      // Step 1: Execute fails
      mockGenerateImage.execute.mockRejectedValue(apiError);
      await act(async () => {
        await result.current.generateImage();
      });
      
      // Step 2: Simulate useApi update (re-render with error state)
      mockedUseApi.mockReset();
      mockedUseApi.mockReturnValue({ execute: vi.fn(), data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() });
      mockedUseApi
        .mockReturnValueOnce(mockGetQuickInsights)
        .mockReturnValueOnce({ ...mockGetDeepDive, data: 'A great meal plan' })
        .mockReturnValueOnce(mockSearchWeb)
        .mockReturnValueOnce(mockPlanTrip)
        .mockReturnValueOnce(mockComparePrices)
        .mockReturnValueOnce({ ...mockGenerateImage, error: apiError }); // Image gen has error

      rerender();

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