/**
 * L4 — {@link LayoutGrid} wired through the real {@link DIProvider} with the
 * in-memory {@link MockLayoutGateway} (+ {@link MockTerminalGateway} for the
 * leaves' embedded {@link TerminalView}).
 *
 * Under jsdom xterm's `term.open` may bail gracefully (no real layout engine),
 * so these tests assert the *structure* the grid renders (cells, no throw)
 * rather than xterm's visual output. They stay robust whether or not xterm
 * bailed.
 */
import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";

import type { Gateways } from "@/ports";
import { MockLayoutGateway, MockTerminalGateway } from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { leaves } from "./layout";
import { LayoutGrid } from "./LayoutGrid";

function renderGrid(layout: MockLayoutGateway, projectId = "p1") {
  const gateways = {
    layout,
    terminal: new MockTerminalGateway(),
  } as unknown as Gateways;
  return render(
    <DIProvider gateways={gateways}>
      <LayoutGrid projectId={projectId} cwd="/home/me/proj" />
    </DIProvider>,
  );
}

describe("LayoutGrid (with MockLayoutGateway)", () => {
  it("renders a single-leaf layout without throwing", async () => {
    const layout = new MockLayoutGateway();
    expect(() => renderGrid(layout)).not.toThrow();

    // The grid container always renders.
    expect(screen.getByTestId("layout-grid")).toBeTruthy();
    // Once loaded, the single leaf cell is shown.
    await waitFor(() => {
      expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1);
    });
  });

  it("renders N cells for a pre-split tree", async () => {
    const layout = new MockLayoutGateway();
    // Pre-split the project's tree into two leaves before rendering.
    const initial = await layout.loadLayout("p1");
    await layout.mutateLayout("p1", {
      type: "split",
      target: leaves(initial)[0].id,
      direction: "row",
      newLeaf: "b",
      container: "c",
    });

    renderGrid(layout);

    await waitFor(() => {
      expect(screen.getAllByTestId("layout-leaf")).toHaveLength(2);
    });
    // The split container is present with its direction recorded.
    const split = screen.getByTestId("layout-split");
    expect(split.getAttribute("data-direction")).toBe("row");
  });

  it("stays graceful (no throw) even though xterm bails under jsdom", async () => {
    const layout = new MockLayoutGateway();
    // Render a 3-leaf tree: nested splits, each leaf hosts a TerminalView.
    const initial = await layout.loadLayout("p1");
    const a = leaves(initial)[0].id;
    await layout.mutateLayout("p1", {
      type: "split",
      target: a,
      direction: "row",
      newLeaf: "b",
      container: "c",
    });
    await layout.mutateLayout("p1", {
      type: "split",
      target: "b",
      direction: "column",
      newLeaf: "d",
      container: "e",
    });

    expect(() => renderGrid(layout)).not.toThrow();
    await waitFor(() => {
      expect(screen.getAllByTestId("layout-leaf")).toHaveLength(3);
    });
  });

  it("closing a cell removes THAT cell and keeps its sibling without killing its session", async () => {
    const layout = new MockLayoutGateway();
    const terminal = new MockTerminalGateway();
    const closeSpy = vi.spyOn(terminal, "closeTerminal");
    const initial = await layout.loadLayout("p1");
    const a = leaves(initial)[0].id;
    // Split A → container c with children [A (index 0), b (index 1)].
    await layout.mutateLayout("p1", {
      type: "split",
      target: a,
      direction: "row",
      newLeaf: "b",
      container: "c",
    });
    await layout.mutateLayout("p1", {
      type: "setSession",
      target: "b",
      session: "running-session-b",
    });

    const gateways = { layout, terminal } as unknown as Gateways;
    render(
      <DIProvider gateways={gateways}>
        <LayoutGrid projectId="p1" cwd="/home/me/proj" />
      </DIProvider>,
    );
    await waitFor(() =>
      expect(screen.getAllByTestId("layout-leaf")).toHaveLength(2),
    );

    // Click the close button on cell `b` — `b` must disappear, `a` must remain.
    fireEvent.click(screen.getByLabelText("close b"));

    await waitFor(() =>
      expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1),
    );
    expect(
      screen.getByTestId("layout-leaf").getAttribute("data-node-id"),
    ).toBe(a);
    expect(closeSpy).not.toHaveBeenCalled();
  });

  it("closing the other cell keeps the opposite sibling", async () => {
    const layout = new MockLayoutGateway();
    const initial = await layout.loadLayout("p1");
    const a = leaves(initial)[0].id;
    await layout.mutateLayout("p1", {
      type: "split",
      target: a,
      direction: "row",
      newLeaf: "b",
      container: "c",
    });

    renderGrid(layout);
    await waitFor(() =>
      expect(screen.getAllByTestId("layout-leaf")).toHaveLength(2),
    );

    // Closing `a` (index 0) must keep `b` (index 1).
    fireEvent.click(screen.getByLabelText(`close ${a}`));

    await waitFor(() =>
      expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1),
    );
    expect(
      screen.getByTestId("layout-leaf").getAttribute("data-node-id"),
    ).toBe("b");
  });

  it("the sole root cell has no close button (cannot be closed)", async () => {
    const layout = new MockLayoutGateway();
    renderGrid(layout);
    await waitFor(() =>
      expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1),
    );
    expect(screen.queryByLabelText(/^close /)).toBeNull();
  });

  it("renders the loading/empty container when the layout gateway is absent", () => {
    // A DI subset without the layout gateway → useLayout is defensive (null).
    const gateways = { terminal: new MockTerminalGateway() } as unknown as Gateways;
    expect(() =>
      render(
        <DIProvider gateways={gateways}>
          <LayoutGrid projectId="p1" cwd="/c" />
        </DIProvider>,
      ),
    ).not.toThrow();
    expect(screen.getByTestId("layout-grid")).toBeTruthy();
  });
});
