import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import type { Server } from "bun";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Journal } from "../src/journal";
import { CollectorState } from "../src/state";
import { startServer } from "../src/server";
import type { FileIncidentRequest, Incident } from "../src/incidents/incident-service";
import type { IncidentsProvider } from "../src/incidents/provider";
import { IncidentOptionsError, type LoadedIncidentOptions } from "../src/incidents/dispatch-options";

const TOKEN = "incidents-post-token";
const validBody = {
  requestId: "123e4567-e89b-42d3-a456-426614174000",
  title: "Collector wedged",
  description: "It stopped emitting deltas.",
  cli: "codex",
  model: "gpt-5.6-sol",
  reasoningEffort: "high",
  account: "work",
  unsafe: false,
  priority: "P1",
} as const;
const validRequest: FileIncidentRequest = { ...validBody, wrapperModel: "gpt-5.6-sol-high" };

function incident(request: FileIncidentRequest): Incident {
  return {
    id: request.requestId, kanboardTaskId: 42, title: request.title, description: request.description,
    priority: request.priority, incidentType: request.incidentType ?? null, dispatchBrief: null, dispatchBriefProvenance: null,
    state: "filed", active: true, createdAt: null, updatedAt: null, resolvedAt: null,
    dispatch: { state: "filed", dispatchId: null, cli: request.cli, model: request.model, wrapperModel: null,
      reasoningEffort: request.reasoningEffort, account: request.account, requestSha256: null, statusRevision: null,
      startedAt: null, heartbeatAt: null, completedAt: null, exitCode: null, failureClass: null, resultSummary: null },
    activity: [], coverage: { stale: false },
  };
}

function optionsFixture(overrides: Partial<LoadedIncidentOptions> = {}): LoadedIncidentOptions {
  const internal = new Map<string, { wrapperModel: string; permissionMode: "safe" | "unsafe"; fixed: boolean }>();
  for (const mode of ["safe", "unsafe"] as const) internal.set(["codex", "gpt-5.6-sol", "high", "work", mode].join("\0"), { wrapperModel: "gpt-5.6-sol-high", permissionMode: mode, fixed: false });
  return {
    types: [],
    clis: [{ id: "codex", label: "Codex", models: [{ id: "gpt-5.6-sol", efforts: ["high"] }], accounts: [{ slug: "work", label: "work", ready: true, fixed: false }], permissionModes: ["safe", "unsafe"] }],
    sourcePaths: [],
    internal,
    ...overrides,
  };
}

