// src/hooks/useUserData.test.tsx
import React, { ReactNode } from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useUserData } from '../hooks/useUserData';
import { useAuth } from '../hooks/useAuth';
import { UserDataProvider } from '../providers/UserDataProvider';
import { useApiOnMount } from './useApiOnMount';
import type { MasterGroceryItem, ShoppingList, UserProfile } from '../types';

// 1. Mock the hook's dependencies
vi.mock('../hooks/useAuth');
vi.mock('./useApiOnMount');

// 2. Create typed mocks for type safety and autocompletion
const mockedUseAuth = vi.mocked(useAuth);
const mockedUseApiOnMount = vi.mocked(useApiOnMount);

// 3. A simple wrapper component that renders our provider.
// This is necessary because the useUserData hook needs to be a child of UserDataProvider.
const wrapper = ({ children }: { children: ReactNode }) => <UserDataProvider>{children}</UserDataProvider>; // No change needed here, just for context

// 4. Mock data for testing
const mockUser: UserProfile = {
  user_id: 'user-123',
  full_name: 'Test User',
  role: 'user',
  points: 100,
  user: { user_id: 'user-123', email: 'test@example.com' },
};

const mockWatchedItems: MasterGroceryItem[] = [
  { master_grocery_item_id: 1, name: 'Milk', created_at: '' },
];

const mockShoppingLists: ShoppingList[] = [
  { shopping_list_id: 1, name: 'Weekly Groceries', user_id: 'user-123', created_at: '', items: [] },
];

