// src/hooks/useAuth.test.tsx
import React, { ReactNode } from 'react';
import { renderHook, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useAuth } from './useAuth';
import { AuthProvider } from '../providers/AuthProvider';
import * as apiClient from '../services/apiClient';
import type { UserProfile } from '../types';
import * as tokenStorage from '../services/tokenStorage';
import { createMockUserProfile } from '../tests/utils/mockFactories';
import { logger } from '../services/logger.client';

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

const mockedApiClient = vi.mocked(apiClient);
const mockedTokenStorage = vi.mocked(tokenStorage);

const mockProfile: UserProfile = createMockUserProfile({
  full_name: 'Test User',
  points: 100,
  role: 'user',
  user: { user_id: 'user-abc-123', email: 'test@example.com' },
});

// Create a fresh QueryClient for each test to ensure isolation
const createTestQueryClient = () =>
  new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
        gcTime: 0,
      },
      mutations: {
        retry: false,
      },
    },
  });

// Reusable wrapper for rendering the hook within the provider
const wrapper = ({ children }: { children: ReactNode }) => {
  const testQueryClient = createTestQueryClient();
  return (
    <QueryClientProvider client={testQueryClient}>
      <AuthProvider>{children}</AuthProvider>
    </QueryClientProvider>
  );
};

