/**
 * ErrorBoundary Component Tests
 *
 * Tests for the error boundary component that catches JavaScript errors
 * in child components and displays fallback UI.
 */

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ErrorBoundary } from '@/components/common/ErrorBoundary';

// Component that throws an error for testing
function ThrowingComponent({ shouldThrow = true }: { shouldThrow?: boolean }) {
  if (shouldThrow) {
    throw new Error('Test error message');
  }
  return <div>Child content</div>;
}

describe('ErrorBoundary', () => {
  // Suppress console.error during tests since we're testing error handling
  const originalError = console.error;

  beforeAll(() => {
    console.error = jest.fn();
  });

  afterAll(() => {
    console.error = originalError;
  });

  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('when no error occurs', () => {
    it('renders children normally', () => {
      render(
        <ErrorBoundary>
          <div>Test content</div>
        </ErrorBoundary>
      );

      expect(screen.getByText('Test content')).toBeInTheDocument();
    });

    it('does not show error UI', () => {
      render(
        <ErrorBoundary>
          <div>Test content</div>
        </ErrorBoundary>
      );

      expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
    });

    it('renders multiple children', () => {
      render(
        <ErrorBoundary>
          <div>First child</div>
          <div>Second child</div>
        </ErrorBoundary>
      );

      expect(screen.getByText('First child')).toBeInTheDocument();
      expect(screen.getByText('Second child')).toBeInTheDocument();
    });
  });

  describe('when an error occurs', () => {
    it('catches the error and shows default fallback UI', () => {
      render(
        <ErrorBoundary>
          <ThrowingComponent />
        </ErrorBoundary>
      );

      expect(screen.getByText('Something went wrong')).toBeInTheDocument();
      expect(screen.getByText(/An unexpected error occurred/)).toBeInTheDocument();
    });

    it('displays the error message', () => {
      render(
        <ErrorBoundary>
          <ThrowingComponent />
        </ErrorBoundary>
      );

      expect(screen.getByText('Test error message')).toBeInTheDocument();
    });

    it('shows the reset button by default', () => {
      render(
        <ErrorBoundary>
          <ThrowingComponent />
        </ErrorBoundary>
      );

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

    it('logs error to console', () => {
      render(
        <ErrorBoundary>
          <ThrowingComponent />
        </ErrorBoundary>
      );

      expect(console.error).toHaveBeenCalledWith(
        'ErrorBoundary caught an error:',
        expect.any(Error),
        expect.objectContaining({ componentStack: expect.any(String) })
      );
    });

    it('hides the throwing component', () => {
      render(
        <ErrorBoundary>
          <ThrowingComponent />
        </ErrorBoundary>
      );

      expect(screen.queryByText('Child content')).not.toBeInTheDocument();
    });
  });

  describe('custom fallback', () => {
    it('renders custom fallback when provided', () => {
      render(
        <ErrorBoundary fallback={<div>Custom error UI</div>}>
          <ThrowingComponent />
        </ErrorBoundary>
      );

      expect(screen.getByText('Custom error UI')).toBeInTheDocument();
      expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
    });

    it('does not show default fallback when custom fallback is provided', () => {
      render(
        <ErrorBoundary fallback={<span>Fallback</span>}>
          <ThrowingComponent />
        </ErrorBoundary>
      );

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

  describe('onError callback', () => {
    it('calls onError when an error occurs', () => {
      const onError = jest.fn();

      render(
        <ErrorBoundary onError={onError}>
          <ThrowingComponent />
        </ErrorBoundary>
      );

      expect(onError).toHaveBeenCalledTimes(1);
      expect(onError).toHaveBeenCalledWith(
        expect.objectContaining({ message: 'Test error message' }),
        expect.objectContaining({ componentStack: expect.any(String) })
      );
    });

    it('does not call onError when no error occurs', () => {
      const onError = jest.fn();

      render(
        <ErrorBoundary onError={onError}>
          <div>Normal content</div>
        </ErrorBoundary>
      );

      expect(onError).not.toHaveBeenCalled();
    });
  });

  describe('reset functionality', () => {
    it('resets error state when reset button is clicked', async () => {
      const user = userEvent.setup();

      // Track whether we should throw
      let shouldThrow = true;

      function ResettableThrowingComponent() {
        if (shouldThrow) {
          throw new Error('Resettable error');
        }
        return <div>Recovered content</div>;
      }

      render(
        <ErrorBoundary>
          <ResettableThrowingComponent />
        </ErrorBoundary>
      );

      // Should show error UI after first render throws
      expect(screen.getByText('Something went wrong')).toBeInTheDocument();
      expect(screen.getByText('Resettable error')).toBeInTheDocument();

      // Stop throwing before clicking reset
      shouldThrow = false;

      // Click reset
      await user.click(screen.getByRole('button', { name: /try again/i }));

      // After reset, the component re-renders without throwing
      expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
      expect(screen.getByText('Recovered content')).toBeInTheDocument();
    });
  });

  describe('showReset prop', () => {
    it('hides reset button when showReset is false', () => {
      render(
        <ErrorBoundary showReset={false}>
          <ThrowingComponent />
        </ErrorBoundary>
      );

      expect(screen.getByText('Something went wrong')).toBeInTheDocument();
      expect(screen.queryByRole('button', { name: /try again/i })).not.toBeInTheDocument();
    });

    it('shows reset button when showReset is true', () => {
      render(
        <ErrorBoundary showReset={true}>
          <ThrowingComponent />
        </ErrorBoundary>
      );

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

    it('shows reset button by default (showReset undefined)', () => {
      render(
        <ErrorBoundary>
          <ThrowingComponent />
        </ErrorBoundary>
      );

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

  describe('error without message', () => {
    it('handles error with null message gracefully', () => {
      function ThrowNullError() {
        const error = new Error();
        error.message = '';
        throw error;
      }

      render(
        <ErrorBoundary>
          <ThrowNullError />
        </ErrorBoundary>
      );

      expect(screen.getByText('Something went wrong')).toBeInTheDocument();
      // Empty message should still render the error container but be empty
    });
  });

  describe('accessibility', () => {
    it('has descriptive error title', () => {
      render(
        <ErrorBoundary>
          <ThrowingComponent />
        </ErrorBoundary>
      );

      expect(screen.getByText('Something went wrong')).toBeInTheDocument();
    });

    it('has descriptive help text', () => {
      render(
        <ErrorBoundary>
          <ThrowingComponent />
        </ErrorBoundary>
      );

      expect(
        screen.getByText(/An unexpected error occurred. Please try again or contact support/)
      ).toBeInTheDocument();
    });

    it('icons have aria-hidden', () => {
      render(
        <ErrorBoundary>
          <ThrowingComponent />
        </ErrorBoundary>
      );

      const icons = document.querySelectorAll('[aria-hidden="true"]');
      expect(icons.length).toBeGreaterThan(0);
    });

    it('reset button is keyboard accessible', async () => {
      const user = userEvent.setup();

      render(
        <ErrorBoundary>
          <ThrowingComponent shouldThrow={false} />
        </ErrorBoundary>
      );

      // First render without error to set up the component
      expect(screen.getByText('Child content')).toBeInTheDocument();
    });
  });

  describe('DefaultFallback rendering', () => {
    it('shows error in monospace font', () => {
      render(
        <ErrorBoundary>
          <ThrowingComponent />
        </ErrorBoundary>
      );

      const errorMessage = screen.getByText('Test error message');
      expect(errorMessage).toHaveClass('font-mono');
    });

    it('renders within a Card component', () => {
      render(
        <ErrorBoundary>
          <ThrowingComponent />
        </ErrorBoundary>
      );

      // The card should have destructive styling
      const card = document.querySelector('.border-destructive\\/50');
      expect(card).toBeInTheDocument();
    });
  });

  describe('edge cases', () => {
    it('handles deeply nested errors', () => {
      function DeepChild() {
        throw new Error('Deep error');
      }

      function MiddleComponent() {
        return (
          <div>
            <DeepChild />
          </div>
        );
      }

      render(
        <ErrorBoundary>
          <div>
            <MiddleComponent />
          </div>
        </ErrorBoundary>
      );

      expect(screen.getByText('Something went wrong')).toBeInTheDocument();
      expect(screen.getByText('Deep error')).toBeInTheDocument();
    });

    it('isolates errors to its own boundary', () => {
      render(
        <div>
          <ErrorBoundary>
            <ThrowingComponent />
          </ErrorBoundary>
          <div>Sibling content</div>
        </div>
      );

      expect(screen.getByText('Something went wrong')).toBeInTheDocument();
      expect(screen.getByText('Sibling content')).toBeInTheDocument();
    });

    it('allows nested error boundaries', () => {
      function InnerThrowing() {
        throw new Error('Inner error');
      }

      render(
        <ErrorBoundary fallback={<div>Outer fallback</div>}>
          <div>
            <ErrorBoundary fallback={<div>Inner fallback</div>}>
              <InnerThrowing />
            </ErrorBoundary>
          </div>
        </ErrorBoundary>
      );

      // Inner boundary should catch the error
      expect(screen.getByText('Inner fallback')).toBeInTheDocument();
      expect(screen.queryByText('Outer fallback')).not.toBeInTheDocument();
    });
  });
});
