import { afterEach, describe, expect, test } from "bun:test";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { loadConfig, persistProjectColors, ProjectColorSchema } from "../src/config";
import { setAtomicWriteHooksForTest } from "../src/atomic-write";
import { Journal } from "../src/journal";
import { startServer, type CollectorServer } from "../src/server";
import { CollectorState } from "../src/state";

const TOKEN = "project-colors-test-token";
let server: CollectorServer | undefined;
let directory: string | undefined;

function start(configToml: string): { baseUrl: string; configPath: string } {
  directory = mkdtempSync(join(tmpdir(), "overdeck-project-colors-"));
  const configPath = join(directory, "config.toml");
  writeFileSync(configPath, configToml);
  const state = new CollectorState(new Journal(join(directory, "items.jsonl")));
  server = startServer({
    host: "127.0.0.1",
    port: 0,
    token: TOKEN,
    state,
    config: loadConfig(configPath),
    configPath,
  });
  return { baseUrl: `http://127.0.0.1:${server.port}`, configPath };
}

async function request(baseUrl: string, path: string, init: RequestInit = {}): Promise<Response> {
  return fetch(`${baseUrl}${path}`, {
    ...init,
    headers: { authorization: `Bearer ${TOKEN}`, ...init.headers },
  });
}

afterEach(() => {
  server?.stop(true);
  server = undefined;
  if (directory) rmSync(directory, { recursive: true, force: true });
  directory = undefined;
  setAtomicWriteHooksForTest();
});

