// src/hooks/useActiveDeals.test.tsx
import { renderHook, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { useActiveDeals } from './useActiveDeals';
import * as apiClient from '../services/apiClient';
import type { Flyer, MasterGroceryItem, FlyerItem } from '../types';
import {
  createMockFlyer,
  createMockFlyerItem,
  createMockMasterGroceryItem,
  createMockDealItem,
} from '../tests/utils/mockFactories';
import { mockUseFlyers, mockUseUserData } from '../tests/setup/mockHooks';
import { QueryWrapper } from '../tests/utils/renderWithProviders';

// Must explicitly call vi.mock() for apiClient
vi.mock('../services/apiClient');

// Mock the hooks to avoid Missing Context errors
vi.mock('./useFlyers', () => ({
  useFlyers: () => mockUseFlyers(),
}));

vi.mock('../hooks/useUserData', () => ({
  useUserData: () => mockUseUserData(),
}));

const mockedApiClient = vi.mocked(apiClient);

// Set a consistent "today" for testing flyer validity to make tests deterministic
const TODAY = new Date('2024-01-15T12:00:00.000Z');

describe('useActiveDeals Hook', () => {
  // Use fake timers to control the current date in tests
  beforeEach(() => {
    // FIX: Only fake the 'Date' object.
    // This allows `new Date()` to be mocked (via setSystemTime) while keeping
    // `setTimeout`/`setInterval` native so `waitFor` doesn't hang.
    vi.useFakeTimers({ toFake: ['Date'] });
    vi.setSystemTime(TODAY);
    vi.clearAllMocks();

    // Set up default successful mocks for the new data hooks
    mockUseFlyers.mockReturnValue({
      flyers: mockFlyers,
      isLoadingFlyers: false,
      flyersError: null,
      fetchNextFlyersPage: vi.fn(),
      hasNextFlyersPage: false,
      isRefetchingFlyers: false,
      refetchFlyers: vi.fn(),
    });
    mockUseUserData.mockReturnValue({
      watchedItems: mockWatchedItems,
      shoppingLists: [],
      setWatchedItems: vi.fn(),
      setShoppingLists: vi.fn(),
      isLoading: false,
      error: null,
    });
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  const mockFlyers: Flyer[] = [
    // A currently valid flyer
    createMockFlyer({
      flyer_id: 1,
      file_name: 'valid.pdf',
      item_count: 10,
      valid_from: '2024-01-10',
      valid_to: '2024-01-20',
      store: { store_id: 1, name: 'Valid Store' },
    }),
    // An expired flyer
    createMockFlyer({
      flyer_id: 2,
      file_name: 'expired.pdf',
      item_count: 5,
      valid_from: '2024-01-01',
      valid_to: '2024-01-05',
      store: { store_id: 2, name: 'Expired Store' },
    }),
    // A future flyer
    createMockFlyer({
      flyer_id: 3,
      file_name: 'future.pdf',
      item_count: 8,
      valid_from: '2024-02-01',
      valid_to: '2024-02-10',
      store: { store_id: 3, name: 'Future Store' },
    }),
  ];

  const mockWatchedItems: MasterGroceryItem[] = [
    createMockMasterGroceryItem({ master_grocery_item_id: 101, name: 'Apples' }),
    createMockMasterGroceryItem({ master_grocery_item_id: 102, name: 'Milk' }),
  ];

  const mockFlyerItems: FlyerItem[] = [
    // A deal for a watched item in a valid flyer
    createMockFlyerItem({
      flyer_item_id: 1,
      flyer_id: 1,
      item: 'Red Apples',
      price_display: '$1.99',
      price_in_cents: 199,
      quantity: 'lb',
      master_item_id: 101,
      master_item_name: 'Apples',
    }),
    // An item that is not a deal
    createMockFlyerItem({
      flyer_item_id: 2,
      flyer_id: 1,
      item: 'Oranges',
      price_display: '$2.49',
      price_in_cents: 249,
      quantity: 'lb',
      master_item_id: 201,
    }),
  ];

  it('should return loading state initially and then calculated data', async () => {
    mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify({ count: 10 })),
    );
    mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify(mockFlyerItems)),
    );

    const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    // The hook runs the effect almost immediately. We shouldn't strictly assert false
    // because depending on render timing, it might already be true.
    // We mainly care that it eventually resolves.

    // Wait for the hook's useEffect to run and complete
    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.totalActiveItems).toBe(10);
      expect(result.current.activeDeals).toHaveLength(1);
      expect(result.current.activeDeals[0].item).toBe('Red Apples');
    });
  });

  it('should correctly filter for valid flyers and make API calls with their IDs', async () => {
    mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify({ count: 0 })),
    );
    mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify([])));

    const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    await waitFor(() => {
      // Only the valid flyer (id: 1) should be used in the API calls
      expect(mockedApiClient.countFlyerItemsForFlyers).toHaveBeenCalledWith([1]);
      expect(mockedApiClient.fetchFlyerItemsForFlyers).toHaveBeenCalledWith([1]);
      expect(result.current.isLoading).toBe(false);
    });
  });

  it('should not fetch flyer items if there are no watched items', async () => {
    mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify({ count: 10 })),
    );
    mockUseUserData.mockReturnValue({
      watchedItems: [],
      shoppingLists: [],
      setWatchedItems: vi.fn(),
      setShoppingLists: vi.fn(),
      isLoading: false,
      error: null,
    }); // Override for this test

    const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.totalActiveItems).toBe(10);
      expect(result.current.activeDeals).toEqual([]);
      // The key assertion: fetchFlyerItemsForFlyers should not be called
      expect(mockedApiClient.fetchFlyerItemsForFlyers).not.toHaveBeenCalled();
    });
  });

  it('should handle the case where there are no valid flyers', async () => {
    // Override flyers mock to only include invalid ones
    mockUseFlyers.mockReturnValue({
      flyers: [mockFlyers[1], mockFlyers[2]],
      isLoadingFlyers: false,
      flyersError: null,
      fetchNextFlyersPage: vi.fn(),
      hasNextFlyersPage: false,
      isRefetchingFlyers: false,
      refetchFlyers: vi.fn(),
    });
    const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.totalActiveItems).toBe(0);
      expect(result.current.activeDeals).toEqual([]);
      // No API calls should be made if there are no valid flyers
      expect(mockedApiClient.countFlyerItemsForFlyers).not.toHaveBeenCalled();
      expect(mockedApiClient.fetchFlyerItemsForFlyers).not.toHaveBeenCalled();
    });
  });

  it('should set an error state if counting items fails', async () => {
    const apiError = new Error('Network Failure');
    mockedApiClient.countFlyerItemsForFlyers.mockRejectedValue(apiError);
    // Also mock fetchFlyerItemsForFlyers to avoid interference from the other query
    mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify([])));

    const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.error).toBe('Could not fetch active deals or totals: Network Failure');
    });
  });

  it('should set an error state if fetching items fails', async () => {
    const apiError = new Error('Item fetch failed');
    // Mock the count to succeed but the item fetch to fail
    mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify({ count: 10 })),
    );
    mockedApiClient.fetchFlyerItemsForFlyers.mockRejectedValue(apiError);

    const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      // This covers the `|| errorItems?.message` part of the error logic
      expect(result.current.error).toBe(
        'Could not fetch active deals or totals: Item fetch failed',
      );
    });
  });

  it('should correctly map flyer items to DealItem format', async () => {
    mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify({ count: 10 })),
    );
    mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify(mockFlyerItems)),
    );

    const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    await waitFor(() => {
      const deal = result.current.activeDeals[0];
      const expectedDeal = createMockDealItem({
        item: 'Red Apples',
        price_display: '$1.99',
        price_in_cents: 199,
        quantity: 'lb',
        storeName: 'Valid Store',
        master_item_name: 'Apples',
        unit_price: null, // Expect null as the hook ensures undefined is converted to null
      });
      expect(deal).toEqual(expectedDeal);
    });
  });

  it('should use "Unknown Store" as a fallback if flyer has no store or store name', async () => {
    // Create a flyer with a null store object
    const flyerWithoutStore = createMockFlyer({
      flyer_id: 4,
      file_name: 'no-store.pdf',
      item_count: 1,
      valid_from: '2024-01-10',
      valid_to: '2024-01-20',
    });
    (flyerWithoutStore as any).store = null; // Explicitly set to null

    const itemInFlyerWithoutStore = createMockFlyerItem({
      flyer_item_id: 3,
      flyer_id: 4,
      item: 'Mystery Item',
      price_display: '$5.00',
      price_in_cents: 500,
      master_item_id: 101,
      master_item_name: 'Apples',
    });

    mockUseFlyers.mockReturnValue({ ...mockUseFlyers(), flyers: [flyerWithoutStore] });
    mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify({ count: 1 })),
    );
    mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify([itemInFlyerWithoutStore])),
    );

    const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    await waitFor(() => {
      expect(result.current.activeDeals).toHaveLength(1);
      // This covers the `|| 'Unknown Store'` fallback logic
      expect(result.current.activeDeals[0].storeName).toBe('Unknown Store');
    });
  });

  it('should filter out items that do not match watched items or have no master ID', async () => {
    mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify({ count: 5 })),
    );

    const mixedItems: FlyerItem[] = [
      // Watched item (Master ID 101 is in mockWatchedItems)
      createMockFlyerItem({
        flyer_item_id: 1,
        flyer_id: 1,
        item: 'Watched Item',
        price_display: '$1.00',
        price_in_cents: 100,
        quantity: 'ea',
        master_item_id: 101,
        master_item_name: 'Apples',
      }),
      // Unwatched item (Master ID 999 is NOT in mockWatchedItems)
      createMockFlyerItem({
        flyer_item_id: 2,
        flyer_id: 1,
        item: 'Unwatched Item',
        price_display: '$2.00',
        price_in_cents: 200,
        quantity: 'ea',
        master_item_id: 999,
        master_item_name: 'Unknown',
      }),
      // Item with no master ID
      createMockFlyerItem({
        flyer_item_id: 3,
        flyer_id: 1,
        item: 'No Master ID',
        price_display: '$3.00',
        price_in_cents: 300,
        quantity: 'ea',
        master_item_id: undefined,
      }),
    ];

    mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify(mixedItems)),
    );

    const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      // Should only contain the watched item
      expect(result.current.activeDeals).toHaveLength(1);
      expect(result.current.activeDeals[0].item).toBe('Watched Item');
    });
  });

  it('should return true for isLoading while API calls are pending', async () => {
    // Create promises we can control
    let resolveCount: (value: Response) => void;
    const countPromise = new Promise<Response>((resolve) => {
      resolveCount = resolve;
    });

    let resolveItems: (value: Response) => void;
    const itemsPromise = new Promise<Response>((resolve) => {
      resolveItems = resolve;
    });

    mockedApiClient.countFlyerItemsForFlyers.mockReturnValue(countPromise);
    mockedApiClient.fetchFlyerItemsForFlyers.mockReturnValue(itemsPromise);

    const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    // Wait for the effect to trigger the API call and set loading to true
    await waitFor(() => expect(result.current.isLoading).toBe(true));

    // Resolve promises
    await act(async () => {
      resolveCount!(new Response(JSON.stringify({ count: 5 })));
      resolveItems!(new Response(JSON.stringify([])));
    });

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });
  });

  it('should re-filter active deals when watched items change (client-side filtering)', async () => {
    // With TanStack Query, changing watchedItems does NOT trigger a new API call
    // because the query key is based on flyerIds, not watchedItems.
    // The filtering happens client-side via useMemo. This is more efficient.
    const allFlyerItems: FlyerItem[] = [
      createMockFlyerItem({
        flyer_item_id: 1,
        flyer_id: 1,
        item: 'Red Apples',
        price_display: '$1.99',
        price_in_cents: 199,
        master_item_id: 101, // matches mockWatchedItems
        master_item_name: 'Apples',
      }),
      createMockFlyerItem({
        flyer_item_id: 2,
        flyer_id: 1,
        item: 'Fresh Bread',
        price_display: '$2.99',
        price_in_cents: 299,
        master_item_id: 103, // NOT in initial mockWatchedItems
        master_item_name: 'Bread',
      }),
    ];

    mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify({ count: 2 })),
    );
    mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify(allFlyerItems)),
    );

    const { result, rerender } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    // Wait for initial data to load
    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });

    // Initially, only Apples (master_item_id: 101) should be in activeDeals
    expect(result.current.activeDeals).toHaveLength(1);
    expect(result.current.activeDeals[0].item).toBe('Red Apples');

    // API should have been called exactly once
    expect(mockedApiClient.fetchFlyerItemsForFlyers).toHaveBeenCalledTimes(1);

    // Now add Bread to watched items
    const newWatchedItems = [
      ...mockWatchedItems,
      createMockMasterGroceryItem({ master_grocery_item_id: 103, name: 'Bread' }),
    ];
    mockUseUserData.mockReturnValue({
      watchedItems: newWatchedItems,
      shoppingLists: [],
      setWatchedItems: vi.fn(),
      setShoppingLists: vi.fn(),
      isLoading: false,
      error: null,
    });

    // Rerender to pick up new watchedItems
    rerender();

    // After rerender, client-side filtering should now include both items
    await waitFor(() => {
      expect(result.current.activeDeals).toHaveLength(2);
    });

    // Verify both items are present
    const dealItems = result.current.activeDeals.map((d) => d.item);
    expect(dealItems).toContain('Red Apples');
    expect(dealItems).toContain('Fresh Bread');

    // The API should NOT be called again - data is already cached
    expect(mockedApiClient.fetchFlyerItemsForFlyers).toHaveBeenCalledTimes(1);
  });

  it('should include flyers valid exactly on the start or end date', async () => {
    // TODAY is 2024-01-15T12:00:00.000Z
    const boundaryFlyers: Flyer[] = [
      // Ends today
      createMockFlyer({
        flyer_id: 10,
        file_name: 'ends-today.pdf',
        item_count: 1,
        valid_from: '2024-01-01',
        valid_to: '2024-01-15',
        store: { store_id: 1, name: 'Store A' },
      }),
      // Starts today
      createMockFlyer({
        flyer_id: 11,
        file_name: 'starts-today.pdf',
        item_count: 1,
        valid_from: '2024-01-15',
        valid_to: '2024-01-30',
        store: { store_id: 1, name: 'Store B' },
      }),
      // Valid only today
      createMockFlyer({
        flyer_id: 12,
        file_name: 'only-today.pdf',
        item_count: 1,
        valid_from: '2024-01-15',
        valid_to: '2024-01-15',
        store: { store_id: 1, name: 'Store C' },
      }),
      // Ends yesterday (invalid)
      createMockFlyer({
        flyer_id: 13,
        file_name: 'ends-yesterday.pdf',
        item_count: 1,
        valid_from: '2024-01-01',
        valid_to: '2024-01-14',
        store: { store_id: 1, name: 'Store D' },
      }),
    ];

    mockUseFlyers.mockReturnValue({
      flyers: boundaryFlyers,
      isLoadingFlyers: false,
      flyersError: null,
      fetchNextFlyersPage: vi.fn(),
      hasNextFlyersPage: false,
      isRefetchingFlyers: false,
      refetchFlyers: vi.fn(),
    });

    mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify({ count: 0 })),
    );
    mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify([])));

    renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    await waitFor(() => {
      // Should call with IDs 10, 11, 12. Should NOT include 13.
      expect(mockedApiClient.countFlyerItemsForFlyers).toHaveBeenCalledWith([10, 11, 12]);
    });
  });

  it('should handle missing price_in_cents and quantity in deal items', async () => {
    const incompleteItem = createMockFlyerItem({
      flyer_item_id: 99,
      flyer_id: 1,
      item: 'Incomplete Item',
      price_display: '$1.99',
      // price_in_cents and quantity are missing
      master_item_id: 101,
      master_item_name: 'Apples',
      price_in_cents: null,
      quantity: undefined,
    });

    mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify({ count: 1 })),
    );
    mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(
      new Response(JSON.stringify([incompleteItem])),
    );

    const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });

    await waitFor(() => {
      expect(result.current.activeDeals).toHaveLength(1);
      const deal = result.current.activeDeals[0];
      expect(deal.price_in_cents).toBeNull();
      expect(deal.quantity).toBe('');
    });
  });
});
