// 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 { useAuth } from './useAuth';
import { AuthProvider } from '../providers/AuthProvider';
import * as apiClient from '../services/apiClient';
import type { User, UserProfile } from '../types';

// Mock the dependencies
vi.mock('../services/apiClient', () => ({
  // Mock other functions if needed
  getAuthenticatedUserProfile: vi.fn(),
}));

vi.mock('../services/logger.client', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  },
}));

const mockedApiClient = vi.mocked(apiClient);

// Define mock data
const mockUser: User = { user_id: 'user-abc-123', email: 'test@example.com' };
const mockProfile: UserProfile = {
  user_id: 'user-abc-123',
  full_name: 'Test User',
  points: 100,
  role: 'user',
  user: mockUser,
};

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

describe('useAuth Hook and AuthProvider', () => {
  // Mock localStorage
  let storage: { [key: string]: string } = {};
  const localStorageMock = {
    getItem: vi.fn((key: string) => storage[key] || null),
    setItem: vi.fn((key: string, value: string) => { storage[key] = value; }),
    removeItem: vi.fn((key: string) => { delete storage[key]; }),
    clear: vi.fn(() => { storage = {}; }),
  };

  beforeEach(() => {
    // Reset mocks and storage before each test
    vi.clearAllMocks();
    storage = {};
    Object.defineProperty(window, 'localStorage', { value: localStorageMock, configurable: true });
  });
  
  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.user).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', async () => {
      const { result } = renderHook(() => useAuth(), { wrapper });

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

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

    it('sets state to AUTHENTICATED if a valid token is found', async () => {
      localStorageMock.setItem('authToken', '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.user).toEqual(mockUser);
      expect(result.current.profile).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 () => {
      localStorageMock.setItem('authToken', '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.user).toBeNull();
      expect(result.current.profile).toBeNull();
      expect(localStorageMock.removeItem).toHaveBeenCalledWith('authToken');
    });
  });

  describe('login function', () => {
    it('sets token, fetches profile, and updates state on successful login', async () => {
      mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
        ok: true,
        status: 200,
        json: () => Promise.resolve(mockProfile),
      } as unknown as Response);
      
      const { result } = renderHook(() => useAuth(), { wrapper });

      // Wait for the initial effect to complete and loading to be false
      await waitFor(() => {
        expect(result.current.isLoading).toBe(false);
      });

      // Perform login
      await act(async () => {
        await result.current.login(mockUser, 'new-valid-token');
      });

      // Assertions
      expect(localStorageMock.setItem).toHaveBeenCalledWith('authToken', 'new-valid-token');
      // We must wait for the state update inside the hook to propagate
      await waitFor(() => {
        expect(result.current.authStatus).toBe('AUTHENTICATED');
        expect(result.current.user).toEqual(mockUser);
        expect(result.current.profile).toEqual(mockProfile);
      });
    });

    it('logs out and throws an error if profile fetch fails after login', async () => {
      const fetchError = new Error('API is down');
      mockedApiClient.getAuthenticatedUserProfile.mockRejectedValue(fetchError);

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

      // We expect the login to fail. The specific error message might vary depending on 
      // internal error handling (e.g., if it catches and tries to access null properties),
      // so we check for the main error prefix.
      await act(async () => {
         await expect(result.current.login(mockUser, 'new-token')).rejects.toThrow(
          /Login succeeded, but failed to fetch your data/
        );
      });
      
      // Should trigger the logout flow
      expect(localStorageMock.removeItem).toHaveBeenCalledWith('authToken');
      expect(result.current.authStatus).toBe('SIGNED_OUT');
      expect(result.current.user).toBeNull();
    });
  });

  describe('logout function', () => {
    it('removes token and resets auth state', async () => {
      // Start in a logged-in state
      localStorageMock.setItem('authToken', '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.user).not.toBeNull();

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

      expect(localStorageMock.removeItem).toHaveBeenCalledWith('authToken');
      expect(result.current.authStatus).toBe('SIGNED_OUT');
      expect(result.current.user).toBeNull();
      expect(result.current.profile).toBeNull();
    });
  });
  
  describe('updateProfile function', () => {
    it('merges new data into the existing profile state', async () => {
      // Start in a logged-in state
      localStorageMock.setItem('authToken', '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.profile?.full_name).toBe('Test User');

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

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

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