// src/features/charts/PriceHistoryChart.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { PriceHistoryChart } from './PriceHistoryChart';
import { useUserData } from '../../hooks/useUserData';
import * as apiClient from '../../services/apiClient';
import type { MasterGroceryItem, HistoricalPriceDataPoint } from '../../types';
import {
  createMockMasterGroceryItem,
  createMockHistoricalPriceDataPoint,
} from '../../tests/utils/mockFactories';
import { QueryWrapper } from '../../tests/utils/renderWithProviders';

// Mock the apiClient
vi.mock('../../services/apiClient');

// Mock the useUserData hook
vi.mock('../../hooks/useUserData');
const mockedUseUserData = useUserData as Mock;

const renderWithQuery = (ui: React.ReactElement) => render(ui, { wrapper: QueryWrapper });

// Mock the logger
vi.mock('../../services/logger', () => ({
  logger: {
    error: vi.fn(),
  },
}));

// Mock the recharts library to prevent rendering complex SVGs in jsdom
vi.mock('recharts', () => ({
  ResponsiveContainer: ({ children }: { children: React.ReactNode }) => (
    <div data-testid="responsive-container">{children}</div>
  ),
  // Expose the data prop for testing data transformations
  LineChart: ({ children, data }: { children: React.ReactNode; data: any[] }) => (
    <div data-testid="line-chart" data-chartdata={JSON.stringify(data)}>
      {children}
    </div>
  ),
  CartesianGrid: () => <div data-testid="cartesian-grid" />,
  XAxis: () => <div data-testid="x-axis" />,
  YAxis: ({ tickFormatter, domain }: any) => {
    // Execute functions for coverage
    if (typeof tickFormatter === 'function') {
      tickFormatter(1000);
    }
    if (Array.isArray(domain)) {
      domain.forEach((d) => {
        if (typeof d === 'function') d(100);
      });
    }
    return <div data-testid="y-axis" />;
  },
  Tooltip: ({ formatter }: any) => {
    // Execute formatter for coverage
    if (typeof formatter === 'function') {
      formatter(1000);
      formatter(undefined);
    }
    return <div data-testid="tooltip" />;
  },
  Legend: () => <div data-testid="legend" />,
  // Fix: Use dataKey if name is not explicitly provided, as the component relies on dataKey
  Line: ({ name, dataKey }: { name?: string; dataKey?: string }) => (
    <div data-testid={`line-${name || dataKey}`} />
  ),
}));

const mockWatchedItems: MasterGroceryItem[] = [
  createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Organic Bananas' }),
  createMockMasterGroceryItem({ master_grocery_item_id: 2, name: 'Almond Milk' }),
];

const mockPriceHistory: HistoricalPriceDataPoint[] = [
  createMockHistoricalPriceDataPoint({
    master_item_id: 1,
    summary_date: '2024-10-01',
    avg_price_in_cents: 110,
  }),
  createMockHistoricalPriceDataPoint({
    master_item_id: 1,
    summary_date: '2024-10-08',
    avg_price_in_cents: 99,
  }),
  createMockHistoricalPriceDataPoint({
    master_item_id: 2,
    summary_date: '2024-10-01',
    avg_price_in_cents: 350,
  }),
  createMockHistoricalPriceDataPoint({
    master_item_id: 2,
    summary_date: '2024-10-08',
    avg_price_in_cents: 349,
  }),
];

