import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ErrorBoundary, ErrorDisplay } from "@/components/ErrorBoundary";
import type { McError, McErrorCode } from "@/lib/types";

/** Helper to create a McError with defaults */
function makeMcError(overrides: Partial<McError> = {}): McError {
  return {
    code: "INTERNAL_ERROR" as McErrorCode,
    message: "Something went wrong",
    recoverable: true,
    ...overrides,
  };
}

/** Component that throws on render */
function ThrowingComponent({ error }: { error?: Error }): React.ReactElement {
  throw error ?? new Error("Test error");
}

describe("ErrorBoundary", () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    // Suppress console.error during tests (React logs caught errors)
    consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it("renders children when there is no error", () => {
    render(
      <ErrorBoundary>
        <div>Child content</div>
      </ErrorBoundary>
    );

    expect(screen.getByText("Child content")).toBeInTheDocument();
  });

  it("catches errors and shows fallback UI", () => {
    render(
      <ErrorBoundary>
        <ThrowingComponent />
      </ErrorBoundary>
    );

    expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
    expect(
      screen.getByRole("button", { name: /reload/i })
    ).toBeInTheDocument();
  });

  it("logs error details to console", () => {
    render(
      <ErrorBoundary>
        <ThrowingComponent error={new Error("Custom test error")} />
      </ErrorBoundary>
    );

    // React's error boundary logs to console.error
    expect(consoleSpy).toHaveBeenCalled();
  });

  it("shows error stack in development mode", () => {
    // We're in test mode which acts like development
    const testError = new Error("Stack trace test");
    testError.stack = "Error: Stack trace test\n    at TestComponent";

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

    // In dev mode, should show stack trace
    expect(screen.getByText(/Stack trace test/)).toBeInTheDocument();
  });

  it("provides Try Again button that calls onRecover", async () => {
    const onRecover = vi.fn();
    const user = userEvent.setup();

    const { rerender } = render(
      <ErrorBoundary onRecover={onRecover}>
        <ThrowingComponent />
      </ErrorBoundary>
    );

    // Should show error state
    expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();

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

    expect(onRecover).toHaveBeenCalledOnce();
  });

  it("resets error state when Try Again is clicked", async () => {
    const user = userEvent.setup();
    let shouldThrow = true;

    function MaybeThrow(): React.ReactElement {
      if (shouldThrow) {
        throw new Error("Conditional error");
      }
      return <div>Recovered content</div>;
    }

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

    // Should show error state
    expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();

    // Stop throwing
    shouldThrow = false;

    // Click Try Again to recover
    await user.click(screen.getByRole("button", { name: /try again/i }));

    // Should show recovered content
    expect(screen.getByText("Recovered content")).toBeInTheDocument();
  });
});

describe("ErrorDisplay", () => {
  it("shows CLI not found error with setup guide", () => {
    const error = makeMcError({
      code: "LORE_UNAVAILABLE",
      message: "lore command not found",
    });

    render(<ErrorDisplay error={error} />);

    expect(screen.getByText(/lore not found/i)).toBeInTheDocument();
    expect(screen.getByText(/install with/i)).toBeInTheDocument();
  });

  it("shows beads CLI not found error with setup guide", () => {
    const error = makeMcError({
      code: "BEADS_UNAVAILABLE",
      message: "br command not found",
    });

    render(<ErrorDisplay error={error} />);

    expect(screen.getByText(/beads.*not found/i)).toBeInTheDocument();
    expect(screen.getByText(/install with/i)).toBeInTheDocument();
  });

  it("shows bv CLI not found error with setup guide", () => {
    const error = makeMcError({
      code: "BV_UNAVAILABLE",
      message: "bv command not found",
    });

    render(<ErrorDisplay error={error} />);

    expect(screen.getByText(/bv.*not found/i)).toBeInTheDocument();
    expect(screen.getByText(/install with/i)).toBeInTheDocument();
  });

  it("shows retry button for recoverable sync errors", async () => {
    const onRetry = vi.fn();
    const user = userEvent.setup();

    const error = makeMcError({
      code: "LORE_FETCH_FAILED",
      message: "Failed to fetch from GitLab",
      recoverable: true,
    });

    render(<ErrorDisplay error={error} onRetry={onRetry} />);

    const retryButton = screen.getByRole("button", { name: /retry/i });
    expect(retryButton).toBeInTheDocument();

    await user.click(retryButton);
    expect(onRetry).toHaveBeenCalledOnce();
  });

  it("shows no retry button for non-recoverable errors", () => {
    const onRetry = vi.fn();

    const error = makeMcError({
      code: "BRIDGE_MAP_CORRUPTED",
      message: "State file corrupted",
      recoverable: false,
    });

    render(<ErrorDisplay error={error} onRetry={onRetry} />);

    expect(
      screen.queryByRole("button", { name: /retry/i })
    ).not.toBeInTheDocument();
  });

  it("shows network error with offline mode hint", () => {
    const error = makeMcError({
      code: "LORE_FETCH_FAILED",
      message: "Network request failed",
      recoverable: true,
    });

    render(<ErrorDisplay error={error} />);

    // Should indicate connection issue
    expect(screen.getByText(/sync failed/i)).toBeInTheDocument();
  });

  it("shows dismiss button when onDismiss is provided", async () => {
    const onDismiss = vi.fn();
    const user = userEvent.setup();

    const error = makeMcError({
      code: "INTERNAL_ERROR",
      message: "Something unexpected happened",
    });

    render(<ErrorDisplay error={error} onDismiss={onDismiss} />);

    const dismissButton = screen.getByRole("button", { name: /dismiss/i });
    await user.click(dismissButton);

    expect(onDismiss).toHaveBeenCalledOnce();
  });

  it("shows bridge locked error with wait message", () => {
    const error = makeMcError({
      code: "BRIDGE_LOCKED",
      message: "Another operation in progress",
      recoverable: true,
    });

    render(<ErrorDisplay error={error} />);

    expect(screen.getByText(/operation in progress/i)).toBeInTheDocument();
  });

  it("shows generic error for unknown error codes", () => {
    const error = makeMcError({
      code: "INTERNAL_ERROR",
      message: "Unexpected error occurred",
    });

    render(<ErrorDisplay error={error} />);

    // Check both title and description are present
    expect(screen.getByRole("heading", { level: 3 })).toHaveTextContent(
      /unexpected error/i
    );
    expect(screen.getByText(/an unexpected error occurred/i)).toBeInTheDocument();
  });

  it("applies destructive variant styling", () => {
    const error = makeMcError({
      code: "BRIDGE_MAP_CORRUPTED",
      message: "Critical error",
      recoverable: false,
    });

    render(<ErrorDisplay error={error} />);

    // Check that error styling is applied (has error-related classes)
    const errorContainer = screen.getByRole("alert");
    expect(errorContainer).toBeInTheDocument();
  });
});
