import { describe, expect, it } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { FetchLike } from "../adapter";
import { createBotmasterAdapter } from "./botmaster";

const FIXTURE_DIR = join(import.meta.dir, "..", "..", "test", "fixtures", "botmaster");
const TOKEN = "test-token-abc123";
const BASE_URL = "http://127.0.0.1:8787";
const NOW_MS = 1_784_289_600_000; // 2026-07-17T12:00:00.000Z

function fixture<T>(name: string): T {
  return JSON.parse(readFileSync(join(FIXTURE_DIR, name), "utf8")) as T;
}

interface MockCall {
  url: string;
  authorization: string | null;
}

function createMockFetch(summaryFile = "summary.json") {
  const calls: MockCall[] = [];
  const summary = fixture<unknown[]>(summaryFile);

  const fetchImpl: FetchLike = async (input, init) => {
    const url = typeof input === "string" ? input : input.toString();
    const path = url.replace(BASE_URL, "");
    const headers = new Headers(init?.headers);
    calls.push({ url: path, authorization: headers.get("authorization") });

    if (path === "/api/bots/summary") {
      return new Response(JSON.stringify(summary), { status: 200 });
    }

    return new Response(JSON.stringify({ error: "not-found" }), { status: 404 });
  };

  return { fetchImpl, calls };
}

function makeAdapter(fetchImpl: FetchLike) {
  return createBotmasterAdapter({
    fetchImpl,
    baseUrl: BASE_URL,
    token: TOKEN,
    interval: 30_000,
    now: () => NOW_MS,
  });
}

describe("createBotmasterAdapter", () => {
  it("requires an injected fetch implementation", () => {
    expect(() =>
      createBotmasterAdapter({
        baseUrl: BASE_URL,
        token: TOKEN,
      } as Parameters<typeof createBotmasterAdapter>[0]),
    ).toThrow("fetchImpl is required");
  });

  it("builds a bots panel with runtime status, traffic health, msgs, errors, and cost", async () => {
    const { fetchImpl } = createMockFetch();
    const adapter = makeAdapter(fetchImpl);

    const result = await adapter.poll();
    const panel = result.panels.find((entry) => entry.id === "bots");
    expect(panel).toBeDefined();

    const data = panel!.data as { bots: Array<Record<string, unknown>> };
    expect(data.bots).toHaveLength(5);
    expect(data.bots).toEqual([
      {
        id: "alertbot",
        name: "alertbot",
        project: "alerts",
        runtimeStatus: "running",
        status: "slow",
        statusDetail: "running · slow",
        messages24h: 7,
        errors: 4,
        cost: 0.3,
        messagesTotal: 7,
        costTotal: 0.3,
        lastActivityAt: "2026-07-17T10:55:00.000Z",
        lastMessageAt: null,
      },
      {
        id: "dealbot",
        name: "dealbot",
        project: "deals",
        runtimeStatus: "running",
        status: "live",
        statusDetail: "running · live",
        messages24h: 10,
        errors: 0,
        cost: 1.9,
        messagesTotal: 10,
        costTotal: 1.9,
        lastActivityAt: "2026-07-17T11:00:00.000Z",
        lastMessageAt: null,
      },
      {
        id: "ghostbot",
        name: "ghostbot",
        project: "ghost",
        runtimeStatus: "running",
        status: "down",
        statusDetail: "running · last call 2h ago",
        messages24h: 1,
        errors: 0,
        cost: null,
        messagesTotal: 1,
        costTotal: null,
        lastActivityAt: "2026-07-17T09:59:00.000Z",
        lastMessageAt: null,
      },
      {
        id: "supportbot",
        name: "supportbot",
        project: "support",
        runtimeStatus: "running",
        status: "live",
        statusDetail: "running · live",
        messages24h: 5,
        errors: 0,
        cost: 0.7,
        messagesTotal: 5,
        costTotal: 0.7,
        lastActivityAt: "2026-07-17T11:00:00.000Z",
        lastMessageAt: null,
      },
      {
        id: "zyncbot",
        name: "zyncbot",
        project: "zync",
        runtimeStatus: "stopped",
        status: "down",
        statusDetail: "stopped",
        messages24h: 1,
        errors: 0,
        cost: null,
        messagesTotal: 1,
        costTotal: null,
        lastActivityAt: "2026-07-17T09:59:00.000Z",
        lastMessageAt: null,
      },
    ]);
  });

  it("emits an alert item for each running bot that went silent after prior activity", async () => {
    const { fetchImpl } = createMockFetch();
    const adapter = makeAdapter(fetchImpl);

    const result = await adapter.poll();
    const alerts = result.items.filter((item) => item.kind === "alert");

    expect(alerts).toHaveLength(1);
    expect(alerts[0]).toMatchObject({
      id: "botmaster:ghostbot:down",
      source: "botmaster",
      severity: "act",
      kind: "alert",
      title: "ghostbot is silent",
      detail: "last call 2h ago",
      actions: [],
    });
  });

  it("omits the down alert once the bot recovers — the reconciler resolves it", async () => {
    const first = createMockFetch("summary.json");
    const recoveredSummary = fixture<unknown[]>("summary.json").map((row) =>
      (row as { id: string }).id === "ghostbot"
        ? {
            ...(row as Record<string, unknown>),
            messages24h: 2,
            messagesTotal: 2,
            lastTs: NOW_MS - 60_000,
          }
        : row,
    );
    const recoveredFetch: FetchLike = async (input, init) => {
      const url = typeof input === "string" ? input : input.toString();
      const path = url.replace(BASE_URL, "");
      if (path === "/api/bots/summary") {
        return new Response(JSON.stringify(recoveredSummary), { status: 200 });
      }
      return new Response(JSON.stringify({ error: "not-found" }), { status: 404 });
    };
    const adapter = makeAdapter(first.fetchImpl);

    const initial = await adapter.poll();
    expect(initial.items.filter((item) => item.kind === "alert")).toHaveLength(1);

    const recoveredAdapter = makeAdapter(recoveredFetch);
    const recovered = await recoveredAdapter.poll();

    expect(recovered.items).toHaveLength(0);
    const ghostbot = (recovered.panels[0]!.data as { bots: Array<{ name: string; status: string }> }).bots.find(
      (bot) => bot.name === "ghostbot",
    );
    expect(ghostbot?.status).toBe("live");
  });

  it("poll rejects when the summary API is unreachable, so prior state is retained", async () => {
    const downFetch: FetchLike = async () => {
      throw new Error("connect ECONNREFUSED");
    };
    const adapter = makeAdapter(downFetch);

    await expect(adapter.poll()).rejects.toThrow();
  });

  it("attaches the bearer token to every API call during poll()", async () => {
    const { fetchImpl, calls } = createMockFetch();
    const adapter = makeAdapter(fetchImpl);

    await adapter.poll();

    expect(calls).toEqual([
      { url: "/api/bots/summary", authorization: `Bearer ${TOKEN}` },
    ]);
  });
});