describe("project color configuration routes", () => {
  test("GET returns the configured project map", async () => {
    const { baseUrl } = start('projectColors = { api = "#abc", web = "rebeccapurple" }\n');
    const response = await request(baseUrl, "/config/projects");
    expect(response.status).toBe(200);
    expect(await response.json()).toEqual({ projects: { api: "#abc", web: "rebeccapurple" } });
  });

  test("project color routes remain bearer-authenticated", async () => {
    const { baseUrl } = start("");
    const response = await fetch(`${baseUrl}/config/projects`);
    expect(response.status).toBe(401);
  });

  test("POST persists project colors for a fresh config read", async () => {
    const { baseUrl, configPath } = start("");
    const response = await request(baseUrl, "/config/projects", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ projects: { api: "#12Ab9f", web: "blue" } }),
    });
    expect(response.status).toBe(200);
    expect(await response.json()).toEqual({ projects: { api: "#12Ab9f", web: "blue" }, durability: "durable" });
    expect(loadConfig(configPath).projectColors).toEqual({ api: "#12Ab9f", web: "blue" });
  });

  test("accepts only plain named or three/six digit CSS color tokens", () => {
    for (const color of ["#AbC", "#12Ab9f", "ReBeccaPurple"]) expect(ProjectColorSchema.safeParse(color).success).toBe(true);
    for (const color of ["#abcd", "#12345678", "#12", "rgb(1,2,3)", "url(x)", "var(--x)", "red; color: blue", " red", "unknown", 3]) {
      expect(ProjectColorSchema.safeParse(color).success).toBe(false);
    }
  });

  test("rejects invalid colors without changing state", async () => {
    const { baseUrl, configPath } = start('projectColors = { api = "#abc" }\n');
    const response = await request(baseUrl, "/config/projects", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ projects: { api: "url(javascript:alert(1))" } }),
    });
    expect(response.status).toBe(400);
    expect(await response.json()).toEqual({ error: "invalid-project-colors" });
    expect(loadConfig(configPath).projectColors).toEqual({ api: "#abc" });
    expect(await (await request(baseUrl, "/config/projects")).json()).toEqual({ projects: { api: "#abc" } });
  });

  test("POST retains unrelated TOML source bytes", async () => {
    const source = '# leading comment\nport = 4999 # unusual spacing stays\nratio = 1.0\ncustomFeature = "keep"\nprojectColors = { old = "red" }\n[adapters.custom]\nenabled = false\nfutureSetting = "retain"\n# trailing comment\n';
    const { baseUrl, configPath } = start(source);
    const response = await request(baseUrl, "/config/projects", {
      method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ projects: { new: "#fff" } }),
    });
    expect(response.status).toBe(200);
    expect(readFileSync(configPath, "utf8")).toBe(source.replace('projectColors = { old = "red" }', 'projectColors = { "new" = "#fff" }'));
  });

  test("POST replaces root inline literal colors while retaining layout and comments", async () => {
    const source = 'port = 4999\r\nprojectColors   =   { api = \'#abc\' }   # palette\r\ntailnetBind = false\r\n';
    const { baseUrl, configPath } = start(source);
    const projects = { api: "#12Ab9f", web: "blue" };
    const response = await request(baseUrl, "/config/projects", {
      method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ projects }),
    });
    expect(response.status).toBe(200);
    expect(await response.json()).toEqual({ projects, durability: "durable" });
    expect(readFileSync(configPath, "utf8")).toBe('port = 4999\r\nprojectColors   =   { "api" = "#12Ab9f", "web" = "blue" }   # palette\r\ntailnetBind = false\r\n');
    expect(loadConfig(configPath).projectColors).toEqual(projects);
  });

  test("replaces a quoted top-level projectColors key without creating a duplicate", async () => {
    const source = '"projectColors" = { old = "red" } # quoted key\n';
    const { baseUrl, configPath } = start(source);
    const response = await request(baseUrl, "/config/projects", {
      method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ projects: { api: "blue" } }),
    });
    expect(response.status).toBe(200);
    expect(readFileSync(configPath, "utf8")).toBe('"projectColors" = { "api" = "blue" } # quoted key\n');
    expect(loadConfig(configPath).projectColors).toEqual({ api: "blue" });
  });

  test("adds the first entry to a final empty table without a trailing newline", async () => {
    const { baseUrl, configPath } = start("[projectColors]");
    const response = await request(baseUrl, "/config/projects", {
      method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ projects: { api: "blue" } }),
    });
    expect(response.status).toBe(200);
    expect(readFileSync(configPath, "utf8")).toBe('[projectColors]\n"api" = "blue"\n');
    expect(loadConfig(configPath).projectColors).toEqual({ api: "blue" });
  });

  test("updates table-form project colors without rewriting adjacent tables", async () => {
    const { baseUrl, configPath } = start('[projectColors]\nold = "red"\n[adapters.custom]\nenabled = false\n');
    const response = await request(baseUrl, "/config/projects", {
      method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ projects: { api: "blue" } }),
    });
    expect(response.status).toBe(200);
    expect(readFileSync(configPath, "utf8")).toBe('[projectColors]\n\n"api" = "blue"\n[adapters.custom]\nenabled = false\n');
  });

  test("removes omitted literal-string table colors while retaining their comments", async () => {
    const source = '[projectColors] # colors\n  old = \'red\' # retired\n\n[adapters.custom]\nenabled = false\n';
    const { baseUrl, configPath } = start(source);
    const response = await request(baseUrl, "/config/projects", {
      method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ projects: { api: "blue" } }),
    });
    expect(response.status).toBe(200);
    expect(await response.json()).toEqual({ projects: { api: "blue" }, durability: "durable" });
    expect(readFileSync(configPath, "utf8")).toBe('[projectColors] # colors\n  # retired\n\n"api" = "blue"\n[adapters.custom]\nenabled = false\n');
    expect(loadConfig(configPath).projectColors).toEqual({ api: "blue" });
  });

  test("retains comments and formatting inside a projectColors table", async () => {
    const source = '[projectColors] # header\r\n  # leading\r\n  "keep"   =   "red"   # keep note\r\n\r\n  old = "blue" # old note\r\n  # trailing\r\n[adapters.custom]\r\nenabled = false\r\n';
    const { baseUrl, configPath } = start(source);
    const response = await request(baseUrl, "/config/projects", {
      method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ projects: { keep: "#ABC", added: "rebeccapurple" } }),
    });
    expect(response.status).toBe(200);
    expect(readFileSync(configPath, "utf8")).toBe('[projectColors] # header\r\n  # leading\r\n  "keep"   =   "#ABC"   # keep note\r\n\r\n  # old note\r\n  # trailing\r\n"added" = "rebeccapurple"\r\n[adapters.custom]\r\nenabled = false\r\n');
    expect(loadConfig(configPath).projectColors).toEqual({ keep: "#ABC", added: "rebeccapurple" });
  });

  test("serializes overlapping writes and retains a concurrent writer's unrelated source", async () => {
    const { configPath } = start('# retained\nport = 4999\n');
    let changed = false;
    setAtomicWriteHooksForTest({ beforeCompare(target) {
      if (!changed) {
        changed = true;
        writeFileSync(target, '# retained\nport = 4999\nexternal = "keep"\n');
      }
    } });
    const [first, second] = await Promise.all([
      persistProjectColors(configPath, { first: "red" }),
      persistProjectColors(configPath, { second: "blue" }),
    ]);
    expect(first.projects).toEqual({ first: "red" });
    expect(second.projects).toEqual({ second: "blue" });
    const source = readFileSync(configPath, "utf8");
    expect(source).toContain('external = "keep"');
    expect(loadConfig(configPath).projectColors).toEqual({ second: "blue" });
  });

  test("treats a post-rename directory fsync failure as committed", async () => {
    const { baseUrl, configPath } = start("");
    setAtomicWriteHooksForTest({ syncDirectory: async () => { throw new Error("directory fsync failed"); } });
    const response = await request(baseUrl, "/config/projects", {
      method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ projects: { api: "red" } }),
    });
    expect(response.status).toBe(200);
    expect(await response.json()).toEqual({ projects: { api: "red" }, durability: "indeterminate" });
    expect(loadConfig(configPath).projectColors).toEqual({ api: "red" });
    expect(await (await request(baseUrl, "/config/projects")).json()).toEqual({ projects: { api: "red" } });
  });
});
