/**
 * @file XTerminal.test.tsx
 * @description Unit tests for the XTerminal component
 */

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import type { ReactElement } from "react";

// ==========================================
// Mocks — set up before importing components
// ==========================================

// Mock xterm packages — they require a DOM canvas not available in jsdom
const mockTerminalDispose = vi.fn();
const mockTerminalWrite = vi.fn();
const mockTerminalClear = vi.fn();
const mockTerminalOpen = vi.fn();
const mockOnData = vi.fn((_handler: (data: string) => void) => ({ dispose: vi.fn() }));
const mockLoadAddon = vi.fn();
let mockTerminalCols = 80;
let mockTerminalRows = 24;

const MockTerminal = vi.fn(function MockTerminalConstructor(
  this: Record<string, unknown>,
  _options: unknown
) {
  this.open = mockTerminalOpen;
  this.loadAddon = mockLoadAddon;
  this.onData = mockOnData;
  this.write = mockTerminalWrite;
  this.clear = mockTerminalClear;
  this.dispose = mockTerminalDispose;
  this.options = {};
  Object.defineProperty(this, "cols", {
    get: () => mockTerminalCols,
    configurable: true,
  });
  Object.defineProperty(this, "rows", {
    get: () => mockTerminalRows,
    configurable: true,
  });
});

const mockFitAddonFit = vi.fn();
const MockFitAddon = vi.fn(function MockFitAddonConstructor(this: Record<string, unknown>) {
  this.fit = mockFitAddonFit;
});

const MockWebLinksAddon = vi.fn(function MockWebLinksAddonConstructor(
  this: Record<string, unknown>
) {
  // no-op
});

vi.mock("@xterm/xterm", () => ({
  Terminal: MockTerminal,
}));

vi.mock("@xterm/addon-fit", () => ({
  FitAddon: MockFitAddon,
}));

vi.mock("@xterm/addon-web-links", () => ({
  WebLinksAddon: MockWebLinksAddon,
}));

// Mock the CSS import
vi.mock("@xterm/xterm/css/xterm.css", () => ({}));

// Mock ResizeObserver
const mockObserve = vi.fn();
const mockUnobserve = vi.fn();
const mockDisconnect = vi.fn();

vi.stubGlobal(
  "ResizeObserver",
  vi.fn(function MockResizeObserver(this: Record<string, unknown>, _callback: unknown) {
    this.observe = mockObserve;
    this.unobserve = mockUnobserve;
    this.disconnect = mockDisconnect;
  })
);

// Mock MutationObserver
const mockMutationObserve = vi.fn();
const mockMutationDisconnect = vi.fn();

vi.stubGlobal(
  "MutationObserver",
  vi.fn(function MockMutationObserver(this: Record<string, unknown>, _callback: unknown) {
    this.observe = mockMutationObserve;
    this.disconnect = mockMutationDisconnect;
  })
);

// ==========================================
// Import component after mocks are set up
// ==========================================

import { XTerminal } from "./XTerminal";

// ==========================================
// Default props factory
// ==========================================

const mockSendInput = vi.fn();
const mockResize = vi.fn();
const mockCloseSession = vi.fn();
const mockRegisterOutputCallback = vi.fn(() => vi.fn()); // returns unsubscribe fn
const mockOnRestart = vi.fn();

function makeDefaultProps(
  overrides: Partial<Parameters<typeof XTerminal>[0]> = {}
): Parameters<typeof XTerminal>[0] {
  return {
    sessionId: "session-test",
    sendInput: mockSendInput,
    resize: mockResize,
    closeSession: mockCloseSession,
    registerOutputCallback: mockRegisterOutputCallback,
    isConnected: false,
    sessionStatus: "active" as const,
    ...overrides,
  };
}

// ==========================================
// Tests
// ==========================================

