// src/hooks/useWatchedItems.test.tsx
import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useWatchedItems } from './useWatchedItems';
import { useApi } from './useApi';
import { useAuth } from '../hooks/useAuth';
import { useUserData } from '../hooks/useUserData';
import type { MasterGroceryItem, User } from '../types';

// Mock the hooks that useWatchedItems depends on
vi.mock('./useApi');
vi.mock('../hooks/useAuth');
vi.mock('../hooks/useUserData');

// The apiClient is globally mocked in our test setup, so we just need to cast it
const mockedUseApi = vi.mocked(useApi);
const mockedUseAuth = vi.mocked(useAuth);
const mockedUseUserData = vi.mocked(useUserData);

const mockUser: User = { user_id: 'user-123', email: 'test@example.com' };
const mockInitialItems: MasterGroceryItem[] = [
  { master_grocery_item_id: 1, name: 'Milk', created_at: '' },
  { master_grocery_item_id: 2, name: 'Bread', created_at: '' },
];

describe('useWatchedItems Hook', () => {
  // Create a mock setter function that we can spy on
  const mockSetWatchedItems = vi.fn();
  const mockAddWatchedItemApi = vi.fn();
  const mockRemoveWatchedItemApi = vi.fn();

  beforeEach(() => {
    // Reset all mocks before each test to ensure isolation
    // Use resetAllMocks to ensure previous test implementations (like mockResolvedValue) don't leak.
    vi.resetAllMocks();
    // Default mock for useApi to handle any number of calls/re-renders safely
    mockedUseApi.mockReturnValue({
      execute: vi.fn(),
      error: null,
      data: null,
      loading: false,
      isRefetching: false,
      reset: vi.fn()
    });

    // Specific overrides for the first render sequence: 
    // 1st call = addWatchedItemApi, 2nd call = removeWatchedItemApi
    mockedUseApi
      .mockReturnValueOnce({ execute: mockAddWatchedItemApi, error: null, data: null, loading: false, isRefetching: false, reset: vi.fn() })
      .mockReturnValueOnce({ execute: mockRemoveWatchedItemApi, error: null, data: null, loading: false, isRefetching: false, reset: vi.fn() });

    // Provide a default implementation for the mocked hooks
    mockedUseAuth.mockReturnValue({
      user: mockUser,
      profile: null,
      authStatus: 'AUTHENTICATED',
      isLoading: false,
      login: vi.fn(),
      logout: vi.fn(),
      updateProfile: vi.fn(),
    });

    mockedUseUserData.mockReturnValue({
      watchedItems: mockInitialItems,
      setWatchedItems: mockSetWatchedItems,
      shoppingLists: [],
      setShoppingLists: vi.fn(),
      isLoading: false,
      error: null,
    });
  });

  it('should initialize with the watched items from useData', () => {
    const { result } = renderHook(() => useWatchedItems());

    expect(result.current.watchedItems).toEqual(mockInitialItems);
    expect(result.current.error).toBeNull();
  });

  describe('addWatchedItem', () => {
    it('should call the API and update state on successful addition', async () => {
      const newItem: MasterGroceryItem = { master_grocery_item_id: 3, name: 'Cheese', created_at: '' };
      mockAddWatchedItemApi.mockResolvedValue(newItem);

      const { result } = renderHook(() => useWatchedItems());

      await act(async () => {
        await result.current.addWatchedItem('Cheese', 'Dairy');
      });

      expect(mockAddWatchedItemApi).toHaveBeenCalledWith('Cheese', 'Dairy');
      // Check that the global state setter was called with an updater function
      expect(mockSetWatchedItems).toHaveBeenCalledWith(expect.any(Function));

      // To verify the logic inside the updater, we can call it directly
      const updater = mockSetWatchedItems.mock.calls[0][0];
      const newState = updater(mockInitialItems);

      expect(newState).toHaveLength(3);
      expect(newState).toContainEqual(newItem);
    });

    it('should set an error message if the API call fails', async () => {
      // Clear existing mocks to set a specific sequence for this test
      mockedUseApi.mockReset();
      
      // Default fallback
      mockedUseApi.mockReturnValue({ execute: vi.fn(), error: null, data: null, loading: false, isRefetching: false, reset: vi.fn() });

      // Mock the first call (add) to return an error immediately
      mockedUseApi
        .mockReturnValueOnce({
          execute: mockAddWatchedItemApi,
          error: new Error('API Error'),
          data: null,
          loading: false,
          isRefetching: false,
          reset: vi.fn(),
        })
        .mockReturnValueOnce({ execute: mockRemoveWatchedItemApi, error: null, data: null, loading: false, isRefetching: false, reset: vi.fn() });

      const { result } = renderHook(() => useWatchedItems());

      await act(async () => {
        await result.current.addWatchedItem('Failing Item', 'Error');
      });
      expect(result.current.error).toBe('API Error');
      expect(mockSetWatchedItems).not.toHaveBeenCalled();
    });
  });

  describe('removeWatchedItem', () => {
    it('should call the API and update state on successful removal', async () => {
      const itemIdToRemove = 1;
      mockRemoveWatchedItemApi.mockResolvedValue(null); // Successful 204 returns null

      const { result } = renderHook(() => useWatchedItems());

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

      await waitFor(() => {
        expect(mockRemoveWatchedItemApi).toHaveBeenCalledWith(itemIdToRemove);
      });
      expect(mockSetWatchedItems).toHaveBeenCalledWith(expect.any(Function));

      // Verify the logic inside the updater function
      const updater = mockSetWatchedItems.mock.calls[0][0];
      const newState = updater(mockInitialItems);

      expect(newState).toHaveLength(1);
      expect(newState.some((item: MasterGroceryItem) => item.master_grocery_item_id === itemIdToRemove)).toBe(false);
    });

    it('should set an error message if the API call fails', async () => {
      // Clear existing mocks
      mockedUseApi.mockReset();
      
      // Ensure the execute function returns null/undefined so the hook doesn't try to set state
      mockAddWatchedItemApi.mockResolvedValue(null);

      // Default fallback
      mockedUseApi.mockReturnValue({ execute: vi.fn(), error: null, data: null, loading: false, isRefetching: false, reset: vi.fn() });

      // Mock sequence: 1st (add) success, 2nd (remove) error
      mockedUseApi
        .mockReturnValueOnce({ execute: vi.fn(), error: null, data: null, loading: false, isRefetching: false, reset: vi.fn() }) 
        .mockReturnValueOnce({ execute: vi.fn(), error: new Error('Deletion Failed'), data: null, loading: false, isRefetching: false, reset: vi.fn() });

      const { result } = renderHook(() => useWatchedItems());

      await act(async () => {
        await result.current.removeWatchedItem(999);
      });
      
      expect(result.current.error).toBe('Deletion Failed');
      expect(mockSetWatchedItems).not.toHaveBeenCalled();
    });
  });

  it('should not perform actions if the user is not authenticated', async () => {
    mockedUseAuth.mockReturnValue({
      user: null,
      profile: null,
      authStatus: 'SIGNED_OUT',
      isLoading: false,
      login: vi.fn(),
      logout: vi.fn(),
      updateProfile: vi.fn(),
    });

    const { result } = renderHook(() => useWatchedItems());

    await act(async () => {
      await result.current.addWatchedItem('Test', 'Category');
      await result.current.removeWatchedItem(1);
    });

    expect(mockAddWatchedItemApi).not.toHaveBeenCalled();
    expect(mockRemoveWatchedItemApi).not.toHaveBeenCalled();
  });
});