import React from 'react';
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react';
import { vi, type Mocked, type MockedFunction } from 'vitest';
import { AuthProvider, useAuth } from '../../contexts/AuthContext';
import { mockUser } from '../../mocks/handlers';

// Mock the API module
vi.mock('../../services/api', () => {
  const mockAuthAPI = {
    login: vi.fn(),
    register: vi.fn(),
    googleLogin: vi.fn(),
    logout: vi.fn(),
    getStatus: vi.fn(),
  };

  const mockFetchCSRFToken = vi.fn().mockResolvedValue('test-csrf-token');
  const mockResetCSRFToken = vi.fn();

  return {
    authAPI: mockAuthAPI,
    fetchCSRFToken: mockFetchCSRFToken,
    resetCSRFToken: mockResetCSRFToken,
  };
});

// Get mocked modules
import { authAPI, fetchCSRFToken, resetCSRFToken } from '../../services/api';

const mockAuthAPI = authAPI as Mocked<typeof authAPI>;
const mockFetchCSRFToken = fetchCSRFToken as MockedFunction<typeof fetchCSRFToken>;
const mockResetCSRFToken = resetCSRFToken as MockedFunction<typeof resetCSRFToken>;

// Helper to create mock Axios response
const mockAxiosResponse = <T,>(data: T) => ({
  data,
  status: 200,
  statusText: 'OK',
  headers: {},
  config: {} as any,
});

// Test component that uses the auth context
const TestComponent: React.FC = () => {
  const auth = useAuth();

  return (
    <div>
      <div data-testid="loading">{auth.loading ? 'loading' : 'not-loading'}</div>
      <div data-testid="user">{auth.user ? auth.user.email : 'no-user'}</div>
      <div data-testid="verified">{auth.user?.isVerified ? 'verified' : 'not-verified'}</div>
      <div data-testid="modal-open">{auth.showAuthModal ? 'open' : 'closed'}</div>
      <div data-testid="modal-mode">{auth.authModalMode}</div>
      <button onClick={() => auth.login('test@example.com', 'password123')}>Login</button>
      <button onClick={() => auth.register({ email: 'new@example.com', username: 'newuser', password: 'password123' })}>Register</button>
      <button onClick={() => auth.googleLogin('valid-google-code')}>Google Login</button>
      <button onClick={() => auth.logout()}>Logout</button>
      <button onClick={() => auth.openAuthModal('login')}>Open Login Modal</button>
      <button onClick={() => auth.openAuthModal('signup')}>Open Signup Modal</button>
      <button onClick={() => auth.closeAuthModal()}>Close Modal</button>
      <button onClick={() => auth.checkAuth()}>Check Auth</button>
    </div>
  );
};

// Wrapper component for testing
const renderWithAuth = (ui: React.ReactElement = <TestComponent />) => {
  return render(
    <AuthProvider>
      {ui}
    </AuthProvider>
  );
};

