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 type { FetchLike } from "./adapter";
import { Journal } from "./journal";
import { CollectorState } from "./state";
import { startServer } from "./server";
import { RegistryUnavailableError, type BuildboxRegistry, type RegistryHost } from "./buildbox-registry";

const TOKEN = "collector-logs-token";
const CONTROLLER_TOKEN = "controller-logs-token";
const CONTROLLER_URL = "http://127.0.0.1:8787";

function registryHost(name: string, state: RegistryHost["state"]): RegistryHost {
  return {
    name,
    ssh_alias: name,
    state,
    machine_id: null,
    roles: ["builder"],
    access: {},
    rustdesk: null,
    notes: "fixture",
  };
}

function registry(...hosts: RegistryHost[]): BuildboxRegistry {
  return { schema_version: 1, source: "test-fixture", orders: { build: [] }, hosts };
}

const REGISTRY = registry(registryHost("debian1", "reachable"));

describe("GET /hosts/:hostname/logs proxy", () => {
  let server: Server<undefined> | undefined;
  let dir: string;
  let state: CollectorState;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), "collector-host-logs-"));
    state = new CollectorState(new Journal(join(dir, "items.jsonl")));
  });

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

  test("proxies to controller with bearer token and returns JSON body", async () => {
    const requests: Array<{ url: string; init?: RequestInit }> = [];
    const fetcher: FetchLike = async (input, init) => {
      requests.push({ url: String(input), init });
      return new Response(JSON.stringify({
        host: "debian1",
        lines: ["line-1"],
        truncated: false,
      }), {
        status: 200,
        headers: { "content-type": "application/json" },
      });
    };

    server = startServer({
      host: "127.0.0.1",
      port: 0,
      token: TOKEN,
      state,
      controllerUrl: CONTROLLER_URL,
      controllerToken: CONTROLLER_TOKEN,
      fetcher,
      loadBuildboxRegistry: async () => REGISTRY,
    });
    const origin = `http://127.0.0.1:${server.port}`;

    expect((await fetch(`${origin}/hosts/debian1/logs`)).status).toBe(401);

    const response = await fetch(`${origin}/hosts/debian1/logs`, {
      headers: { authorization: `Bearer ${TOKEN}` },
    });
    expect(response.status).toBe(200);
    expect(await response.json()).toEqual({
      host: "debian1",
      lines: ["line-1"],
      truncated: false,
    });
    expect(requests).toEqual([{
      url: `${CONTROLLER_URL}/hosts/debian1/logs`,
      init: { headers: { authorization: `Bearer ${CONTROLLER_TOKEN}` } },
    }]);
  });

  test("returns 503 when controller connection is not configured", async () => {
    server = startServer({
      host: "127.0.0.1",
      port: 0,
      token: TOKEN,
      state,
    });
    const origin = `http://127.0.0.1:${server.port}`;

    const response = await fetch(`${origin}/hosts/debian1/logs`, {
      headers: { authorization: `Bearer ${TOKEN}` },
    });
    expect(response.status).toBe(503);
    expect(await response.json()).toEqual({ error: "offload controller unavailable" });
  });

  test("refuses a registry-disabled host without contacting the controller", async () => {
    const requests: string[] = [];
    server = startServer({
      host: "127.0.0.1",
      port: 0,
      token: TOKEN,
      state,
      controllerUrl: CONTROLLER_URL,
      controllerToken: CONTROLLER_TOKEN,
      fetcher: async (input) => {
        requests.push(String(input));
        return new Response("unexpected");
      },
      loadBuildboxRegistry: async () => registry(registryHost("debian9", "bricked")),
    });
    const response = await fetch(`http://127.0.0.1:${server.port}/hosts/debian9/logs`, {
      headers: { authorization: `Bearer ${TOKEN}` },
    });
    expect(response.status).toBe(409);
    expect(await response.json()).toEqual({ error: "host-declared-bricked" });
    expect(requests).toEqual([]);
  });

  test("refuses a host the registry does not declare", async () => {
    const requests: string[] = [];
    server = startServer({
      host: "127.0.0.1",
      port: 0,
      token: TOKEN,
      state,
      controllerUrl: CONTROLLER_URL,
      controllerToken: CONTROLLER_TOKEN,
      fetcher: async (input) => {
        requests.push(String(input));
        return new Response("unexpected");
      },
      loadBuildboxRegistry: async () => REGISTRY,
    });
    const response = await fetch(`http://127.0.0.1:${server.port}/hosts/debian7/logs`, {
      headers: { authorization: `Bearer ${TOKEN}` },
    });
    expect(response.status).toBe(404);
    expect(await response.json()).toEqual({ error: "host-not-in-buildbox-registry" });
    expect(requests).toEqual([]);
  });

  test("fails closed when the registry cannot be read", async () => {
    const requests: string[] = [];
    server = startServer({
      host: "127.0.0.1",
      port: 0,
      token: TOKEN,
      state,
      controllerUrl: CONTROLLER_URL,
      controllerToken: CONTROLLER_TOKEN,
      fetcher: async (input) => {
        requests.push(String(input));
        return new Response("unexpected");
      },
      loadBuildboxRegistry: async () => {
        throw new RegistryUnavailableError("no such file");
      },
    });
    const response = await fetch(`http://127.0.0.1:${server.port}/hosts/debian1/logs`, {
      headers: { authorization: `Bearer ${TOKEN}` },
    });
    expect(response.status).toBe(503);
    expect(await response.json()).toEqual({
      error: "buildbox-registry-unavailable",
      detail: "buildbox registry unavailable: no such file",
    });
    expect(requests).toEqual([]);
  });
});