describe('useUserData Hook and UserDataProvider', () => {
  beforeEach(() => {
    // Clear mock history before each test to ensure test isolation.
    vi.clearAllMocks();
  });

  it('should throw an error if useUserData is used outside of UserDataProvider', () => {
    // Suppress the expected console.error for this test to keep the output clean.
    const originalError = console.error;
    console.error = vi.fn();
    
    // Expecting renderHook to throw an error because there's no provider.
    expect(() => renderHook(() => useUserData())).toThrow('useUserData must be used within a UserDataProvider');
    
    // Restore the original console.error function.
    console.error = originalError;
  });

  it('should return initial empty state when user is not authenticated', () => {
    // Arrange: Simulate a logged-out user.
    mockedUseAuth.mockReturnValue({
      user: null,
      profile: null,
      authStatus: 'SIGNED_OUT',
      isLoading: false,
      login: vi.fn(),
      logout: vi.fn(),
      updateProfile: vi.fn(),
    });
    // Arrange: Mock the return value of the inner hooks.
    mockedUseApiOnMount.mockReturnValue({ data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() });

    // Act: Render the hook within the provider wrapper.
    const { result } = renderHook(() => useUserData(), { wrapper });

    // Assert: Check that the context values are in their initial, empty state.
    expect(result.current.isLoading).toBe(false);
    expect(result.current.watchedItems).toEqual([]);
    expect(result.current.shoppingLists).toEqual([]);
    expect(result.current.error).toBeNull();
    // Assert: Check that useApiOnMount was called with `enabled: false`.
    expect(mockedUseApiOnMount).toHaveBeenCalledWith(expect.any(Function), [null], { enabled: false });
  });

  it('should return loading state when user is authenticated and data is fetching', () => {
    // Arrange: Simulate a logged-in user.
    mockedUseAuth.mockReturnValue({
      user: mockUser.user,
      profile: mockUser,
      authStatus: 'AUTHENTICATED',
      isLoading: false,
      login: vi.fn(),
      logout: vi.fn(),
      updateProfile: vi.fn(),
    });
    // Arrange: Mock one of the inner hooks to be in a loading state.
    mockedUseApiOnMount
      .mockReturnValueOnce({ data: null, loading: true, error: null, isRefetching: false, reset: vi.fn() }) // watched items
      .mockReturnValueOnce({ data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() }); // shopping lists

    // Act
    const { result } = renderHook(() => useUserData(), { wrapper });

    // Assert
    expect(result.current.isLoading).toBe(true);
  });

  it('should return data on successful fetch when user is authenticated', async () => {
    // Arrange: Simulate a logged-in user.
    mockedUseAuth.mockReturnValue({
      user: mockUser.user,
      profile: mockUser,
      authStatus: 'AUTHENTICATED',
      isLoading: false,
      login: vi.fn(),
      logout: vi.fn(),
      updateProfile: vi.fn(),
    });
    // Arrange: Mock successful data fetches for both inner hooks.
    mockedUseApiOnMount
      .mockReturnValueOnce({ data: mockWatchedItems, loading: false, error: null, isRefetching: false, reset: vi.fn() })
      .mockReturnValueOnce({ data: mockShoppingLists, loading: false, error: null, isRefetching: false, reset: vi.fn() });

    // Act
    const { result } = renderHook(() => useUserData(), { wrapper });

    // Assert: Use `waitFor` to allow the `useEffect` hook to update the state.
    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.watchedItems).toEqual(mockWatchedItems);
      expect(result.current.shoppingLists).toEqual(mockShoppingLists);
      expect(result.current.error).toBeNull();
    });
  });

  it('should return an error state if one of the fetches fails', async () => {
    // Arrange: Simulate a logged-in user.
    mockedUseAuth.mockReturnValue({
      user: mockUser.user,
      profile: mockUser,
      authStatus: 'AUTHENTICATED',
      isLoading: false,
      login: vi.fn(),
      logout: vi.fn(),
      updateProfile: vi.fn(),
    });
    const mockError = new Error('Failed to fetch watched items');
    
    // Arrange: Mock the behavior persistently to handle re-renders.
    // We use mockImplementation to return based on call order in a loop or similar,
    // OR just use mockReturnValueOnce enough times.
    // Since we don't know exact render count, mockImplementation is safer if valid.
    // But simplified: assuming 2 hooks called per render.
    
    // reset mocks to be sure
    mockedUseApiOnMount.mockReset();
    
    // Define the sequence: 1st call (Watched) -> Error, 2nd call (Shopping) -> Success
    // We want this to persist for multiple renders.
    mockedUseApiOnMount.mockImplementation((_fn) => {
        // We can't easily distinguish based on 'fn' arg without inspecting it, 
        // but we know the order is Watched then Shopping in the provider.
        // A simple toggle approach works if strict order is maintained.
        // However, stateless mocks are better.
        // Let's fallback to setting up "many" return values.
        return { data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() };
    });
    
    mockedUseApiOnMount
      .mockReturnValueOnce({ data: null, loading: false, error: mockError, isRefetching: false, reset: vi.fn() }) // 1st render: Watched
      .mockReturnValueOnce({ data: mockShoppingLists, loading: false, error: null, isRefetching: false, reset: vi.fn() }) // 1st render: Shopping
      .mockReturnValueOnce({ data: null, loading: false, error: mockError, isRefetching: false, reset: vi.fn() }) // 2nd render: Watched
      .mockReturnValueOnce({ data: mockShoppingLists, loading: false, error: null, isRefetching: false, reset: vi.fn() }); // 2nd render: Shopping

    // Act
    const { result } = renderHook(() => useUserData(), { wrapper });

    // Assert
    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.error).toBe('Failed to fetch watched items');
      // Data that was fetched successfully should still be populated.
      expect(result.current.shoppingLists).toEqual(mockShoppingLists);
      // Data that failed should be empty.
      expect(result.current.watchedItems).toEqual([]);
    });
  });

  it('should clear data when the user logs out', async () => {
    // Arrange: Start with a logged-in user and data.
    mockedUseAuth.mockReturnValue({
      user: mockUser.user,
      profile: mockUser,
      authStatus: 'AUTHENTICATED',
      isLoading: false,
      login: vi.fn(),
      logout: vi.fn(),
      updateProfile: vi.fn(),
    });
    mockedUseApiOnMount
      .mockReturnValueOnce({ data: mockWatchedItems, loading: false, error: null, isRefetching: false, reset: vi.fn() })
      .mockReturnValueOnce({ data: mockShoppingLists, loading: false, error: null, isRefetching: false, reset: vi.fn() });
    const { result, rerender } = renderHook(() => useUserData(), { wrapper });
    await waitFor(() => expect(result.current.watchedItems).not.toEqual([]));

    // Act: Simulate logout by re-rendering with a null user.
    mockedUseAuth.mockReturnValue({
      user: null,
      profile: null,
      authStatus: 'SIGNED_OUT',
      isLoading: false,
      login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn()
    });
    rerender();

    // Assert: The data should now be cleared.
    expect(result.current.watchedItems).toEqual([]);
    expect(result.current.shoppingLists).toEqual([]);
  });
});