import { describe, expect, test } from "bun:test";
import type { FetchLike } from "./kanboard-client";
import { createKanboardClient } from "./kanboard-client";
import { checkIncidentsReadiness } from "./bootstrap";

const BASE_URL = "http://127.0.0.1:31339/jsonrpc.php";
const TOKEN = "incident-bootstrap-token";

type PlannedResponse =
  | { kind: "result"; value: unknown }
  | { kind: "error"; error: { code: number; message: string } };

interface RecordedCall {
  method: string;
  params: unknown;
}

function createBootstrapFetcher(routes: Partial<Record<string, (id: string, params: unknown) => PlannedResponse>>) {
  const calls: RecordedCall[] = [];
  const ordering: Record<string, (id: string, params: unknown) => PlannedResponse> = {
    changeColumnPosition: () => ({ kind: "result", value: true }),
    changeSwimlanePosition: () => ({ kind: "result", value: true }),
    enableSwimlane: () => ({ kind: "result", value: true }),
    updateSwimlane: () => ({ kind: "result", value: true }),
  };
  const fetchImpl: FetchLike = async (_input, init) => {
    const request = JSON.parse(String(init?.body));
    const method = request.method as string;
    const route = routes[method] ?? ordering[method];
    if (!route) {
      throw new Error(`Unexpected method ${method}`);
    }
    calls.push({ method, params: request.params });

    const planned = route(request.id, request.params);
    if (planned.kind === "result") {
      return new Response(JSON.stringify({ jsonrpc: "2.0", id: request.id, result: planned.value }), { status: 200 });
    }
    return new Response(
      JSON.stringify({ jsonrpc: "2.0", id: request.id, error: planned.error }),
      { status: 200 },
    );
  };
  return { fetchImpl, calls };
}

function projectRecord(overrides: Record<string, unknown> = {}) {
  return {
    id: 11,
    identifier: "OVERDECKINCIDENTS",
    name: "Overdeck Incidents",
    is_public: false,
    is_private: false,
    is_active: true,
    per_swimlane_task_limits: false,
    enable_global_tags: false,
    hide_in_dashboard: false,
    priority_start: 0,
    priority_end: 3,
    priority_default: 2,
    description: null,
    email: null,
    token: "token",
    url: {},
    last_modified: 0,
    ...overrides,
  };
}

function expectedColumns() {
  return [
    { id: 101, title: "Filed", position: 1 },
    { id: 102, title: "Dispatching", position: 2 },
    { id: 103, title: "Running", position: 3 },
    { id: 104, title: "Needs attention", position: 4 },
    { id: 105, title: "Resolved", position: 5 },
  ];
}

function expectedSwimlanes() {
  return [
    { id: 201, name: "P0 · Critical", position: 1, is_active: true },
    { id: 202, name: "P1 · High", position: 2, is_active: true },
    { id: 203, name: "P2 · Normal", position: 3, is_active: true },
    { id: 204, name: "P3 · Low", position: 4, is_active: true },
  ];
}

