/**
 * Ticket #48 — cell controls must stay above in-cell failure/busy overlays.
 *
 * The regression was visual but user-facing: once a cell showed a terminal
 * launch failure banner or the F3 busy veil, the agent selector / split / close
 * controls could fall under it and become unusable. These tests keep the real
 * LayoutGrid/LeafView composition and stub only xterm, as the neighboring layout
 * tests do under jsdom.
 */
import { describe, it, expect, vi } from "vitest";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";

vi.mock("@xterm/xterm", () => ({
  Terminal: class {
    loadAddon() {}
    open() {}
    onData() {
      return { dispose() {} };
    }
    onResize() {
      return { dispose() {} };
    }
    write() {}
    dispose() {}
    get cols() {
      return 80;
    }
    get rows() {
      return 24;
    }
  },
}));
vi.mock("@xterm/addon-fit", () => ({
  FitAddon: class {
    fit() {}
  },
}));
vi.mock("@xterm/xterm/css/xterm.css", () => ({}));

if (typeof globalThis.ResizeObserver === "undefined") {
  globalThis.ResizeObserver = class {
    observe() {}
    unobserve() {}
    disconnect() {}
  } as unknown as typeof ResizeObserver;
}

import type { DomainEvent } from "@/domain";
import type { Gateways } from "@/ports";
import {
  MockAgentGateway,
  MockLayoutGateway,
  MockSystemGateway,
  MockTerminalGateway,
} from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { AnnouncementsProvider } from "@/features/announcements";
import { leaves } from "./layout";
import { LayoutGrid } from "./LayoutGrid";

interface GridSetup {
  agentId: string;
  bId: string;
  gateways: Gateways;
  layout: MockLayoutGateway;
  system: MockSystemGateway;
}

async function makeSplitAgentGrid(): Promise<GridSetup> {
  const layout = new MockLayoutGateway();
  const agentGateway = new MockAgentGateway();
  const terminal = new MockTerminalGateway();
  const system = new MockSystemGateway();

  const agent = await agentGateway.createAgent("p1", {
    name: "Worker",
    profileId: "claude",
  });

  const initial = await layout.loadLayout("p1");
  const aId = leaves(initial)[0].id;
  await layout.mutateLayout("p1", {
    type: "split",
    target: aId,
    direction: "row",
    newLeaf: "b",
    container: "c",
  });
  await layout.mutateLayout("p1", {
    type: "setCellAgent",
    target: "b",
    agent: agent.id,
  });

  return {
    agentId: agent.id,
    bId: "b",
    gateways: {
      layout,
      agent: agentGateway,
      terminal,
      system,
    } as unknown as Gateways,
    layout,
    system,
  };
}

function renderGrid(gateways: Gateways) {
  return render(
    <DIProvider gateways={gateways}>
      <AnnouncementsProvider>
        <LayoutGrid projectId="p1" cwd="/home/me/proj" />
      </AnnouncementsProvider>
    </DIProvider>,
  );
}

function busy(agentId: string, isBusy: boolean): DomainEvent {
  return { type: "agentBusyChanged", agentId, busy: isBusy };
}

async function flush() {
  await act(async () => {
    await Promise.resolve();
    await Promise.resolve();
  });
}

function controlsFor(cellId: string) {
  const selector = screen.getByLabelText(`agent selector ${cellId}`);
  const controls = selector.parentElement as HTMLElement;
  return {
    selector,
    controls,
    splitColumns: screen.getByLabelText(`split ${cellId} columns`),
    close: screen.getByLabelText(`close ${cellId}`),
  };
}

type CellAction = "selector" | "split" | "close";

function performAction(action: CellAction, cellId: string) {
  const { selector, splitColumns, close } = controlsFor(cellId);
  if (action === "selector") {
    fireEvent.change(selector, { target: { value: "" } });
  } else if (action === "split") {
    fireEvent.click(splitColumns);
  } else {
    fireEvent.click(close);
  }
}

function expectedOperation(action: CellAction, cellId: string) {
  if (action === "selector") {
    return expect.objectContaining({
      type: "setCellAgent",
      target: cellId,
      agent: null,
    });
  }
  if (action === "split") {
    return expect.objectContaining({
      type: "split",
      target: cellId,
      direction: "row",
    });
  }
  return expect.objectContaining({
    type: "merge",
    container: "c",
    keepIndex: 0,
  });
}

describe("LayoutGrid — ticket #48 cell control layering", () => {
  it.each<CellAction>(["selector", "split", "close"])(
    "keeps %s above/clickable over a terminal launch error",
    async (action) => {
      const setup = await makeSplitAgentGrid();
      const agentGateway = setup.gateways.agent as MockAgentGateway;
      vi.spyOn(agentGateway, "launchAgent").mockRejectedValue(
        new Error("profile CLI failed to start"),
      );
      const mutate = vi.spyOn(setup.layout, "mutateLayout");

      renderGrid(setup.gateways);

      const error = await screen.findByTestId("terminal-error");
      const { controls } = controlsFor(setup.bId);

      expect(controls.style.zIndex).toBe("5");
      expect(error.style.zIndex).toBe("3");

      performAction(action, setup.bId);
      await waitFor(() =>
        expect(mutate).toHaveBeenCalledWith(
          "p1",
          expectedOperation(action, setup.bId),
          undefined,
        ),
      );
    },
  );

  it.each<CellAction>(["selector", "split", "close"])(
    "keeps %s above/clickable over the F3 busy overlay",
    async (action) => {
      const setup = await makeSplitAgentGrid();
      const mutate = vi.spyOn(setup.layout, "mutateLayout");

      renderGrid(setup.gateways);
      await flush();

      act(() => setup.system.emit(busy(setup.agentId, true)));

      const overlay = await screen.findByTestId("target-announcements-overlay");
      const { controls } = controlsFor(setup.bId);

      expect(controls.style.zIndex).toBe("5");
      expect(overlay.className).toContain("z-[4]");
      expect(overlay.className).toContain("pointer-events-none");

      performAction(action, setup.bId);
      await waitFor(() =>
        expect(mutate).toHaveBeenCalledWith(
          "p1",
          expectedOperation(action, setup.bId),
          undefined,
        ),
      );
    },
  );
});
