/**
 * Tests for the AuthContext
 * @jest-environment jsdom
 */

import React from 'react';
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react';
import { AuthProvider, useAuth } from '@/contexts/AuthContext';
import { authService } from '@/services/auth.service';
import toast from 'react-hot-toast';

// Mock authService
jest.mock('@/services/auth.service', () => ({
  authService: {
    login: jest.fn(),
    register: jest.fn(),
    logout: jest.fn(),
    getCurrentUser: jest.fn(),
    googleLogin: jest.fn(),
    facebookLogin: jest.fn(),
  },
}));

// Mock react-hot-toast
jest.mock('react-hot-toast', () => ({
  success: jest.fn(),
  error: jest.fn(),
}));

// Mock utils/helpers
jest.mock('@/utils/helpers', () => ({
  setToken: jest.fn(),
  removeToken: jest.fn(),
  getToken: jest.fn(),
  storage: {
    get: jest.fn(),
    set: jest.fn(),
    remove: jest.fn(),
  },
}));

import { setToken, removeToken, getToken, storage } from '@/utils/helpers';

// Mock next/navigation
const mockPush = jest.fn();
const mockReplace = jest.fn();
jest.mock('next/navigation', () => ({
  useRouter: () => ({
    push: mockPush,
    replace: mockReplace,
    prefetch: jest.fn(),
  }),
}));

// Test component that uses the hook
const TestComponent: React.FC = () => {
  const { user, isLoading, isAuthenticated, login, logout, register } = useAuth();

  if (isLoading) return <div>Loading...</div>;

  const handleLogin = async () => {
    try {
      await login({ email: 'test@test.com', password: 'password' });
    } catch {
      // Error is handled in AuthContext
    }
  };

  const handleRegister = async () => {
    try {
      await register({
        email: 'new@test.com',
        password: 'password',
        firstName: 'John',
        lastName: 'Doe',
      });
    } catch {
      // Error is handled in AuthContext
    }
  };

  return (
    <div>
      <div data-testid="authenticated">{isAuthenticated ? 'yes' : 'no'}</div>
      <div data-testid="user">{user ? JSON.stringify(user) : 'null'}</div>
      <button onClick={handleLogin}>Login</button>
      <button onClick={() => logout()}>Logout</button>
      <button onClick={handleRegister}>Register</button>
    </div>
  );
};

