import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, mkdir, access } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { InMemorySdkAdapter } from "../../registry/AgentSdkAdapter.ts";
import { SwitchboardPaths } from "../paths/SwitchboardPaths.ts";
import { AtomicFile } from "../atomic/AtomicFile.ts";
import {
  AgentHostRegistry,
  AgentHostRegistryError,
  InMemoryHostAdapter,
  LegacySdkHostAdapter,
  IN_MEMORY_HOST_CAPABILITIES,
  LEGACY_SDK_HOST_CAPABILITIES,
} from "../hosts/index.ts";
import type { PrepareSessionInput } from "../contracts/HostSession.ts";

const workerInput = (): PrepareSessionInput => ({
  worker: {
    workerId: "DEV-01",
    roleId: "DEV",
    layer: "worker",
  },
});

describe("WP-03 AgentHostRegistry + adapters", () => {
  it("registers multiple hosts and describes capabilities", async () => {
    const registry = new AgentHostRegistry();
    const memA = new InMemoryHostAdapter({ hostId: "host-mem-a" });
    const memB = new InMemoryHostAdapter({ hostId: "host-mem-b", enabled: false });
    const legacy = new LegacySdkHostAdapter(new InMemorySdkAdapter(), {
      hostId: "host-legacy",
    });

    registry.register(memA);
    registry.register(memB);
    registry.register(legacy);

    assert.equal(registry.has("host-mem-a"), true);
    assert.equal(registry.list().length, 3);

    const all = await registry.describeAll();
    assert.equal(all.length, 3);
    assert.ok(all.some((d) => d.adapterType === "in-memory"));
    assert.ok(all.some((d) => d.adapterType === "legacy-sdk"));

    const enabled = await registry.listEnabled();
    assert.equal(enabled.length, 2);

    const d = await registry.assertConformance("host-legacy", "H2");
    assert.equal(d.capabilities.level, "H2");
    await assert.rejects(
      () => registry.assertConformance("host-mem-a", "H3"),
      (err: unknown) => err instanceof AgentHostRegistryError,
    );
  });

  it("persists host descriptors under switchboard/hosts when paths provided", async () => {
    const root = await mkdtemp(join(tmpdir(), "wp03-reg-"));
    const persistDir = join(root, ".codeflowmu", "state");
    await mkdir(persistDir, { recursive: true });
    const paths = new SwitchboardPaths({ persistDir });
    const registry = new AgentHostRegistry({ paths });
    registry.register(new InMemoryHostAdapter({ hostId: "host-persist" }));
    await registry.flush();
    const file = paths.hostFile("host-persist");
    await access(file);
    const saved = await AtomicFile.readJson<{ hostId: string }>(file);
    assert.equal(saved.hostId, "host-persist");
  });

  it("InMemoryHostAdapter prepare/start/stream/cancel round-trip", async () => {
    const host = new InMemoryHostAdapter({ hostId: "host-mem" });
    const prepared = await host.prepareSession(workerInput());
    assert.equal(prepared.created, true);
    assert.equal(await host.inspectSession(prepared.reference), "ready");

    const run = await host.startRun(prepared.reference, {
      runId: "run-1",
      workerId: "DEV-01",
      prompt: "hello",
      deliveryAttemptId: "attempt-1",
    });
    const events = [];
    for await (const evt of host.streamEvents(run)) {
      events.push(evt.type);
    }
    assert.ok(events.includes("host.run.started"));
    assert.ok(events.includes("host.run.completed"));
    assert.equal((await host.describe()).capabilities.level, IN_MEMORY_HOST_CAPABILITIES.level);
  });

  it("LegacySdkHostAdapter wraps InMemorySdkAdapter create/send without Cursor fields on descriptor", async () => {
    const sdk = new InMemorySdkAdapter();
    const host = new LegacySdkHostAdapter(sdk, { hostId: "host-legacy-wrap" });
    assert.equal(host.underlyingSdkAdapter, sdk);

    const descriptor = await host.describe();
    assert.equal(descriptor.adapterType, "legacy-sdk");
    assert.equal(descriptor.capabilities.level, LEGACY_SDK_HOST_CAPABILITIES.level);
    assert.equal(
      JSON.stringify(descriptor).includes("sdk_agent_id"),
      false,
      "HostDescriptor must stay vendor-agnostic",
    );
    assert.equal(JSON.stringify(descriptor).toLowerCase().includes("cursor"), false);

    const prepared = await host.prepareSession(workerInput());
    assert.equal(prepared.created, true);
    assert.ok(prepared.reference.externalSessionId.startsWith("sdk-fake-"));
    assert.equal(sdk.calls.create.length, 1);

    const run = await host.startRun(prepared.reference, {
      runId: "run-legacy-1",
      workerId: "DEV-01",
      prompt: "ping",
      deliveryAttemptId: "attempt-legacy-1",
    });
    assert.ok(run.externalRunId);
    assert.equal(sdk.calls.send.length, 1);

    const collected: string[] = [];
    for await (const evt of host.streamEvents(run)) {
      collected.push(evt.type);
    }
    // InMemoryRunHandle may emit zero or more sdk events before settle; opaque/empty ok.
    assert.ok(Array.isArray(collected));
  });

  it("package root still exports CursorSdkAdapter (legacy path intact)", async () => {
    const mod = await import("../../index.ts");
    assert.equal(typeof mod.CursorSdkAdapter, "function");
    assert.equal(typeof mod.InMemorySdkAdapter, "function");
    assert.equal(typeof mod.AgentHostRegistry, "function");
    assert.equal(typeof mod.LegacySdkHostAdapter, "function");
    assert.equal(typeof mod.InMemoryHostAdapter, "function");
  });
});
