/**
 * CompletePayment Page Tests
 *
 * Tests for the payment completion page that handles
 * Stripe 3DS authentication and payment confirmation.
 */

import React from 'react';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { vi, type MockedFunction } from 'vitest';
import { MemoryRouter, Routes, Route } from 'react-router';
import { useAuth } from '../../contexts/AuthContext';
import { rentalAPI } from '../../services/api';
import { mockUser } from '../../mocks/handlers';

// Hoist mock definitions BEFORE module imports
const { mockLoadStripe, mockStripe } = vi.hoisted(() => {
  const mockStripe = {
    confirmCardPayment: vi.fn(),
  };
  return {
    mockStripe,
    mockLoadStripe: vi.fn().mockResolvedValue(mockStripe),
  };
});

vi.mock('@stripe/stripe-js', () => ({
  loadStripe: mockLoadStripe,
}));

// Mock dependencies
vi.mock('../../contexts/AuthContext');
vi.mock('../../services/api', () => ({
  rentalAPI: {
    getPaymentClientSecret: vi.fn(),
    completePayment: vi.fn(),
  },
}));

const mockNavigate = vi.fn();
vi.mock('react-router', async (importOriginal) => {
  const actual = await importOriginal<typeof import('react-router')>();
  return {
    ...actual,
    useNavigate: () => mockNavigate,
  };
});

// Import after mocks are set up
import CompletePayment from '../../pages/CompletePayment';
import { loadStripe } from '@stripe/stripe-js';

const mockedUseAuth = useAuth as MockedFunction<typeof useAuth>;
const mockedGetPaymentClientSecret = rentalAPI.getPaymentClientSecret as MockedFunction<typeof rentalAPI.getPaymentClientSecret>;
const mockedCompletePayment = rentalAPI.completePayment as MockedFunction<typeof rentalAPI.completePayment>;
const mockedLoadStripe = loadStripe as MockedFunction<typeof loadStripe>;

// Helper to render CompletePayment with route params
const renderCompletePayment = (rentalId: string = '123', authOverrides: Partial<ReturnType<typeof useAuth>> = {}) => {
  mockedUseAuth.mockReturnValue({
    user: mockUser,
    loading: false,
    showAuthModal: false,
    authModalMode: 'login',
    login: vi.fn(),
    register: vi.fn(),
    googleLogin: vi.fn(),
    logout: vi.fn(),
    checkAuth: vi.fn(),
    updateUser: vi.fn(),
    openAuthModal: vi.fn(),
    closeAuthModal: vi.fn(),
    ...authOverrides,
  });

  return render(
    <MemoryRouter initialEntries={[`/complete-payment/${rentalId}`]}>
      <Routes>
        <Route path="/complete-payment/:rentalId" element={<CompletePayment />} />
      </Routes>
    </MemoryRouter>
  );
};