describe('AuthContext', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    (getToken as jest.Mock).mockReturnValue(null);
  });

  describe('Initial state', () => {
    it('should show loading state when token exists', async () => {
      // When token exists, it fetches user data which shows loading
      (getToken as jest.Mock).mockReturnValue('valid-token');

      // Delay the response to ensure loading state is visible
      let resolveUser: (value: unknown) => void;
      const userPromise = new Promise((resolve) => {
        resolveUser = resolve;
      });
      (authService.getCurrentUser as jest.Mock).mockReturnValue(userPromise);

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      // Should show loading while fetching user
      expect(screen.getByText('Loading...')).toBeInTheDocument();

      // Resolve the promise
      await act(async () => {
        resolveUser!({ id: '1', email: 'test@test.com', role: 'CLIENT' });
      });

      await waitFor(() => {
        expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
      });
    });

    it('should have null user when no token exists', async () => {
      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      // When no token, loading completes immediately
      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('null');
        expect(screen.getByTestId('authenticated')).toHaveTextContent('no');
      });
    });

    it('should load user when token exists', async () => {
      const mockUser = {
        id: '1',
        email: 'test@test.com',
        firstName: 'John',
        lastName: 'Doe',
        role: 'CLIENT',
      };

      (getToken as jest.Mock).mockReturnValue('valid-token');
      (authService.getCurrentUser as jest.Mock).mockResolvedValueOnce(mockUser);

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.getByTestId('authenticated')).toHaveTextContent('yes');
      });
    });

    it('should remove token if getCurrentUser fails', async () => {
      (getToken as jest.Mock).mockReturnValue('invalid-token');
      (authService.getCurrentUser as jest.Mock).mockRejectedValueOnce(
        new Error('Invalid token')
      );

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(removeToken).toHaveBeenCalled();
        expect(storage.remove).toHaveBeenCalled();
      });
    });
  });

  describe('login', () => {
    it('should login successfully and redirect CLIENT', async () => {
      const mockResponse = {
        token: 'new-token',
        user: {
          id: '1',
          email: 'test@test.com',
          firstName: 'John',
          lastName: 'Doe',
          role: 'CLIENT',
        },
      };

      (authService.login as jest.Mock).mockResolvedValueOnce(mockResponse);

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
      });

      const loginButton = screen.getByText('Login');
      await act(async () => {
        fireEvent.click(loginButton);
      });

      await waitFor(() => {
        expect(setToken).toHaveBeenCalledWith('new-token');
        expect(toast.success).toHaveBeenCalledWith('Connexion réussie !');
        expect(mockReplace).toHaveBeenCalledWith('/client');
      });
    });

    it('should redirect ADMIN to admin dashboard', async () => {
      const mockResponse = {
        token: 'admin-token',
        user: {
          id: '1',
          email: 'admin@test.com',
          firstName: 'Admin',
          lastName: 'User',
          role: 'ADMIN',
        },
      };

      (authService.login as jest.Mock).mockResolvedValueOnce(mockResponse);

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
      });

      const loginButton = screen.getByText('Login');
      await act(async () => {
        fireEvent.click(loginButton);
      });

      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalledWith('/admin/dashboard');
      });
    });

    it('should redirect EMPLOYEE to employee dashboard', async () => {
      const mockResponse = {
        token: 'employee-token',
        user: {
          id: '1',
          email: 'employee@test.com',
          firstName: 'Employee',
          lastName: 'User',
          role: 'EMPLOYEE',
        },
      };

      (authService.login as jest.Mock).mockResolvedValueOnce(mockResponse);

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
      });

      const loginButton = screen.getByText('Login');
      await act(async () => {
        fireEvent.click(loginButton);
      });

      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalledWith('/employe/dashboard');
      });
    });

    it('should show error toast on login failure', async () => {
      const errorMessage = 'Invalid credentials';
      (authService.login as jest.Mock).mockRejectedValueOnce(
        new Error(errorMessage)
      );

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
      });

      const loginButton = screen.getByText('Login');
      await act(async () => {
        fireEvent.click(loginButton);
      });

      await waitFor(() => {
        expect(toast.error).toHaveBeenCalledWith(errorMessage);
      });
    });
  });

  describe('register', () => {
    it('should register successfully', async () => {
      const mockResponse = {
        token: 'new-user-token',
        user: {
          id: '2',
          email: 'new@test.com',
          firstName: 'John',
          lastName: 'Doe',
          role: 'CLIENT',
        },
      };

      (authService.register as jest.Mock).mockResolvedValueOnce(mockResponse);

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
      });

      const registerButton = screen.getByText('Register');
      await act(async () => {
        fireEvent.click(registerButton);
      });

      await waitFor(() => {
        expect(setToken).toHaveBeenCalledWith('new-user-token');
        expect(toast.success).toHaveBeenCalledWith('Inscription réussie !');
        expect(mockPush).toHaveBeenCalledWith('/client');
      });
    });
  });

  describe('logout', () => {
    it('should logout and redirect to login page', async () => {
      const mockUser = {
        id: '1',
        email: 'test@test.com',
        firstName: 'John',
        lastName: 'Doe',
        role: 'CLIENT',
      };

      (getToken as jest.Mock).mockReturnValue('valid-token');
      (authService.getCurrentUser as jest.Mock).mockResolvedValueOnce(mockUser);
      (authService.logout as jest.Mock).mockResolvedValueOnce(undefined);

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.getByTestId('authenticated')).toHaveTextContent('yes');
      });

      const logoutButton = screen.getByText('Logout');
      await act(async () => {
        fireEvent.click(logoutButton);
      });

      await waitFor(() => {
        expect(removeToken).toHaveBeenCalled();
        expect(storage.remove).toHaveBeenCalled();
        expect(toast.success).toHaveBeenCalledWith('Déconnexion réussie');
        expect(mockPush).toHaveBeenCalledWith('/login');
      });
    });

    it('should still logout even if API call fails', async () => {
      (getToken as jest.Mock).mockReturnValue('valid-token');
      (authService.getCurrentUser as jest.Mock).mockResolvedValueOnce({
        id: '1',
        email: 'test@test.com',
        role: 'CLIENT',
      });
      (authService.logout as jest.Mock).mockRejectedValueOnce(
        new Error('Logout failed')
      );

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.getByTestId('authenticated')).toHaveTextContent('yes');
      });

      const logoutButton = screen.getByText('Logout');
      await act(async () => {
        fireEvent.click(logoutButton);
      });

      await waitFor(() => {
        expect(removeToken).toHaveBeenCalled();
        expect(mockPush).toHaveBeenCalledWith('/login');
      });
    });
  });

  describe('useAuth hook error', () => {
    it('should throw error when used outside AuthProvider', () => {
      const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});

      expect(() => {
        render(<TestComponent />);
      }).toThrow('useAuth must be used within an AuthProvider');

      consoleError.mockRestore();
    });
  });
});