describe('AuthContext', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Default: user is authenticated
    mockAuthAPI.getStatus.mockResolvedValue(
      mockAxiosResponse({ authenticated: true, user: mockUser })
    );
    mockFetchCSRFToken.mockResolvedValue('test-csrf-token');
  });

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

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

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

  describe('Initial State', () => {
    it('starts with loading state', () => {
      renderWithAuth();
      expect(screen.getByTestId('loading')).toHaveTextContent('loading');
    });

    it('checks authentication status on mount', async () => {
      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
      expect(mockAuthAPI.getStatus).toHaveBeenCalled();
    });

    it('sets user to null when not authenticated', async () => {
      mockAuthAPI.getStatus.mockResolvedValue(
        mockAxiosResponse({ authenticated: false, user: null })
      );

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      expect(screen.getByTestId('user')).toHaveTextContent('no-user');
    });

    it('handles network errors gracefully', async () => {
      mockAuthAPI.getStatus.mockRejectedValue(new Error('Network error'));

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      expect(screen.getByTestId('user')).toHaveTextContent('no-user');
    });
  });

  describe('Login', () => {
    it('logs in successfully with valid credentials', async () => {
      mockAuthAPI.getStatus.mockResolvedValue(
        mockAxiosResponse({ authenticated: false, user: null })
      );

      mockAuthAPI.login.mockResolvedValue(
        mockAxiosResponse({ user: mockUser })
      );

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      expect(screen.getByTestId('user')).toHaveTextContent('no-user');

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

      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
      });

      expect(mockAuthAPI.login).toHaveBeenCalledWith({
        email: 'test@example.com',
        password: 'password123',
      });
    });

    it('keeps user as null when login fails', async () => {
      mockAuthAPI.getStatus.mockResolvedValue(
        mockAxiosResponse({ authenticated: false, user: null })
      );

      mockAuthAPI.login.mockRejectedValue(new Error('Invalid credentials'));

      // Create a test component that captures login errors
      const LoginErrorTestComponent: React.FC = () => {
        const auth = useAuth();
        const [error, setError] = React.useState<string | null>(null);

        const handleLogin = async () => {
          try {
            await auth.login('test@example.com', 'wrongpassword');
          } catch (err: any) {
            setError(err.message);
          }
        };

        return (
          <div>
            <div data-testid="loading">{auth.loading ? 'loading' : 'not-loading'}</div>
            <div data-testid="user">{auth.user ? auth.user.email : 'no-user'}</div>
            <div data-testid="error">{error || 'no-error'}</div>
            <button onClick={handleLogin}>Login</button>
          </div>
        );
      };

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

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

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

      await waitFor(() => {
        expect(screen.getByTestId('error')).toHaveTextContent('Invalid credentials');
      });

      // User should still be null after failed login
      expect(screen.getByTestId('user')).toHaveTextContent('no-user');
    });
  });

  describe('Registration', () => {
    it('registers a new user successfully', async () => {
      mockAuthAPI.getStatus.mockResolvedValue(
        mockAxiosResponse({ authenticated: false, user: null })
      );

      mockAuthAPI.register.mockResolvedValue(
        mockAxiosResponse({ user: { ...mockUser, email: 'new@example.com', isVerified: false } })
      );

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

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

      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('new@example.com');
      });
    });
  });

  describe('Google Login', () => {
    it('logs in with Google successfully', async () => {
      mockAuthAPI.getStatus.mockResolvedValue(
        mockAxiosResponse({ authenticated: false, user: null })
      );

      mockAuthAPI.googleLogin.mockResolvedValue(
        mockAxiosResponse({ user: mockUser })
      );

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

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

      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
      });

      expect(mockAuthAPI.googleLogin).toHaveBeenCalledWith('valid-google-code');
    });
  });

  describe('Logout', () => {
    it('logs out successfully', async () => {
      mockAuthAPI.logout.mockResolvedValue(mockAxiosResponse({ message: 'Logged out' }));

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
      });

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

      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('no-user');
      });

      expect(mockResetCSRFToken).toHaveBeenCalled();
    });

    it('clears user state even if logout API fails', async () => {
      mockAuthAPI.logout.mockRejectedValue(new Error('Server error'));

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
      });

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

      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('no-user');
      });
    });
  });

  describe('Auth Modal', () => {
    it('opens login modal', async () => {
      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      expect(screen.getByTestId('modal-open')).toHaveTextContent('closed');

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

      expect(screen.getByTestId('modal-open')).toHaveTextContent('open');
      expect(screen.getByTestId('modal-mode')).toHaveTextContent('login');
    });

    it('opens signup modal', async () => {
      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      await act(async () => {
        fireEvent.click(screen.getByText('Open Signup Modal'));
      });

      expect(screen.getByTestId('modal-open')).toHaveTextContent('open');
      expect(screen.getByTestId('modal-mode')).toHaveTextContent('signup');
    });

    it('closes modal', async () => {
      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

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

      expect(screen.getByTestId('modal-open')).toHaveTextContent('open');

      await act(async () => {
        fireEvent.click(screen.getByText('Close Modal'));
      });

      expect(screen.getByTestId('modal-open')).toHaveTextContent('closed');
    });
  });

  describe('updateUser', () => {
    it('updates user state', async () => {
      const TestComponentWithUpdate: React.FC = () => {
        const auth = useAuth();

        return (
          <div>
            <div data-testid="loading">{auth.loading ? 'loading' : 'not-loading'}</div>
            <div data-testid="user-email">{auth.user?.email || 'no-user'}</div>
            <div data-testid="user-name">{auth.user?.firstName || 'no-name'}</div>
            <button onClick={() => auth.updateUser({
              ...auth.user!,
              firstName: 'Updated',
              lastName: 'Name',
            })}>
              Update User
            </button>
          </div>
        );
      };

      renderWithAuth(<TestComponentWithUpdate />);

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      expect(screen.getByTestId('user-name')).toHaveTextContent('Test');

      await act(async () => {
        fireEvent.click(screen.getByText('Update User'));
      });

      expect(screen.getByTestId('user-name')).toHaveTextContent('Updated');
    });
  });

  describe('checkAuth', () => {
    it('refreshes authentication status', async () => {
      let callCount = 0;

      mockAuthAPI.getStatus.mockImplementation(() => {
        callCount++;
        if (callCount === 1) {
          return Promise.resolve(mockAxiosResponse({ authenticated: false, user: null }));
        }
        return Promise.resolve(mockAxiosResponse({ authenticated: true, user: mockUser }));
      });

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      expect(screen.getByTestId('user')).toHaveTextContent('no-user');

      await act(async () => {
        fireEvent.click(screen.getByText('Check Auth'));
      });

      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
      });
    });
  });

  describe('OAuth Callback Handling', () => {
    it('skips auth check on OAuth callback page', async () => {
      // Mock being on the OAuth callback page
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          pathname: '/auth/google/callback',
        },
        writable: true,
      });

      mockAuthAPI.getStatus.mockClear();

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      // Status should not be called on OAuth callback page
      expect(mockAuthAPI.getStatus).not.toHaveBeenCalled();

      // Reset location
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          pathname: '/',
        },
        writable: true,
      });
    });
  });

  describe('CSRF Token Management', () => {
    it('fetches CSRF token on initial load', async () => {
      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      expect(mockFetchCSRFToken).toHaveBeenCalled();
    });

    it('handles CSRF token fetch failure gracefully', async () => {
      mockFetchCSRFToken.mockRejectedValue(new Error('CSRF fetch failed'));

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      // When CSRF fetch fails, checkAuth is not called, so user remains null
      // But the app should handle this gracefully (not crash)
      expect(screen.getByTestId('user')).toHaveTextContent('no-user');
    });

    it('refreshes CSRF token after login', async () => {
      mockAuthAPI.getStatus.mockResolvedValue(
        mockAxiosResponse({ authenticated: false, user: null })
      );

      mockAuthAPI.login.mockResolvedValue(
        mockAxiosResponse({ user: mockUser })
      );

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      mockFetchCSRFToken.mockClear();

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

      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
      });

      // CSRF token should be refreshed after login
      expect(mockFetchCSRFToken).toHaveBeenCalled();
    });

    it('refreshes CSRF token after registration', async () => {
      mockAuthAPI.getStatus.mockResolvedValue(
        mockAxiosResponse({ authenticated: false, user: null })
      );

      mockAuthAPI.register.mockResolvedValue(
        mockAxiosResponse({ user: { ...mockUser, email: 'new@example.com', isVerified: false } })
      );

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      mockFetchCSRFToken.mockClear();

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

      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('new@example.com');
      });

      expect(mockFetchCSRFToken).toHaveBeenCalled();
    });

    it('refreshes CSRF token after Google login', async () => {
      mockAuthAPI.getStatus.mockResolvedValue(
        mockAxiosResponse({ authenticated: false, user: null })
      );

      mockAuthAPI.googleLogin.mockResolvedValue(
        mockAxiosResponse({ user: mockUser })
      );

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      mockFetchCSRFToken.mockClear();

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

      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
      });

      expect(mockFetchCSRFToken).toHaveBeenCalled();
    });
  });

  describe('Auth Modal Mode State Transitions', () => {
    it('transitions from login to signup mode', async () => {
      const ModalTestComponent: React.FC = () => {
        const auth = useAuth();
        return (
          <div>
            <div data-testid="loading">{auth.loading ? 'loading' : 'not-loading'}</div>
            <div data-testid="modal-mode">{auth.authModalMode}</div>
            <button onClick={() => auth.openAuthModal('login')}>Open Login</button>
            <button onClick={() => auth.openAuthModal('signup')}>Open Signup</button>
            <button onClick={() => auth.openAuthModal('forgot-password')}>Open Forgot</button>
          </div>
        );
      };

      renderWithAuth(<ModalTestComponent />);

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      await act(async () => {
        fireEvent.click(screen.getByText('Open Login'));
      });
      expect(screen.getByTestId('modal-mode')).toHaveTextContent('login');

      await act(async () => {
        fireEvent.click(screen.getByText('Open Signup'));
      });
      expect(screen.getByTestId('modal-mode')).toHaveTextContent('signup');

      await act(async () => {
        fireEvent.click(screen.getByText('Open Forgot'));
      });
      expect(screen.getByTestId('modal-mode')).toHaveTextContent('forgot-password');
    });
  });

  describe('Multiple Concurrent Auth Calls', () => {
    it('handles multiple login attempts gracefully', async () => {
      mockAuthAPI.getStatus.mockResolvedValue(
        mockAxiosResponse({ authenticated: false, user: null })
      );

      let loginResolve: (value: any) => void;
      mockAuthAPI.login.mockImplementation(() =>
        new Promise((resolve) => {
          loginResolve = resolve;
        })
      );

      const ConcurrentTestComponent: React.FC = () => {
        const auth = useAuth();
        return (
          <div>
            <div data-testid="loading">{auth.loading ? 'loading' : 'not-loading'}</div>
            <div data-testid="user">{auth.user ? auth.user.email : 'no-user'}</div>
            <button onClick={() => auth.login('test@example.com', 'pass')}>Login 1</button>
            <button onClick={() => auth.login('test@example.com', 'pass')}>Login 2</button>
          </div>
        );
      };

      renderWithAuth(<ConcurrentTestComponent />);

      await waitFor(() => {
        expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
      });

      // Trigger two login calls quickly
      await act(async () => {
        fireEvent.click(screen.getByText('Login 1'));
        fireEvent.click(screen.getByText('Login 2'));
      });

      // Resolve both
      await act(async () => {
        loginResolve!(mockAxiosResponse({ user: mockUser }));
      });

      await waitFor(() => {
        expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
      });
    });
  });

  describe('User Verification Status', () => {
    it('correctly tracks verified user status', async () => {
      mockAuthAPI.getStatus.mockResolvedValue(
        mockAxiosResponse({ authenticated: true, user: { ...mockUser, isVerified: true } })
      );

      renderWithAuth();

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

    it('correctly tracks unverified user status', async () => {
      mockAuthAPI.getStatus.mockResolvedValue(
        mockAxiosResponse({ authenticated: true, user: { ...mockUser, isVerified: false } })
      );

      renderWithAuth();

      await waitFor(() => {
        expect(screen.getByTestId('verified')).toHaveTextContent('not-verified');
      });
    });
  });
});
