// src/pages/ResetPasswordPage.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { ResetPasswordPage } from './ResetPasswordPage';
import * as apiClient from '../services/apiClient';
import { logger } from '../services/logger.client';

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

const mockedApiClient = vi.mocked(apiClient);

// The logger is mocked globally.
// Helper function to render the component within a router context
const renderWithRouter = (token: string) => {
  return render(
    <MemoryRouter initialEntries={[`/reset-password/${token}`]}>
      <Routes>
        <Route path="/reset-password/:token" element={<ResetPasswordPage />} />
        <Route path="/" element={<div>Home Page</div>} />
      </Routes>
    </MemoryRouter>,
  );
};

describe('ResetPasswordPage', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

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

  it('should render the form with password fields and a submit button', () => {
    renderWithRouter('test-token-123');
    expect(screen.getByRole('heading', { name: /set a new password/i })).toBeInTheDocument();
    expect(screen.getByPlaceholderText('New Password')).toBeInTheDocument();
    expect(screen.getByPlaceholderText('Confirm New Password')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /reset password/i })).toBeInTheDocument();
  });

  it('should call resetPassword and show success message on valid submission', async () => {
    const setTimeoutSpy = vi.spyOn(global, 'setTimeout');
    mockedApiClient.resetPassword.mockResolvedValue(
      new Response(JSON.stringify({ message: 'Password reset was successful!' })),
    );
    const token = 'valid-token';
    renderWithRouter(token);

    fireEvent.change(screen.getByPlaceholderText('New Password'), {
      target: { value: 'newSecurePassword123' },
    });
    fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
      target: { value: 'newSecurePassword123' },
    });
    fireEvent.click(screen.getByRole('button', { name: /reset password/i }));

    await waitFor(() => {
      expect(mockedApiClient.resetPassword).toHaveBeenCalledWith(token, 'newSecurePassword123');
      expect(screen.getByText(/password reset was successful!/i)).toBeInTheDocument();
      expect(screen.getByText(/return to home/i)).toBeInTheDocument();
    });
    expect(logger.info).toHaveBeenCalledWith('Password has been successfully reset.');

    // Check that form is cleared
    expect(screen.queryByPlaceholderText('New Password')).not.toBeInTheDocument();

    // Verify redirect timeout was set
    expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 4000);
    setTimeoutSpy.mockRestore();
  });

  it('should show an error message if passwords do not match', async () => {
    renderWithRouter('test-token');

    fireEvent.change(screen.getByPlaceholderText('New Password'), {
      target: { value: 'passwordA' },
    });
    fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
      target: { value: 'passwordB' },
    });
    fireEvent.click(screen.getByRole('button', { name: /reset password/i }));

    await waitFor(() => {
      expect(screen.getByText('Passwords do not match.')).toBeInTheDocument();
    });
    expect(mockedApiClient.resetPassword).not.toHaveBeenCalled();
  });

  it('should show an error message if the API call fails', async () => {
    mockedApiClient.resetPassword.mockRejectedValueOnce(new Error('Invalid or expired token.'));
    renderWithRouter('invalid-token');

    fireEvent.change(screen.getByPlaceholderText('New Password'), {
      target: { value: 'newSecurePassword123' },
    });
    fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
      target: { value: 'newSecurePassword123' },
    });
    fireEvent.click(screen.getByRole('button', { name: /reset password/i }));

    await waitFor(() => {
      expect(screen.getByText('Invalid or expired token.')).toBeInTheDocument();
    });
    expect(logger.error).toHaveBeenCalledWith(
      { err: expect.any(Error) },
      'Failed to reset password.',
    );
  });

  it('should show an error message if API returns a non-JSON error response', async () => {
    // Simulate a server error returning HTML instead of JSON
    mockedApiClient.resetPassword.mockResolvedValue(
      new Response('<h1>Server Error</h1>', {
        status: 500,
        headers: { 'Content-Type': 'text/html' },
      }),
    );
    renderWithRouter('test-token');

    fireEvent.change(screen.getByPlaceholderText('New Password'), {
      target: { value: 'newSecurePassword123' },
    });
    fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
      target: { value: 'newSecurePassword123' },
    });
    fireEvent.click(screen.getByRole('button', { name: /reset password/i }));

    await waitFor(() => {
      // The error from response.json() is implementation-dependent.
      // We check for a substring that is likely to be present.
      expect(screen.getByText(/not valid JSON/i)).toBeInTheDocument();
    });

    expect(logger.error).toHaveBeenCalledWith(
      { err: expect.any(SyntaxError) },
      'Failed to reset password.',
    );
  });

  it('should show a loading spinner while submitting', async () => {
    let resolvePromise: (value: Response) => void;
    const mockPromise = new Promise<Response>((resolve) => {
      resolvePromise = resolve;
    });
    mockedApiClient.resetPassword.mockReturnValue(mockPromise);
    renderWithRouter('test-token');

    fireEvent.change(screen.getByPlaceholderText('New Password'), {
      target: { value: 'newSecurePassword123' },
    });
    fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
      target: { value: 'newSecurePassword123' },
    });
    fireEvent.click(screen.getByRole('button', { name: /reset password/i }));

    // Expect button to be disabled and text to be gone (replaced by spinner)
    // We use the accessible name which persists via aria-label even when text content is replaced
    expect(screen.getByRole('button', { name: /reset password/i })).toBeDisabled();
    expect(screen.queryByText('Reset Password')).not.toBeInTheDocument();

    await act(async () => {
      resolvePromise!(new Response(JSON.stringify({ message: 'Password reset was successful!' })));
    });

    await waitFor(() => {
      expect(screen.getByText(/password reset was successful!/i)).toBeInTheDocument();
    });
  });

  it('should show an error if no token is provided', async () => {
    render(
      <MemoryRouter initialEntries={['/reset-password']}>
        <Routes>
          <Route path="/reset-password" element={<ResetPasswordPage />} />
        </Routes>
      </MemoryRouter>,
    );

    // Fill in required fields to trigger form submission
    fireEvent.change(screen.getByPlaceholderText('New Password'), {
      target: { value: 'password123' },
    });
    fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
      target: { value: 'password123' },
    });

    fireEvent.click(screen.getByRole('button', { name: /reset password/i }));

    await waitFor(() => {
      expect(
        screen.getByText('No reset token provided. Please use the link from your email.'),
      ).toBeInTheDocument();
    });
  });

  it('should handle unknown errors', async () => {
    mockedApiClient.resetPassword.mockRejectedValue('Unknown error');
    renderWithRouter('test-token');

    fireEvent.change(screen.getByPlaceholderText('New Password'), {
      target: { value: 'newSecurePassword123' },
    });
    fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
      target: { value: 'newSecurePassword123' },
    });
    fireEvent.click(screen.getByRole('button', { name: /reset password/i }));

    await waitFor(() => {
      expect(screen.getByText('An unknown error occurred.')).toBeInTheDocument();
    });
    expect(logger.error).toHaveBeenCalledWith(
      { err: 'Unknown error' },
      'Failed to reset password.',
    );
  });
});
