import { describe, expect, test } from "bun:test";
import { INCIDENTS_COLUMNS, INCIDENTS_SWIMLANES, checkIncidentsReadiness } from "./bootstrap";
import { createKanboardClient } from "./kanboard-client";
import { createIncidentService } from "./incident-service";

const LIVE_URL = process.env.OVERDECK_KANBOARD_LIVE_URL;
const LIVE_TOKEN = process.env.OVERDECK_KANBOARD_LIVE_TOKEN;

const runLiveSuite = Boolean(LIVE_URL && LIVE_TOKEN);
const describeLive = runLiveSuite ? describe : describe.skip;

// removeTask is deliberately absent from the client's closed procedure union:
// Overdeck never deletes an incident. Fixture cleanup calls it out-of-band.
async function removeTaskFixture(taskId: number): Promise<void> {
  const response = await fetch(LIVE_URL!, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization: `Basic ${Buffer.from(`jsonrpc:${LIVE_TOKEN!}`).toString("base64")}`,
    },
    body: JSON.stringify({ jsonrpc: "2.0", id: "cleanup", method: "removeTask", params: { task_id: taskId } }),
  });
  const body = (await response.json()) as { result?: unknown };
  if (body.result !== true) {
    throw new Error(`failed to remove fixture task ${taskId}`);
  }
}

describeLive("checkIncidentsReadiness (live Kanboard)", () => {
  test("bootstraps with real store idempotently and validates measured shape", async () => {
    if (!LIVE_URL || !LIVE_TOKEN) {
      return;
    }
    const client = createKanboardClient({
      baseUrl: LIVE_URL,
      token: LIVE_TOKEN,
      fetchImpl: fetch,
    });

    const first = await checkIncidentsReadiness(client);
    const second = await checkIncidentsReadiness(client);

    expect(first.status).toBe("ready");
    expect(first.drift).toEqual([]);
    expect(second.status).toBe("ready");
    expect(second.drift).toEqual([]);
    expect(second.projectId).toBe(first.projectId);
    expect(second.columnIds).toEqual(first.columnIds);
    expect(second.swimlaneIds).toEqual(first.swimlaneIds);
    expect(second.agentUserId).toBe(first.agentUserId);

    const columns = await client.call("getColumns", { project_id: first.projectId });
    expect(columns).toHaveLength(5);
    expect(columns.map((column) => column.title)).toEqual([...INCIDENTS_COLUMNS]);
    for (const [index, column] of columns.entries()) {
      expect(column.position).toBe(index + 1);
    }

    const swimlanes = await client.call("getAllSwimlanes", { project_id: first.projectId });
    const defaultIndex = swimlanes.findIndex((swimlane) => swimlane.name === "Default swimlane");
    expect(defaultIndex).toBeGreaterThanOrEqual(0);
    const incidentSwimlanes = swimlanes.slice(defaultIndex + 1, defaultIndex + 1 + INCIDENTS_SWIMLANES.length);
    expect(incidentSwimlanes).toHaveLength(INCIDENTS_SWIMLANES.length);
    expect(incidentSwimlanes.map((swimlane) => swimlane.name)).toEqual([...INCIDENTS_SWIMLANES]);
    for (const swimlane of incidentSwimlanes) {
      expect(swimlane.is_active).toBe(true);
    }

    const project = await client.call("getProjectByIdentifier", { identifier: "OVERDECKINCIDENTS" });
    expect(project.is_public).toBe(false);
    expect(project.priority_start).toBe(0);
    expect(project.priority_end).toBe(3);
    expect(project.priority_default).toBe(2);
    expect(project.id).toBe(first.projectId);
  });

  test("projects real tasks through the incident read path", async () => {
    if (!LIVE_URL || !LIVE_TOKEN) {
      return;
    }
    const client = createKanboardClient({ baseUrl: LIVE_URL, token: LIVE_TOKEN, fetchImpl: fetch });
    const readiness = await checkIncidentsReadiness(client);
    const service = createIncidentService({ client, readiness });

    const incidentId = `live-${crypto.randomUUID()}`;
    const bareTaskId = await client.call("createTask", {
      project_id: readiness.projectId,
      title: "Live read-path bare task",
      column_id: readiness.columnIds.Filed,
      swimlane_id: readiness.swimlaneIds["P2 · Normal"],
      priority: 2,
      description: `Operator body\n\nOverdeck-Incident: ${incidentId}`,
    });

    try {
      await client.call("updateTask", { id: bareTaskId, reference: incidentId });

      const listed = await service.listIncidents({ scope: "active" });
      const bare = listed.incidents.find((entry) => entry.kanboardTaskId === bareTaskId);
      expect(bare).toBeDefined();
      expect(bare!.state).toBe("filed");
      expect(bare!.priority).toBe("P2");
      expect(bare!.description).toBe("Operator body");
      expect(bare!.dispatch.cli).toBeNull();
      expect(bare!.dispatch.startedAt).toBeNull();
      expect(bare!.dispatch.exitCode).toBeNull();
      expect(bare!.createdAt).not.toBeNull();
      expect(bare!.coverage.stale).toBe(false);
      expect(bare!.activity).toEqual([]);

      await client.call("createComment", {
        task_id: bareTaskId,
        user_id: readiness.agentUserId,
        content: "live lifecycle note",
      });
      await client.call("saveTaskMetadata", {
        task_id: bareTaskId,
        values: { "overdeck.dispatch_state": "running", "overdeck.cli": "codex" },
      });
      await client.call("moveTaskPosition", {
        project_id: readiness.projectId,
        task_id: bareTaskId,
        column_id: readiness.columnIds.Running,
        position: 1,
        swimlane_id: readiness.swimlaneIds["P2 · Normal"],
      });

      const detail = await service.getIncident(incidentId);
      expect(detail).not.toBeNull();
      expect(detail!.kanboardTaskId).toBe(bareTaskId);
      expect(detail!.state).toBe("running");
      expect(detail!.dispatch.cli).toBe("codex");
      expect(detail!.activity).toHaveLength(1);
      expect(detail!.activity[0]!.comment).toBe("live lifecycle note");

      expect(await service.getIncident(`missing-${crypto.randomUUID()}`)).toBeNull();
    } finally {
      await removeTaskFixture(bareTaskId);
    }
  });
});