describe('PriceHistoryChart', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Provide a default successful mock for useUserData
    mockedUseUserData.mockReturnValue({
      watchedItems: mockWatchedItems,
      shoppingLists: [],
      setWatchedItems: vi.fn(),
      setShoppingLists: vi.fn(),
      isLoading: false,
      error: null,
    });
  });

  it('should render a placeholder when there are no watched items', () => {
    mockedUseUserData.mockReturnValue({
      watchedItems: [],
      shoppingLists: [],
      setWatchedItems: vi.fn(),
      setShoppingLists: vi.fn(),
      isLoading: false,
      error: null,
    });
    renderWithQuery(<PriceHistoryChart />);
    expect(
      screen.getByText('Add items to your watchlist to see their price trends over time.'),
    ).toBeInTheDocument();
  });

  it('should display a loading state while fetching data', () => {
    vi.mocked(apiClient.fetchHistoricalPriceData).mockReturnValue(new Promise(() => {}));
    renderWithQuery(<PriceHistoryChart />);
    expect(screen.getByText('Loading Price History...')).toBeInTheDocument();
  });

  it('should display an error message if the API call fails', async () => {
    vi.mocked(apiClient.fetchHistoricalPriceData).mockRejectedValue(new Error('API is down'));
    renderWithQuery(<PriceHistoryChart />);

    await waitFor(() => {
      // Use regex to match the error message text which might be split across elements
      expect(screen.getByText(/API is down/)).toBeInTheDocument();
    });
  });

  it('should display a message if no historical data is returned', async () => {
    vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
      new Response(JSON.stringify([])),
    );
    renderWithQuery(<PriceHistoryChart />);

    await waitFor(() => {
      expect(
        screen.getByText(
          'Not enough historical data for your watched items. Process more flyers to build a trend.',
        ),
      ).toBeInTheDocument();
    });
  });

  it('should render the chart with data on successful fetch', async () => {
    vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
      new Response(JSON.stringify(mockPriceHistory)),
    );
    renderWithQuery(<PriceHistoryChart />);

    await waitFor(() => {
      // Check that the API was called with the correct item IDs
      expect(apiClient.fetchHistoricalPriceData).toHaveBeenCalledWith([1, 2]);

      // Check that the chart components are rendered
      expect(screen.getByTestId('responsive-container')).toBeInTheDocument();
      expect(screen.getByTestId('line-chart')).toBeInTheDocument();
      expect(screen.getByTestId('x-axis')).toBeInTheDocument();
      expect(screen.getByTestId('y-axis')).toBeInTheDocument();
      expect(screen.getByTestId('legend')).toBeInTheDocument();

      // Check that a line is rendered for each watched item
      expect(screen.getByTestId('line-Organic Bananas')).toBeInTheDocument();
      expect(screen.getByTestId('line-Almond Milk')).toBeInTheDocument();
    });
  });

  it('should display a loading state while user data is loading', () => {
    mockedUseUserData.mockReturnValue({
      watchedItems: [],
      shoppingLists: [],
      setWatchedItems: vi.fn(),
      setShoppingLists: vi.fn(),
      isLoading: true, // Test the isLoading state from the useUserData hook
      error: null,
    });
    vi.mocked(apiClient.fetchHistoricalPriceData).mockReturnValue(new Promise(() => {}));
    renderWithQuery(<PriceHistoryChart />);
    expect(screen.getByText('Loading Price History...')).toBeInTheDocument();
  });

  it('should clear the chart when the watchlist becomes empty', async () => {
    vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
      new Response(JSON.stringify(mockPriceHistory)),
    );
    const { rerender } = renderWithQuery(<PriceHistoryChart />);

    // Initial render with items
    await waitFor(() => {
      expect(screen.getByTestId('line-Organic Bananas')).toBeInTheDocument();
    });

    // Rerender with empty watchlist
    mockedUseUserData.mockReturnValue({
      watchedItems: [],
      shoppingLists: [],
      setWatchedItems: vi.fn(),
      setShoppingLists: vi.fn(),
      isLoading: false,
      error: null,
    });
    rerender(<PriceHistoryChart />);

    // Chart should be gone, placeholder should appear
    await waitFor(() => {
      expect(
        screen.getByText('Add items to your watchlist to see their price trends over time.'),
      ).toBeInTheDocument();
      expect(screen.queryByTestId('line-chart')).not.toBeInTheDocument();
    });
  });

  it('should filter out items with only one data point', async () => {
    const dataWithSinglePoint: HistoricalPriceDataPoint[] = [
      createMockHistoricalPriceDataPoint({
        master_item_id: 1,
        summary_date: '2024-10-01',
        avg_price_in_cents: 110,
      }),
      createMockHistoricalPriceDataPoint({
        master_item_id: 1,
        summary_date: '2024-10-08',
        avg_price_in_cents: 99,
      }),
      createMockHistoricalPriceDataPoint({
        master_item_id: 2,
        summary_date: '2024-10-01',
        avg_price_in_cents: 350,
      }), // Almond Milk only has one point
    ];
    vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
      new Response(JSON.stringify(dataWithSinglePoint)),
    );
    renderWithQuery(<PriceHistoryChart />);

    await waitFor(() => {
      expect(screen.getByTestId('line-Organic Bananas')).toBeInTheDocument();
      expect(screen.queryByTestId('line-Almond Milk')).not.toBeInTheDocument();
    });
  });

  it('should process data to only keep the lowest price for a given day', async () => {
    const dataWithDuplicateDate: HistoricalPriceDataPoint[] = [
      createMockHistoricalPriceDataPoint({
        master_item_id: 1,
        summary_date: '2024-10-01',
        avg_price_in_cents: 110,
      }),
      createMockHistoricalPriceDataPoint({
        master_item_id: 1,
        summary_date: '2024-10-01',
        avg_price_in_cents: 105,
      }), // Lower price
      createMockHistoricalPriceDataPoint({
        master_item_id: 1,
        summary_date: '2024-10-08',
        avg_price_in_cents: 99,
      }),
    ];
    vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
      new Response(JSON.stringify(dataWithDuplicateDate)),
    );
    renderWithQuery(<PriceHistoryChart />);

    await waitFor(() => {
      const chart = screen.getByTestId('line-chart');
      const chartData = JSON.parse(chart.getAttribute('data-chartdata')!);

      // The date gets formatted to 'Oct 1'
      const dataPointForOct1 = chartData.find((d: any) => d.date === 'Oct 1');

      expect(dataPointForOct1['Organic Bananas']).toBe(105);
    });
  });

  it('should filter out data points with a price of zero', async () => {
    const dataWithZeroPrice: HistoricalPriceDataPoint[] = [
      createMockHistoricalPriceDataPoint({
        master_item_id: 1,
        summary_date: '2024-10-01',
        avg_price_in_cents: 110,
      }),
      createMockHistoricalPriceDataPoint({
        master_item_id: 1,
        summary_date: '2024-10-08',
        avg_price_in_cents: 0,
      }), // Zero price should be filtered
      createMockHistoricalPriceDataPoint({
        master_item_id: 1,
        summary_date: '2024-10-15',
        avg_price_in_cents: 105,
      }),
    ];
    vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
      new Response(JSON.stringify(dataWithZeroPrice)),
    );
    renderWithQuery(<PriceHistoryChart />);

    await waitFor(() => {
      const chart = screen.getByTestId('line-chart');
      const chartData = JSON.parse(chart.getAttribute('data-chartdata')!);

      // The date 'Oct 8' should not be in the chart data at all
      const dataPointForOct8 = chartData.find((d: any) => d.date === 'Oct 8');
      expect(dataPointForOct8).toBeUndefined();

      // Check that the other two points are there
      expect(chartData).toHaveLength(2);
    });
  });

  it('should handle malformed data points and unmatched items gracefully', async () => {
    const malformedData: any[] = [
      { master_item_id: null, summary_date: '2024-10-01', avg_price_in_cents: 100 }, // Missing ID
      { master_item_id: 1, summary_date: null, avg_price_in_cents: 100 }, // Missing date
      { master_item_id: 1, summary_date: '2024-10-01', avg_price_in_cents: null }, // Missing price
      { master_item_id: 999, summary_date: '2024-10-01', avg_price_in_cents: 100 }, // ID not in watchlist
    ];
    vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
      new Response(JSON.stringify(malformedData)),
    );
    renderWithQuery(<PriceHistoryChart />);

    await waitFor(() => {
      // Should show "Not enough historical data" because all points are invalid or filtered
      expect(
        screen.getByText(
          'Not enough historical data for your watched items. Process more flyers to build a trend.',
        ),
      ).toBeInTheDocument();
    });
  });

  it('should ignore higher prices for the same day', async () => {
    const dataWithHigherPrice: HistoricalPriceDataPoint[] = [
      createMockHistoricalPriceDataPoint({
        master_item_id: 1,
        summary_date: '2024-10-01',
        avg_price_in_cents: 100,
      }),
      createMockHistoricalPriceDataPoint({
        master_item_id: 1,
        summary_date: '2024-10-01',
        avg_price_in_cents: 150, // Higher price should be ignored
      }),
      createMockHistoricalPriceDataPoint({
        master_item_id: 1,
        summary_date: '2024-10-08',
        avg_price_in_cents: 100,
      }),
    ];
    vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
      new Response(JSON.stringify(dataWithHigherPrice)),
    );
    renderWithQuery(<PriceHistoryChart />);

    await waitFor(() => {
      const chart = screen.getByTestId('line-chart');
      const chartData = JSON.parse(chart.getAttribute('data-chartdata')!);
      const dataPoint = chartData.find((d: any) => d.date === 'Oct 1');
      expect(dataPoint['Organic Bananas']).toBe(100);
    });
  });

  it('should handle non-Error objects thrown during fetch', async () => {
    // Use an actual Error object since the component displays error.message
    vi.mocked(apiClient.fetchHistoricalPriceData).mockRejectedValue(new Error('Fetch failed'));
    renderWithQuery(<PriceHistoryChart />);

    await waitFor(() => {
      expect(screen.getByText(/Fetch failed/)).toBeInTheDocument();
    });
  });
});