describe("XTerminal", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockTerminalCols = 80;
    mockTerminalRows = 24;
    mockRegisterOutputCallback.mockReturnValue(vi.fn());
  });

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

  // ==========================================
  // Rendering
  // ==========================================

  describe("rendering", () => {
    it("renders the terminal container", () => {
      render((<XTerminal {...makeDefaultProps()} />) as ReactElement);
      expect(screen.getByTestId("xterminal-container")).toBeInTheDocument();
    });

    it("renders the xterm viewport div", () => {
      render((<XTerminal {...makeDefaultProps()} />) as ReactElement);
      expect(screen.getByTestId("xterm-viewport")).toBeInTheDocument();
    });

    it("applies the className prop to the container", () => {
      render((<XTerminal {...makeDefaultProps()} className="custom-class" />) as ReactElement);
      expect(screen.getByTestId("xterminal-container")).toHaveClass("custom-class");
    });

    it("sets data-session-id on the container", () => {
      render((<XTerminal {...makeDefaultProps({ sessionId: "my-session" })} />) as ReactElement);
      expect(screen.getByTestId("xterminal-container")).toHaveAttribute(
        "data-session-id",
        "my-session"
      );
    });

    it("shows connecting message when not connected and session is active", () => {
      render((<XTerminal {...makeDefaultProps({ isConnected: false })} />) as ReactElement);
      expect(screen.getByText("Connecting to terminal...")).toBeInTheDocument();
    });

    it("does not show connecting message when connected", () => {
      render((<XTerminal {...makeDefaultProps({ isConnected: true })} />) as ReactElement);
      expect(screen.queryByText("Connecting to terminal...")).not.toBeInTheDocument();
    });

    it("does not show connecting message when session has exited", () => {
      render(
        (
          <XTerminal {...makeDefaultProps({ isConnected: false, sessionStatus: "exited" })} />
        ) as ReactElement
      );
      expect(screen.queryByText("Connecting to terminal...")).not.toBeInTheDocument();
    });
  });

  // ==========================================
  // Exit overlay
  // ==========================================

  describe("exit overlay", () => {
    it("shows restart button when session has exited", () => {
      render((<XTerminal {...makeDefaultProps({ sessionStatus: "exited" })} />) as ReactElement);
      expect(screen.getByRole("button", { name: /restart terminal/i })).toBeInTheDocument();
    });

    it("does not show restart button when session is active", () => {
      render((<XTerminal {...makeDefaultProps({ sessionStatus: "active" })} />) as ReactElement);
      expect(screen.queryByRole("button", { name: /restart terminal/i })).not.toBeInTheDocument();
    });

    it("shows exit code in restart button when provided", () => {
      render(
        (
          <XTerminal {...makeDefaultProps({ sessionStatus: "exited", exitCode: 1 })} />
        ) as ReactElement
      );
      expect(screen.getByRole("button", { name: /exit 1/i })).toBeInTheDocument();
    });

    it("calls onRestart when restart button is clicked", () => {
      render(
        (
          <XTerminal {...makeDefaultProps({ sessionStatus: "exited", onRestart: mockOnRestart })} />
        ) as ReactElement
      );
      fireEvent.click(screen.getByRole("button", { name: /restart terminal/i }));
      expect(mockOnRestart).toHaveBeenCalledTimes(1);
    });
  });

  // ==========================================
  // Output callback registration
  // ==========================================

  describe("registerOutputCallback", () => {
    it("registers a callback for its sessionId on mount", () => {
      render((<XTerminal {...makeDefaultProps({ sessionId: "test-session" })} />) as ReactElement);
      expect(mockRegisterOutputCallback).toHaveBeenCalledWith("test-session", expect.any(Function));
    });

    it("calls the returned unsubscribe function on unmount", () => {
      const unsubscribe = vi.fn();
      mockRegisterOutputCallback.mockReturnValue(unsubscribe);

      const { unmount } = render((<XTerminal {...makeDefaultProps()} />) as ReactElement);
      unmount();

      expect(unsubscribe).toHaveBeenCalled();
    });
  });

  // ==========================================
  // Accessibility
  // ==========================================

  describe("accessibility", () => {
    it("has an accessible region role", () => {
      render((<XTerminal {...makeDefaultProps()} />) as ReactElement);
      expect(screen.getByRole("region", { name: "Terminal" })).toBeInTheDocument();
    });
  });

  // ==========================================
  // Visibility
  // ==========================================

  describe("isVisible", () => {
    it("renders with isVisible=true by default", () => {
      render((<XTerminal {...makeDefaultProps()} />) as ReactElement);
      // Container is present; isVisible affects re-fit timing
      expect(screen.getByTestId("xterminal-container")).toBeInTheDocument();
    });
  });
});