describe("checkIncidentsReadiness", () => {
  test("creates project on bootstrap absent case", async () => {
    const { fetchImpl, calls } = createBootstrapFetcher({
      getProjectByIdentifier: () => ({ kind: "result", value: false }),
      createProject: () => ({
        kind: "result",
        value: 1000,
      }),
      getColumns: () => ({
        kind: "result",
        value: [
          { id: "900", title: "Backlog", position: "1" },
          { id: "901", title: "Ready", position: "2" },
          { id: "902", title: "Work in progress", position: "3" },
          { id: "903", title: "Done", position: "4" },
        ],
      }),
      updateColumn: () => ({ kind: "result", value: true }),
      addColumn: (id, params) => {
        if (typeof params !== "object" || params === null || !("title" in params)) {
          return { kind: "error", error: { code: -1, message: "bad params" } };
        }
        const titles = {
          Resolved: "104",
        };
        const title = (params as { title: string }).title;
        return { kind: "result", value: titles[title as keyof typeof titles] ?? "999" };
      },
      changeColumnPosition: () => ({ kind: "result", value: true }),
      getAllSwimlanes: () => ({
        kind: "result",
        value: [{ id: "301", name: "Default swimlane", position: "1", is_active: true }],
      }),
      addSwimlane: (id, params) => {
        if (typeof params !== "object" || params === null || !("name" in params)) {
          return { kind: "error", error: { code: -1, message: "bad params" } };
        }
        const names = {
          "P0 · Critical": "201",
          "P1 · High": "202",
          "P2 · Normal": "203",
          "P3 · Low": "204",
        };
        const name = (params as { name: string }).name;
        return { kind: "result", value: names[name as keyof typeof names] ?? "999" };
      },
      changeSwimlanePosition: () => ({ kind: "result", value: true }),
      getUserByName: () => ({ kind: "result", value: false }),
      createUser: () => ({ kind: "result", value: 501 }),
    });

    const client = createKanboardClient({ baseUrl: BASE_URL, token: TOKEN, fetchImpl });
    const result = await checkIncidentsReadiness(client);

    expect(result.status).toBe("ready");
    expect(result.projectId).toBe(1000);
    expect(result.agentUserId).toBe(501);
    expect(result.drift).toEqual([]);
    expect(calls.find((call) => call.method === "createProject")).toBeDefined();
    expect(calls.find((call) => call.method === "getProjectByIdentifier")).toBeDefined();
    expect(calls.filter((call) => call.method === "updateColumn")).toHaveLength(4);
    expect(calls.filter((call) => call.method === "addColumn")).toHaveLength(1);
    const createProjectCall = calls.find((call) => call.method === "createProject");
    expect((createProjectCall?.params as { is_private?: unknown }).is_private).toBeUndefined();
    expect(calls.every((call) => call.method !== "createTask")).toBe(true);
  });

  test("is idempotent and makes no redundant writes when already correct", async () => {
    const { fetchImpl, calls } = createBootstrapFetcher({
      getProjectByIdentifier: () => ({ kind: "result", value: projectRecord() }),
      getColumns: () => ({ kind: "result", value: expectedColumns() }),
      getAllSwimlanes: () => ({ kind: "result", value: expectedSwimlanes() }),
      getUserByName: () => ({ kind: "result", value: { id: 501 } }),
    });

    const client = createKanboardClient({ baseUrl: BASE_URL, token: TOKEN, fetchImpl });
    const result = await checkIncidentsReadiness(client);

    expect(result.status).toBe("ready");
    expect(result.drift).toEqual([]);
    const writeMethods = new Set([
      "createProject",
      "updateProject",
      "updateColumn",
      "addColumn",
      "changeColumnPosition",
      "addSwimlane",
      "updateSwimlane",
      "enableSwimlane",
      "changeSwimlanePosition",
      "createUser",
    ]);
    for (const call of calls) {
      expect(writeMethods.has(call.method)).toBe(false);
    }
  });

  test("reports drift when project remains public", async () => {
    const { fetchImpl, calls } = createBootstrapFetcher({
      getProjectByIdentifier: () => ({ kind: "result", value: projectRecord({ is_public: true }) }),
      getColumns: () => ({ kind: "result", value: expectedColumns() }),
      getAllSwimlanes: () => ({ kind: "result", value: expectedSwimlanes() }),
      getUserByName: () => ({ kind: "result", value: { id: 501 } }),
    });

    const client = createKanboardClient({ baseUrl: BASE_URL, token: TOKEN, fetchImpl });
    const result = await checkIncidentsReadiness(client);

    expect(result.status).toBe("degraded");
    expect(result.drift).toEqual(["project:is_public"]);
    expect(calls.find((call) => call.method === "updateProject")).toBeUndefined();
  });

  test("adds a missing column while keeping readiness ready", async () => {
    const columns = [
      { id: 101, title: "Filed", position: "1" },
      { id: 102, title: "Running", position: "2" },
      { id: 103, title: "Needs attention", position: "3" },
      { id: 104, title: "Resolved", position: "4" },
    ];
    const { fetchImpl, calls } = createBootstrapFetcher({
      getProjectByIdentifier: () => ({ kind: "result", value: projectRecord() }),
      getColumns: () => ({ kind: "result", value: columns }),
      addColumn: (id, params) => {
        if (typeof params !== "object" || params === null || !("title" in params)) {
          return { kind: "error", error: { code: -1, message: "bad params" } };
        }
        return {
          kind: "result",
          value: params.title === "Dispatching" ? "105" : "999",
        };
      },
      changeColumnPosition: () => ({ kind: "result", value: true }),
      getAllSwimlanes: () => ({ kind: "result", value: expectedSwimlanes() }),
      getUserByName: () => ({ kind: "result", value: { id: 501 } }),
    });

    const client = createKanboardClient({ baseUrl: BASE_URL, token: TOKEN, fetchImpl });
    const result = await checkIncidentsReadiness(client);

    expect(result.status).toBe("ready");
    expect(result.columnIds["Dispatching"]).toBe(105);
    expect(calls.filter((call) => call.method === "addColumn")).toHaveLength(1);
  });

  test("adds a missing swimlane while keeping readiness ready", async () => {
    const swimlanes = [
      { id: 300, name: "Default swimlane", position: "1", is_active: true },
      { id: 201, name: "P0 · Critical", position: "2", is_active: true },
      { id: 202, name: "P1 · High", position: "3", is_active: true },
      { id: 203, name: "P3 · Low", position: "4", is_active: true },
    ];
    const { fetchImpl, calls } = createBootstrapFetcher({
      getProjectByIdentifier: () => ({ kind: "result", value: projectRecord() }),
      getColumns: () => ({ kind: "result", value: expectedColumns() }),
      getAllSwimlanes: () => ({ kind: "result", value: swimlanes }),
      addSwimlane: (id, params) => {
        if (typeof params !== "object" || params === null || !("name" in params)) {
          return { kind: "error", error: { code: -1, message: "bad params" } };
        }
        return { kind: "result", value: "204" };
      },
      changeSwimlanePosition: () => ({ kind: "result", value: true }),
      getUserByName: () => ({ kind: "result", value: { id: 501 } }),
    });

    const client = createKanboardClient({ baseUrl: BASE_URL, token: TOKEN, fetchImpl });
    const result = await checkIncidentsReadiness(client);

    expect(result.status).toBe("ready");
    expect(result.swimlaneIds["P2 · Normal"]).toBe(204);
    expect(calls.filter((call) => call.method === "addSwimlane")).toHaveLength(1);
  });

  test("reports degraded readiness on unknown structural drift", async () => {
    const driftColumns = [
      ...expectedColumns(),
      { id: 909, title: "Legacy", position: "6" },
    ];
    const driftSwimlanes = [
      { id: 300, name: "Default swimlane", position: "1", is_active: true },
      ...expectedSwimlanes(),
      { id: 909, name: "Backlog", position: "5", is_active: true },
    ];

    const { fetchImpl, calls } = createBootstrapFetcher({
      getProjectByIdentifier: () => ({ kind: "result", value: projectRecord() }),
      getColumns: () => ({ kind: "result", value: driftColumns }),
      getAllSwimlanes: () => ({ kind: "result", value: driftSwimlanes }),
      getUserByName: () => ({ kind: "result", value: { id: 501 } }),
    });

    const client = createKanboardClient({ baseUrl: BASE_URL, token: TOKEN, fetchImpl });
    const result = await checkIncidentsReadiness(client);

    expect(result.status).toBe("degraded");
    expect(result.drift).toEqual(["column:Legacy", "swimlane:Backlog"]);
    expect(calls.every((call) => call.method !== "createTask")).toBe(true);
  });

  test("does not consider token or auth in error messages", async () => {
    const fetchImpl: FetchLike = async (_input, init) => {
      return new Response(
        JSON.stringify({ jsonrpc: "2.0", id: JSON.parse(String(init?.body)).id, error: { code: -1, message: "denied" } }),
        { status: 200 },
      );
    };
    const client = createKanboardClient({ baseUrl: BASE_URL, token: TOKEN, fetchImpl });

    await expect(client.call("getProjectByIdentifier", { identifier: "OVERDECKINCIDENTS" })).rejects.toThrowError(/denied/);
  });
});