describe('CompletePayment', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockLoadStripe.mockResolvedValue(mockStripe as any);
    mockedGetPaymentClientSecret.mockResolvedValue({
      data: { clientSecret: 'pi_test_secret', status: 'requires_action' },
    });
    mockedCompletePayment.mockResolvedValue({ data: { success: true } });
    mockStripe.confirmCardPayment.mockResolvedValue({
      paymentIntent: { status: 'succeeded' },
      error: null,
    });
  });

  describe('Loading State', () => {
    it('shows loading state initially', () => {
      mockedGetPaymentClientSecret.mockImplementation(() => new Promise(() => {}));
      renderCompletePayment();

      expect(screen.getByRole('status')).toBeInTheDocument();
      expect(screen.getByText('Loading payment details...')).toBeInTheDocument();
    });

    it('shows loading while auth is initializing', () => {
      renderCompletePayment('123', { loading: true });

      expect(screen.getByRole('status')).toBeInTheDocument();
      expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent('Loading...');
    });
  });

  describe('Authentication', () => {
    it('redirects unauthenticated users to login', async () => {
      renderCompletePayment('123', { user: null });

      await waitFor(() => {
        expect(mockNavigate).toHaveBeenCalledWith(
          expect.stringContaining('/?login=true&redirect='),
          { replace: true }
        );
      });
    });
  });

  describe('Invalid Rental ID', () => {
    it('handles missing rentalId parameter', async () => {
      render(
        <MemoryRouter initialEntries={['/complete-payment/']}>
          <Routes>
            <Route path="/complete-payment/" element={<CompletePayment />} />
            <Route path="/complete-payment/:rentalId" element={<CompletePayment />} />
          </Routes>
        </MemoryRouter>
      );

      // The component should handle invalid/missing ID gracefully
    });
  });

  describe('Successful Payment Flow', () => {
    it('fetches client secret from API', async () => {
      renderCompletePayment('456');

      await waitFor(() => {
        expect(mockedGetPaymentClientSecret).toHaveBeenCalledWith('456');
      });
    });

    it('calls Stripe confirmCardPayment', async () => {
      renderCompletePayment();

      await waitFor(() => {
        expect(mockStripe.confirmCardPayment).toHaveBeenCalledWith('pi_test_secret');
      });
    });

    it('shows success state on payment success', async () => {
      renderCompletePayment();

      await waitFor(() => {
        expect(screen.getByText('Payment Complete!')).toBeInTheDocument();
      });

      expect(screen.getByText(/confirmation email/i)).toBeInTheDocument();
      expect(screen.getByRole('link', { name: /view my rentals/i })).toBeInTheDocument();
    });

    it('calls completePayment API after Stripe confirmation', async () => {
      renderCompletePayment('789');

      await waitFor(() => {
        expect(mockedCompletePayment).toHaveBeenCalledWith('789');
      });
    });
  });

  describe('Already Succeeded Payment', () => {
    it('handles "already succeeded" payment status', async () => {
      mockedGetPaymentClientSecret.mockResolvedValue({
        data: { clientSecret: 'pi_test_secret', status: 'succeeded' },
      });

      renderCompletePayment();

      await waitFor(() => {
        expect(screen.getByText('Payment Complete!')).toBeInTheDocument();
      });

      // Should not call confirmCardPayment if already succeeded
      expect(mockStripe.confirmCardPayment).not.toHaveBeenCalled();
    });
  });

  describe('Error Handling', () => {
    it('shows error state with message on Stripe failure', async () => {
      mockStripe.confirmCardPayment.mockResolvedValue({
        paymentIntent: null,
        error: { message: 'Card was declined' },
      });

      renderCompletePayment();

      await waitFor(() => {
        expect(screen.getByText('Payment Failed')).toBeInTheDocument();
        expect(screen.getByText('Card was declined')).toBeInTheDocument();
      });
    });

    it('shows generic error when payment fails without message', async () => {
      mockStripe.confirmCardPayment.mockResolvedValue({
        paymentIntent: { status: 'failed' },
        error: null,
      });

      renderCompletePayment();

      await waitFor(() => {
        expect(screen.getByText('Payment Failed')).toBeInTheDocument();
        expect(screen.getByText(/could not be completed/i)).toBeInTheDocument();
      });
    });

    it('handles 400 "not awaiting payment" error', async () => {
      mockedGetPaymentClientSecret.mockRejectedValue({
        response: {
          status: 400,
          data: { message: 'Rental is not awaiting payment' },
        },
      });

      renderCompletePayment();

      await waitFor(() => {
        expect(mockNavigate).toHaveBeenCalledWith('/renting', { replace: true });
      });
    });

    it('handles Stripe loading failure', async () => {
      // Reset modules to get a fresh import with the null mock
      vi.resetModules();

      // Set up the mock to return null BEFORE re-importing
      vi.doMock('@stripe/stripe-js', () => ({
        loadStripe: vi.fn().mockResolvedValue(null),
      }));

      // Re-import the component with fresh module state
      const { default: FreshCompletePayment } = await import('../../pages/CompletePayment');

      render(
        <MemoryRouter initialEntries={['/complete-payment/123']}>
          <Routes>
            <Route path="/complete-payment/:rentalId" element={<FreshCompletePayment />} />
          </Routes>
        </MemoryRouter>
      );

      await waitFor(() => {
        expect(screen.getByText('Payment Failed')).toBeInTheDocument();
        expect(screen.getByText(/stripe failed to load/i)).toBeInTheDocument();
      });

      // Restore original mock
      vi.doMock('@stripe/stripe-js', () => ({
        loadStripe: mockLoadStripe,
      }));
    });

    it('handles API error when fetching client secret', async () => {
      mockedGetPaymentClientSecret.mockRejectedValue({
        response: {
          data: { error: 'Payment not found' },
        },
      });

      renderCompletePayment();

      await waitFor(() => {
        expect(screen.getByText('Payment Failed')).toBeInTheDocument();
        expect(screen.getByText('Payment not found')).toBeInTheDocument();
      });
    });
  });

  describe('Retry Functionality', () => {
    it('shows retry button on error', async () => {
      mockStripe.confirmCardPayment.mockResolvedValue({
        paymentIntent: null,
        error: { message: 'Card declined' },
      });

      renderCompletePayment();

      await waitFor(() => {
        expect(screen.getByText('Payment Failed')).toBeInTheDocument();
      });

      expect(screen.getByRole('button', { name: /try again/i })).toBeInTheDocument();
    });

    it('retry button resets and retries payment', async () => {
      mockStripe.confirmCardPayment
        .mockResolvedValueOnce({
          paymentIntent: null,
          error: { message: 'First attempt failed' },
        })
        .mockResolvedValueOnce({
          paymentIntent: { status: 'succeeded' },
          error: null,
        });

      renderCompletePayment();

      await waitFor(() => {
        expect(screen.getByText('Payment Failed')).toBeInTheDocument();
      });

      const retryButton = screen.getByRole('button', { name: /try again/i });
      fireEvent.click(retryButton);

      await waitFor(() => {
        expect(screen.getByText('Payment Complete!')).toBeInTheDocument();
      });

      expect(mockStripe.confirmCardPayment).toHaveBeenCalledTimes(2);
    });
  });

  describe('StrictMode Double-Execution Prevention', () => {
    it('prevents double processing with useRef', async () => {
      renderCompletePayment();

      await waitFor(() => {
        expect(screen.getByText('Payment Complete!')).toBeInTheDocument();
      });

      // Should only call confirmCardPayment once
      expect(mockStripe.confirmCardPayment).toHaveBeenCalledTimes(1);
    });
  });

  describe('UI States', () => {
    it('shows authenticating state during Stripe confirmation', async () => {
      mockStripe.confirmCardPayment.mockImplementation(
        () => new Promise(() => {})
      );

      renderCompletePayment();

      await waitFor(() => {
        expect(screen.getByText('Completing authentication...')).toBeInTheDocument();
      });

      expect(screen.getByText(/complete the authentication/i)).toBeInTheDocument();
    });

    it('has link to view rentals on success', async () => {
      renderCompletePayment();

      await waitFor(() => {
        expect(screen.getByText('Payment Complete!')).toBeInTheDocument();
      });

      const link = screen.getByRole('link', { name: /view my rentals/i });
      expect(link).toHaveAttribute('href', '/renting');
    });

    it('has link to view rentals on error', async () => {
      mockStripe.confirmCardPayment.mockResolvedValue({
        paymentIntent: null,
        error: { message: 'Failed' },
      });

      renderCompletePayment();

      await waitFor(() => {
        expect(screen.getByText('Payment Failed')).toBeInTheDocument();
      });

      const link = screen.getByRole('link', { name: /view my rentals/i });
      expect(link).toHaveAttribute('href', '/renting');
    });
  });
});