describe("POST /incidents", () => {
  let server: Server<undefined>;
  let dir: string;
  let filed: FileIncidentRequest[];

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), "overdeck-incidents-post-"));
    filed = [];
    const provider: IncidentsProvider = {
      async listIncidents() { return { incidents: [], coverage: { stale: false }, page: { highWater: 0, nextCursor: null, exhausted: true, order: "id_desc" } }; },
      async getIncident() { return null; },
      async fileIncident(request) { filed.push(request); return incident(request); },
      async dispatchIncident() { throw new Error("not under test"); },
      async resolveIncident() { throw new Error("not under test"); },
      getDispatchLauncher() { return null; },
    };
    server = startServer({ host: "127.0.0.1", port: 0, token: TOKEN,
      state: new CollectorState(new Journal(join(dir, "items.jsonl"))), incidents: provider, loadIncidentOptions: async () => optionsFixture() });
  });

  afterEach(() => { server.stop(true); rmSync(dir, { recursive: true, force: true }); });

  function post(body: string, headers: Record<string, string> = {}): Promise<Response> {
    return fetch(`http://127.0.0.1:${server.port}/incidents`, { method: "POST", body,
      headers: { authorization: `Bearer ${TOKEN}`, "content-type": "application/json", ...headers } });
  }

  test("validates and forwards a filing to the incidents provider", async () => {
    const response = await post(JSON.stringify(validBody));
    expect(response.status).toBe(201);
    expect(await response.json()).toEqual({ incident: incident(validRequest) });
    expect(filed).toEqual([validRequest]);
  });

  test("rejects malformed and unknown fields before reaching the provider", async () => {
    const response = await post(JSON.stringify({ ...validBody, priority: "P9", extra: true }));
    expect(response.status).toBe(400);
    expect(await response.json()).toEqual({ error: "invalid-incident" });
    expect(filed).toEqual([]);
  });

  test("rejects a client-supplied wrapper model before reaching the provider", async () => {
    const response = await post(JSON.stringify({ ...validBody, wrapperModel: "gpt-5.6-sol-high" }));
    expect(response.status).toBe(400);
    expect(await response.json()).toEqual({ error: "invalid-incident" });
    expect(filed).toEqual([]);
  });

  test("rejects each tampered dispatch dimension before reaching the provider", async () => {
    for (const body of [
      { ...validBody, cli: "claude" },
      { ...validBody, model: "gpt-5.6-terra" },
      { ...validBody, reasoningEffort: "low" },
      { ...validBody, account: "personal" },
    ]) {
      filed = [];
      const response = await post(JSON.stringify(body));
      expect(response.status).toBe(400);
      expect(await response.json()).toEqual({ error: "invalid-incident-dispatch" });
      expect(filed).toEqual([]);
    }
  });

  test("accepts unsafe:true when authority declares unsafe and persists authoritative wrapper", async () => {
    const response = await post(JSON.stringify({ ...validBody, unsafe: true }));
    expect(response.status).toBe(201);
    expect(filed).toEqual([{ ...validRequest, unsafe: true }]);
    expect(filed[0]?.unsafe).toBe(true);
    expect(filed[0]?.wrapperModel).toBe("gpt-5.6-sol-high");
  });

  test("rejects unsupported unsafe permission before reaching the provider", async () => {
    const safeOnly: LoadedIncidentOptions = {
      types: [],
      clis: [{ id: "codex", label: "Codex", models: [{ id: "gpt-5.6-sol", efforts: ["high"] }], accounts: [{ slug: "work", label: "work", ready: true, fixed: false }], permissionModes: ["safe"] }],
      sourcePaths: [],
      internal: new Map([[["codex", "gpt-5.6-sol", "high", "work", "safe"].join("\0"), { wrapperModel: "gpt-5.6-sol-high", permissionMode: "safe", fixed: false }]]),
    };
    server.stop(true);
    server = startServer({ host: "127.0.0.1", port: 0, token: TOKEN,
      state: new CollectorState(new Journal(join(dir, "items.jsonl"))), incidents: {
        async listIncidents() { return { incidents: [], coverage: { stale: false }, page: { highWater: 0, nextCursor: null, exhausted: true, order: "id_desc" } }; },
        async getIncident() { return null; },
        async fileIncident(request) { filed.push(request); return incident(request); },
        async dispatchIncident() { throw new Error("not under test"); },
        async resolveIncident() { throw new Error("not under test"); },
      getDispatchLauncher() { return null; },
      }, loadIncidentOptions: async () => safeOnly });

    const response = await post(JSON.stringify({ ...validBody, unsafe: true }));
    expect(response.status).toBe(400);
    expect(await response.json()).toEqual({ error: "invalid-incident-dispatch" });
    expect(filed).toEqual([]);
  });

  test("rejects an unready account before reaching the provider", async () => {
    server.stop(true);
    server = startServer({ host: "127.0.0.1", port: 0, token: TOKEN,
      state: new CollectorState(new Journal(join(dir, "items.jsonl"))), incidents: {
        async listIncidents() { return { incidents: [], coverage: { stale: false }, page: { highWater: 0, nextCursor: null, exhausted: true, order: "id_desc" } }; },
        async getIncident() { return null; },
        async fileIncident(request) { filed.push(request); return incident(request); },
        async dispatchIncident() { throw new Error("not under test"); },
        async resolveIncident() { throw new Error("not under test"); },
      getDispatchLauncher() { return null; },
      }, loadIncidentOptions: async () => optionsFixture({
        clis: [{ id: "codex", label: "Codex", models: [{ id: "gpt-5.6-sol", efforts: ["high"] }], accounts: [{ slug: "work", label: "work", ready: false, fixed: false }], permissionModes: ["safe"] }],
      }) });

    const response = await post(JSON.stringify(validBody));
    expect(response.status).toBe(400);
    expect(await response.json()).toEqual({ error: "invalid-incident-dispatch" });
    expect(filed).toEqual([]);
  });

  test("rejects a body over the collector limit", async () => {
    const response = await post("x".repeat(64 * 1024 + 1));
    expect(response.status).toBe(413);
    expect(await response.json()).toEqual({ error: "payload-too-large" });
    expect(filed).toEqual([]);
  });

  test("rejects an unknown incident type before reaching the provider", async () => {
    const response = await post(JSON.stringify({ ...validBody, incidentType: "not-in-taxonomy" }));
    expect(response.status).toBe(400);
    expect(await response.json()).toEqual({ error: "invalid-incident" });
    expect(filed).toEqual([]);
  });

  test("rejects filing when incident dispatch authority is not configured", async () => {
    server.stop(true);
    server = startServer({ host: "127.0.0.1", port: 0, token: TOKEN,
      state: new CollectorState(new Journal(join(dir, "items.jsonl"))), incidents: {
        async listIncidents() { return { incidents: [], coverage: { stale: false }, page: { highWater: 0, nextCursor: null, exhausted: true, order: "id_desc" } }; },
        async getIncident() { return null; },
        async fileIncident(request) { filed.push(request); return incident(request); },
        async dispatchIncident() { throw new Error("not under test"); },
        async resolveIncident() { throw new Error("not under test"); },
      } });

    const response = await post(JSON.stringify(validBody));
    expect(response.status).toBe(503);
    expect(await response.json()).toEqual({ error: "incident-assets-unavailable" });
    expect(filed).toEqual([]);
  });

  test("authority load failures occur before Kanboard mutation or idempotency creation", async () => {
    for (const error of [
      new IncidentOptionsError("assets", "manifest broken"),
      new IncidentOptionsError("accounts", "registry broken"),
    ]) {
      filed = [];
      server.stop(true);
      server = startServer({
        host: "127.0.0.1",
        port: 0,
        token: TOKEN,
        state: new CollectorState(new Journal(join(dir, "items.jsonl"))),
        incidents: {
          async listIncidents() { return { incidents: [], coverage: { stale: false }, page: { highWater: 0, nextCursor: null, exhausted: true, order: "id_desc" } }; },
          async getIncident() { return null; },
          async fileIncident(request) { filed.push(request); return incident(request); },
          async dispatchIncident() { throw new Error("not under test"); },
          async resolveIncident() { throw new Error("not under test"); },
      getDispatchLauncher() { return null; },
        },
        loadIncidentOptions: async () => { throw error; },
      });

      const response = await post(JSON.stringify(validBody));
      expect(response.status).toBe(503);
      expect(await response.json()).toEqual({
        error: error.kind === "accounts" ? "incident-accounts-unavailable" : "incident-assets-unavailable",
      });
      expect(filed).toEqual([]);
    }
  });
});
