// @vitest-environment jsdom
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { MapErrorBoundary } from './MapErrorBoundary';

const captureException = vi.fn();
vi.mock('@sentry/react', () => ({
  captureException: (...args: unknown[]) => captureException(...args),
}));

// Controls whether the child throws on its next render. The boundary remounts
// the child on recovery, so flipping this lets us simulate a transient crash.
let shouldThrow = true;
function MaybeBoom() {
  if (shouldThrow) {
    throw new Error('webgl boom');
  }
  return <div>map content</div>;
}

let consoleErrorSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
  shouldThrow = true;
  captureException.mockClear();
  // React logs caught render errors to console.error; keep test output clean.
  consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
  vi.useFakeTimers();
});

afterEach(() => {
  vi.runOnlyPendingTimers();
  vi.useRealTimers();
  consoleErrorSpy.mockRestore();
  cleanup();
});

describe('MapErrorBoundary', () => {
  it('renders children when they do not throw', () => {
    shouldThrow = false;
    render(
      <MapErrorBoundary>
        <MaybeBoom />
      </MapErrorBoundary>
    );
    expect(screen.getByText('map content')).toBeTruthy();
    expect(captureException).not.toHaveBeenCalled();
  });

  it('reports the error and auto-recovers by remounting the map', () => {
    render(
      <MapErrorBoundary>
        <MaybeBoom />
      </MapErrorBoundary>
    );

    // The throw is caught: children are not shown, and the error is reported.
    expect(screen.queryByText('map content')).toBeNull();
    expect(captureException).toHaveBeenCalledTimes(1);

    // The next mount succeeds; advancing past the recovery delay remounts it.
    shouldThrow = false;
    act(() => {
      vi.advanceTimersByTime(1000);
    });

    expect(screen.getByText('map content')).toBeTruthy();
  });

  it('falls back to a manual retry after repeated crashes, then recovers on retry', () => {
    render(
      <MapErrorBoundary>
        <MaybeBoom />
      </MapErrorBoundary>
    );

    // Keep crashing through every auto-recovery attempt.
    act(() => {
      vi.advanceTimersByTime(1000);
    });
    act(() => {
      vi.advanceTimersByTime(1000);
    });

    const retryButton = screen.getByRole('button', { name: /reload the map/i });
    expect(retryButton).toBeTruthy();
    expect(captureException.mock.calls.length).toBeGreaterThanOrEqual(3);

    // Manual retry remounts the (now healthy) map.
    shouldThrow = false;
    fireEvent.click(retryButton);
    expect(screen.getByText('map content')).toBeTruthy();
  });
});