describe('useAuth Hook and AuthProvider', () => {
  beforeEach(() => {
    // Reset mocks and storage before each test
    vi.clearAllMocks();
  });

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

  it('throws an error when useAuth is used outside of an AuthProvider', () => {
    // Suppress console error for this expected failure
    const originalError = console.error;
    console.error = vi.fn();
    expect(() => renderHook(() => useAuth())).toThrow(
      'useAuth must be used within an AuthProvider',
    );
    console.error = originalError;
  });
  it('initializes with a default state', async () => {
    const { result } = renderHook(() => useAuth(), { wrapper });

    // We verify that it starts in a loading state or quickly resolves to signed out
    // depending on the execution speed.
    // To avoid flakiness, we just ensure it is in a valid state structure.
    expect(result.current.userProfile).toBeNull();
    // It should eventually settle
    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });
  });

  describe('Initial Auth Check (useEffect)', () => {
    it('sets state to SIGNED_OUT if no token is found in storage', async () => {
      mockedTokenStorage.getToken.mockReturnValue(null);
      const { result } = renderHook(() => useAuth(), { wrapper });

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

      expect(result.current.authStatus).toBe('SIGNED_OUT');
      expect(result.current.userProfile).toBeNull();
    });

    it('sets state to AUTHENTICATED if a valid token is found', async () => {
      mockedTokenStorage.getToken.mockReturnValue('valid-token');
      mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
        ok: true,
        status: 200,
        json: () => Promise.resolve(mockProfile),
      } as unknown as Response);

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

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

      expect(result.current.authStatus).toBe('AUTHENTICATED');
      expect(result.current.userProfile).toEqual(mockProfile);

      // Check that it was called at least once.
      // React 18 Strict Mode might call effects twice in dev/test environment.
      expect(mockedApiClient.getAuthenticatedUserProfile.mock.calls.length).toBeGreaterThanOrEqual(
        1,
      );
    });

    it('sets state to SIGNED_OUT and removes token if validation fails', async () => {
      mockedTokenStorage.getToken.mockReturnValue('invalid-token');
      mockedApiClient.getAuthenticatedUserProfile.mockRejectedValue(new Error('Invalid token'));

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

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

      expect(result.current.authStatus).toBe('SIGNED_OUT');
      expect(result.current.userProfile).toBeNull();
      expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
    });
  });

  it('sets state to SIGNED_OUT and removes token if profile fetch returns null after token validation', async () => {
    mockedTokenStorage.getToken.mockReturnValue('valid-token');
    // Mock getAuthenticatedUserProfile to return a 200 OK response with a null body
    mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
      ok: true,
      status: 200,
      json: () => Promise.resolve(null), // Simulate API returning no profile data
    } as unknown as Response);

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

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

    expect(result.current.authStatus).toBe('SIGNED_OUT');
    expect(result.current.userProfile).toBeNull();
    expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
    expect(logger.warn).toHaveBeenCalledWith(
      '[AuthProvider] Token was present but validation failed. Signing out.',
    );
  });

  describe('login function', () => {
    // This was the failing test
    it('sets token, fetches profile, and updates state on successful login', async () => {
      // --- FIX ---
      // Explicitly mock that no token exists initially to prevent state leakage from other tests.
      mockedTokenStorage.getToken.mockReturnValue(null);

      // --- FIX ---
      // The mock for `getAuthenticatedUserProfile` must resolve to a `Response`-like object,
      // as this is the return type of the actual function. The `useApi` hook then
      // processes this response. This mock is now type-safe.
      mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
        ok: true,
        status: 200,
        json: () => Promise.resolve(mockProfile),
      } as Response);

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

      // 1. Wait for the initial effect to complete and loading to be false
      console.log('[TEST-DEBUG] Waiting for initial auth check to complete...');
      await waitFor(() => {
        expect(result.current.isLoading).toBe(false);
      });
      console.log(
        '[TEST-DEBUG] Initial auth check complete. Current status:',
        result.current.authStatus,
      );
      expect(result.current.authStatus).toBe('SIGNED_OUT');

      // 2. Perform login
      await act(async () => {
        console.log('[TEST-DEBUG] Calling login function...');
        await result.current.login('new-valid-token', mockProfile);
        console.log('[TEST-DEBUG] Login function promise resolved.');
      });

      console.log('[TEST-DEBUG] State immediately after login `act` call:', result.current);

      // 3. Assertions
      expect(mockedTokenStorage.setToken).toHaveBeenCalledWith('new-valid-token');

      // 4. We must wait for the state update inside the hook to propagate
      await waitFor(() => {
        console.log(
          `[TEST-DEBUG] Checking authStatus in waitFor... Current status: ${result.current.authStatus}`,
        );
        expect(result.current.authStatus).toBe('AUTHENTICATED');
      });

      console.log('[TEST-DEBUG] Final state after successful login:', result.current);
      expect(result.current.userProfile).toEqual(mockProfile);
    });

    it('logs out and throws an error if profile fetch fails after login', async () => {
      const fetchError = new Error('API is down');
      // The hook will throw, so we mock the rejection.
      mockedApiClient.getAuthenticatedUserProfile.mockRejectedValue(fetchError);

      const { result } = renderHook(() => useAuth(), { wrapper });
      await waitFor(() => expect(result.current.isLoading).toBe(false));

      // The login function should reject the promise it returns.
      await act(async () => {
        await expect(result.current.login('new-token')).rejects.toThrow(
          /Login succeeded, but failed to fetch your data/,
        );
      });

      // Should trigger the logout flow
      expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
      expect(result.current.authStatus).toBe('SIGNED_OUT'); // This was a duplicate, fixed.
      expect(result.current.userProfile).toBeNull();
    });

    it('logs out and throws an error if profile fetch returns null after login (no profileData)', async () => {
      // Simulate successful token setting, but subsequent profile fetch returns null
      mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
        ok: true,
        status: 200,
        json: () => Promise.resolve(null), // Simulate API returning no profile data
      } as unknown as Response);

      const { result } = renderHook(() => useAuth(), { wrapper });
      await waitFor(() => expect(result.current.isLoading).toBe(false));

      // Call login without profileData, forcing a profile fetch
      await act(async () => {
        await expect(result.current.login('new-token-no-profile-data')).rejects.toThrow(
          'Login succeeded, but failed to fetch your data: Received null or undefined profile from API.',
        );
      });

      // Should trigger the logout flow
      expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
      expect(result.current.authStatus).toBe('SIGNED_OUT');
      expect(result.current.userProfile).toBeNull();
      expect(logger.error).toHaveBeenCalledWith(
        expect.any(String), // The error message
        expect.objectContaining({ error: 'Received null or undefined profile from API.' }),
      );
    });
  });

  describe('logout function', () => {
    it('removes token and resets auth state', async () => {
      // Start in a logged-in state by mocking the token storage
      mockedTokenStorage.getToken.mockReturnValue('valid-token');
      mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
        ok: true,
        status: 200,
        json: () => Promise.resolve(mockProfile),
      } as unknown as Response);

      const { result } = renderHook(() => useAuth(), { wrapper });
      await waitFor(() => expect(result.current.authStatus).toBe('AUTHENTICATED'));

      expect(result.current.userProfile).not.toBeNull();

      act(() => {
        result.current.logout();
      });

      expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
      expect(result.current.authStatus).toBe('SIGNED_OUT');
      expect(result.current.userProfile).toBeNull();
    });
  });

  describe('updateProfile function', () => {
    it('merges new data into the existing profile state', async () => {
      // Start in a logged-in state
      mockedTokenStorage.getToken.mockReturnValue('valid-token');
      mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
        ok: true,
        status: 200,
        json: () => Promise.resolve(mockProfile),
      } as unknown as Response);

      const { result } = renderHook(() => useAuth(), { wrapper });
      await waitFor(() => expect(result.current.authStatus).toBe('AUTHENTICATED'));

      expect(result.current.userProfile?.full_name).toBe('Test User');

      const updatedData: Partial<UserProfile> = {
        full_name: 'Test User Updated',
        points: 150,
      };

      act(() => {
        result.current.updateProfile(updatedData);
      });

      expect(result.current.userProfile?.full_name).toBe('Test User Updated');
      expect(result.current.userProfile?.points).toBe(150);
      // Ensure other data was not overwritten
      expect(result.current.userProfile?.role).toBe('user');
    });

    it('should not update profile if user is not authenticated', async () => {
      // --- FIX ---
      // Explicitly mock that no token exists initially to prevent state leakage from other tests.
      mockedTokenStorage.getToken.mockReturnValue(null);

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

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

      expect(result.current.authStatus).toBe('SIGNED_OUT');
      expect(result.current.userProfile).toBeNull();

      const updatedData: Partial<UserProfile> = {
        full_name: 'Should Not Update',
      };

      act(() => result.current.updateProfile(updatedData));

      expect(result.current.userProfile).toBeNull();
    });
  });
});
