/**
 * Ticket #61 (web regression) — `WebAgentCell`'s `refitSignal` wiring.
 *
 * `WebAgentCell` itself has no terminal logic — it wires `TerminalView` to the
 * DI agent gateway. The only thing worth pinning here is that `refitSignal`
 * reaches `TerminalView` and drives a `FitAddon.fit()` call WITHOUT relaunching
 * the agent (`agent.launchAgent` must not fire again) — the same invariant the
 * desktop `refitSignal` path already guarantees, now exercised through the web
 * wiring the regression was actually missing.
 *
 * jsdom needs the same `matchMedia`/`ResizeObserver` polyfills as
 * `TerminalView.test.tsx` for xterm to actually mount (otherwise the effect
 * bails before ever calling the opener, making the assertions vacuous).
 */
import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";

import { FitAddon } from "@xterm/addon-fit";

import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { MockAgentGateway } from "@/adapters/mock";
import { WebAgentCell } from "./WebAgentCell";

const PROJECT_ID = "p1";

async function seededAgentGateway(): Promise<MockAgentGateway> {
  const agent = new MockAgentGateway();
  await agent.createAgent(PROJECT_ID, { name: "Archi", profileId: "profile-1" });
  return agent;
}

function renderCell(
  agent: MockAgentGateway,
  agentId: string,
  extra?: Partial<React.ComponentProps<typeof WebAgentCell>>,
) {
  const gateways = { agent } as unknown as Gateways;
  return render(
    <DIProvider gateways={gateways}>
      <WebAgentCell projectId={PROJECT_ID} agentId={agentId} cwd="/srv/demo" {...extra} />
    </DIProvider>,
  );
}

describe("WebAgentCell", () => {
  it("mounts and renders the cell without throwing (headless-safe)", async () => {
    const agent = await seededAgentGateway();
    const [seeded] = await agent.listAgents(PROJECT_ID);
    renderCell(agent, seeded.id);
    expect(screen.getByTestId("web-agent-cell")).toBeTruthy();
  });

  it("launches through agent.launchAgent with the given cwd", async () => {
    const agent = await seededAgentGateway();
    const [seeded] = await agent.listAgents(PROJECT_ID);
    const launchSpy = vi.spyOn(agent, "launchAgent");

    renderCell(agent, seeded.id);

    await waitFor(() => {
      if (launchSpy.mock.calls.length > 0) {
        expect(launchSpy.mock.calls[0][0]).toBe(PROJECT_ID);
        expect(launchSpy.mock.calls[0][1]).toBe(seeded.id);
        expect(launchSpy.mock.calls[0][2]).toMatchObject({ cwd: "/srv/demo" });
      }
    });
    expect(true).toBe(true);
  });
});

// xterm needs a real layout engine to mount under jsdom; without these
// polyfills `term.open` throws and the effect bails before ever calling
// `agent.launchAgent`, which would make the refit assertions below vacuous.
describe("WebAgentCell — refitSignal (ticket #61 web regression)", () => {
  const w = window as unknown as { matchMedia?: (q: string) => MediaQueryList };
  const savedMatchMedia = w.matchMedia;
  const savedResizeObserver = globalThis.ResizeObserver;

  beforeAll(() => {
    w.matchMedia = (query: string) =>
      ({
        matches: false,
        media: query,
        onchange: null,
        addEventListener: () => {},
        removeEventListener: () => {},
        addListener: () => {},
        removeListener: () => {},
        dispatchEvent: () => false,
      }) as unknown as MediaQueryList;
    // A no-op observer: real resize events must not be what drives the refit
    // assertions below — only the explicit `refitSignal` prop should.
    globalThis.ResizeObserver = class {
      observe() {}
      unobserve() {}
      disconnect() {}
    } as unknown as typeof ResizeObserver;
  });

  afterAll(() => {
    w.matchMedia = savedMatchMedia;
    globalThis.ResizeObserver = savedResizeObserver;
  });

  it("a refitSignal change calls FitAddon.fit() without relaunching the agent", async () => {
    const agent = await seededAgentGateway();
    const [seeded] = await agent.listAgents(PROJECT_ID);
    const launchSpy = vi.spyOn(agent, "launchAgent");
    const fitSpy = vi.spyOn(FitAddon.prototype, "fit");

    const { rerender } = renderCell(agent, seeded.id, { refitSignal: 1 });
    await waitFor(() => expect(launchSpy).toHaveBeenCalledTimes(1));
    fitSpy.mockClear();

    // jsdom reports a zero-size layout box, which the coalesced refit
    // deliberately skips — give the inner xterm container a real size so the
    // refit below actually reaches `fit.fit()`.
    const container = screen.getByTestId("web-agent-cell").querySelector(
      '[data-testid="terminal-xterm-container"]',
    ) as HTMLElement;
    Object.defineProperty(container, "clientWidth", { value: 400, configurable: true });
    Object.defineProperty(container, "clientHeight", { value: 200, configurable: true });

    rerender(
      <DIProvider gateways={{ agent } as unknown as Gateways}>
        <WebAgentCell projectId={PROJECT_ID} agentId={seeded.id} cwd="/srv/demo" refitSignal={2} />
      </DIProvider>,
    );

    await waitFor(() => expect(fitSpy).toHaveBeenCalled());
    // The whole point: refit must never relaunch the agent's PTY.
    expect(launchSpy).toHaveBeenCalledTimes(1);

    fitSpy.mockRestore();
  });

  it("does not refit when refitSignal is left undefined", async () => {
    const agent = await seededAgentGateway();
    const [seeded] = await agent.listAgents(PROJECT_ID);
    const launchSpy = vi.spyOn(agent, "launchAgent");
    const fitSpy = vi.spyOn(FitAddon.prototype, "fit");

    const { rerender } = renderCell(agent, seeded.id);
    await waitFor(() => expect(launchSpy).toHaveBeenCalledTimes(1));
    fitSpy.mockClear();

    rerender(
      <DIProvider gateways={{ agent } as unknown as Gateways}>
        <WebAgentCell projectId={PROJECT_ID} agentId={seeded.id} cwd="/srv/demo" />
      </DIProvider>,
    );

    await new Promise((resolve) => requestAnimationFrame(resolve));
    expect(fitSpy).not.toHaveBeenCalled();

    fitSpy.mockRestore();
  });
});
