/**
 * F5 — the live web surfaces: a `event.domain` event refreshes the displayed
 * work-state; background tasks render with status + cancel/retry wired to the
 * gateway; a WS reconnect re-synchronises the read-model.
 */
import { describe, it, expect, vi, afterEach } from "vitest";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";

import type { Gateways } from "@/ports";
import type { BackgroundCompletion, ProjectWorkState } from "@/domain";
import { DIProvider } from "@/app/di";
import { createMockGateways, MockSystemGateway, MockWorkStateGateway } from "@/adapters/mock";
import {
  WsLiveClient,
  setWebLiveClient,
  WebSession,
} from "@/adapters/http";
import { WebApp } from "./WebApp";

afterEach(() => setWebLiveClient(null));

const okFetch = async () => ({
  ok: true,
  status: 200,
  json: async () => ({ ok: true }),
  text: async () => "{}",
});

function memStore() {
  const map = new Map<string, string>();
  return {
    getItem: (k: string) => map.get(k) ?? null,
    setItem: (k: string, v: string) => void map.set(k, v),
    removeItem: (k: string) => void map.delete(k),
  };
}

function state(agents: ProjectWorkState["agents"]): ProjectWorkState {
  return { agents, conversations: [] };
}

async function seeded(initial: ProjectWorkState): Promise<{ gateways: Gateways; projectId: string }> {
  const gateways = createMockGateways();
  const project = await gateways.project.createProject("Demo", "/srv/demo");
  (gateways.workState as MockWorkStateGateway)._setProjectWorkState(project.id, initial);
  return { gateways, projectId: project.id };
}

function renderPaired(gateways: Gateways) {
  const session = new WebSession({ baseUrl: "https://h", fetchImpl: okFetch, store: memStore() });
  session.markPaired();
  return render(
    <DIProvider gateways={gateways}>
      <WebApp session={session} />
    </DIProvider>,
  );
}

const AGENT_IDLE = { agentId: "a1", name: "Archi", profileId: "p1", busy: { state: "idle" as const }, tickets: [] };

describe("WebWorkspace live surfaces (F5)", () => {
  it("refreshes the work-state on a relevant event.domain event", async () => {
    const { gateways, projectId } = await seeded(state([AGENT_IDLE]));
    renderPaired(gateways);

    fireEvent.click(await screen.findByText("Demo"));
    const panel = await screen.findByTestId("web-workstate");
    expect(panel.textContent).toContain("Idle");

    // The agent goes busy server-side; the read-model now reflects it…
    (gateways.workState as MockWorkStateGateway)._setProjectWorkState(
      projectId,
      state([{ ...AGENT_IDLE, busy: { state: "busy", ticket: "t-1", sinceMs: 1 } }]),
    );
    // …and an `agentBusyChanged` domain event drives a live refresh.
    act(() => {
      (gateways.system as MockSystemGateway).emit({ type: "agentBusyChanged", agentId: "a1", busy: true });
    });

    await waitFor(() =>
      expect(screen.getByTestId("web-workstate").textContent).toContain("Busy"),
    );
  });

  it("renders background tasks and wires cancel through the gateway", async () => {
    const task: BackgroundCompletion = {
      taskId: "bt-1",
      ownerAgentId: "a1",
      projectId: "p",
      kind: "shell",
      status: "running",
      exitCode: null,
      summary: null,
      stdoutTail: null,
      stderrTail: null,
      updatedAtMs: 1,
    };
    const { gateways } = await seeded(state([{ ...AGENT_IDLE, backgroundTasks: [task] }]));
    const cancelSpy = vi.spyOn(gateways.workState, "cancelBackgroundTask");
    renderPaired(gateways);

    fireEvent.click(await screen.findByText("Demo"));
    const tasks = await screen.findByLabelText("Archi background tasks");
    expect(tasks.textContent).toContain("Running");
    expect(tasks.textContent).toContain("shell");

    fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
    await waitFor(() => expect(cancelSpy).toHaveBeenCalledWith("bt-1"));
  });

  it("re-synchronises the read-model when the WS reconnects", async () => {
    const { gateways, projectId } = await seeded(state([AGENT_IDLE]));
    // Register a live client whose connection state we can drive.
    let notify: ((s: "connecting" | "connected" | "reconnecting" | "closed") => void) | null = null;
    const fakeClient = {
      getConnectionState: () => "connected" as const,
      onConnectionStateChange: (l: (s: "connecting" | "connected" | "reconnecting" | "closed") => void) => {
        notify = l;
        return () => {
          notify = null;
        };
      },
    };
    setWebLiveClient(fakeClient as unknown as WsLiveClient);

    const refreshSpy = vi.spyOn(gateways.workState, "getProjectWorkState");
    renderPaired(gateways);
    fireEvent.click(await screen.findByText("Demo"));
    await screen.findByTestId("web-workstate");
    const callsAfterOpen = refreshSpy.mock.calls.length;
    expect(projectId).toBeTruthy();

    // Simulate a reconnect: reconnecting → connected must re-fetch the read-model.
    act(() => {
      notify?.("reconnecting");
      notify?.("connected");
    });
    await waitFor(() => expect(refreshSpy.mock.calls.length).toBeGreaterThan(callsAfterOpen));
  });
});