describe("GET /activity", () => {
  let server: Server<undefined> | undefined;
  let dir: string;
  let state: CollectorState;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), "collector-activity-route-"));
    state = new CollectorState(new Journal(join(dir, "items.jsonl")));
  });

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

  test("returns activity response shape", async () => {
    server = startServer({
      host: "127.0.0.1",
      port: 0,
      token: TOKEN,
      state,
    });
    const origin = `http://127.0.0.1:${server.port}`;

    const response = await fetch(`${origin}/activity`, {
      headers: { authorization: `Bearer ${TOKEN}` },
    });
    const body = await response.json();

    expect(response.status).toBe(200);
    expect(body).toMatchObject({
      limit: 500,
      truncated: expect.any(Boolean),
      total: expect.any(Number),
      events: expect.any(Array),
      coverage: expect.any(Array),
      suppressedNotifications: expect.any(Object),
    });
  });

  test("returns 400 for invalid from", async () => {
    server = startServer({
      host: "127.0.0.1",
      port: 0,
      token: TOKEN,
      state,
    });
    const origin = `http://127.0.0.1:${server.port}`;

    const response = await fetch(`${origin}/activity?from=not-a-date`, {
      headers: { authorization: `Bearer ${TOKEN}` },
    });

    expect(response.status).toBe(400);
    expect(await response.json()).toEqual({ error: "invalid from: not-a-date" });
  });

  test("returns 400 for invalid limit", async () => {
    server = startServer({
      host: "127.0.0.1",
      port: 0,
      token: TOKEN,
      state,
    });
    const origin = `http://127.0.0.1:${server.port}`;

    const response = await fetch(`${origin}/activity?limit=0`, {
      headers: { authorization: `Bearer ${TOKEN}` },
    });
    expect(response.status).toBe(400);
    expect(await response.json()).toEqual({ error: "invalid limit: 0" });
  });

  test("returns 400 for unknown category", async () => {
    server = startServer({
      host: "127.0.0.1",
      port: 0,
      token: TOKEN,
      state,
    });
    const origin = `http://127.0.0.1:${server.port}`;

    const response = await fetch(`${origin}/activity?category=bogus`, {
      headers: { authorization: `Bearer ${TOKEN}` },
    });
    expect(response.status).toBe(400);
    expect(await response.json()).toEqual({ error: "invalid category filter" });
  });

  test("serves per-source entries and rejects an unknown source", async () => {
    server = startServer({
      host: "127.0.0.1",
      port: 0,
      token: TOKEN,
      state,
    });
    const origin = `http://127.0.0.1:${server.port}`;

    const ok = await fetch(`${origin}/activity/sources/actions/entries?limit=5`, {
      headers: { authorization: `Bearer ${TOKEN}` },
    });
    expect(ok.status).toBe(200);
    expect(await ok.json()).toMatchObject({
      source: expect.objectContaining({ id: "actions" }),
      events: expect.any(Array),
      total: expect.any(Number),
      offset: 0,
      limit: 5,
      skipped: expect.any(Array),
      skippedTotal: expect.any(Number),
      skippedTruncated: expect.any(Boolean),
    });

    const unknown = await fetch(`${origin}/activity/sources/nope/entries`, {
      headers: { authorization: `Bearer ${TOKEN}` },
    });
    expect(unknown.status).toBe(404);
    expect(await unknown.json()).toMatchObject({ error: "unknown-source" });

    const badOffset = await fetch(`${origin}/activity/sources/actions/entries?offset=-1`, {
      headers: { authorization: `Bearer ${TOKEN}` },
    });
    expect(badOffset.status).toBe(400);
    expect(await badOffset.json()).toEqual({ error: "invalid offset: -1" });
  });
});
