/**
 * WP-10 Worker registration ≠ Host Binding separation.
 */

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

import type { Agent } from "@codeflowmu/protocol";

import { InMemorySdkAdapter } from "../../registry/AgentSdkAdapter.ts";
import {
  AgentHostRegistry,
  InMemoryHostAdapter,
  SwitchboardPaths,
  WorkerBindingService,
  WorkerIdentityService,
  WorkerRegistrationCompat,
} from "../index.ts";

async function tempPaths(): Promise<SwitchboardPaths> {
  const root = await mkdtemp(join(tmpdir(), "sb-wp10-"));
  const persistDir = join(root, ".codeflowmu", "state");
  await mkdir(persistDir, { recursive: true });
  return new SwitchboardPaths({ persistDir });
}

function sampleAgent(id = "DEV-01"): Agent {
  return {
    agent_id: id,
    role: "DEV",
    layer: "worker",
    node: "local",
    runtime: "local",
    workspace: "D:/codeflowmu-core-dev",
    skills: ["fcop"],
    status: "idle",
  };
}

describe("WP-10 registerIdentity without Host", () => {
  it("registerIdentity does not call any Host / SDK", async () => {
    const paths = await tempPaths();
    const registry = new AgentHostRegistry();
    const host = new InMemoryHostAdapter({ hostId: "host-a" });
    registry.register(host);
    const identities = new WorkerIdentityService({ paths });
    const bindings = new WorkerBindingService({
      paths,
      identities,
      hostRegistry: registry,
    });

    const before = bindings.prepareSessionAttempts;
    const { record } = await identities.registerIdentityFromAgent(sampleAgent());
    assert.equal(record.sessionOpened, false);
    assert.equal(bindings.prepareSessionAttempts, before);
    assert.equal(bindings.sdkSendAttempts, 0);

    const listed = await identities.listIdentities();
    assert.equal(listed.length, 1);
    assert.equal(await bindings.isUnbound("DEV-01"), true);
    assert.ok(await identities.getIdentity("DEV-01"));
  });
});

describe("WP-10 ensureBinding / replace / lost", () => {
  it("ensureBinding targets only the requested host; two workers may use different hosts", async () => {
    const paths = await tempPaths();
    const registry = new AgentHostRegistry();
    registry.register(new InMemoryHostAdapter({ hostId: "host-cursor" }));
    registry.register(new InMemoryHostAdapter({ hostId: "host-codex" }));
    const identities = new WorkerIdentityService({ paths });
    const bindings = new WorkerBindingService({
      paths,
      identities,
      hostRegistry: registry,
    });

    await identities.registerIdentityFromAgent(sampleAgent("DEV-01"));
    await identities.registerIdentityFromAgent(sampleAgent("QA-01"));

    const d = await bindings.ensureBinding("DEV-01", "host-cursor");
    const q = await bindings.ensureBinding("QA-01", "host-codex");
    assert.equal(d.status, "bound");
    assert.equal(q.status, "bound");
    if (d.status === "bound" && q.status === "bound") {
      assert.equal(d.revision.host.hostId, "host-cursor");
      assert.equal(q.revision.host.hostId, "host-codex");
      assert.equal(d.openedHostSession, true);
      assert.notEqual(d.revision.host.externalSessionId, "DEV-01");
      assert.notEqual(q.revision.host.externalSessionId, "QA-01");
    }
    assert.equal(bindings.prepareSessionAttempts, 2);
  });

  it("replaceBinding revises host; markBindingLost keeps identity", async () => {
    const paths = await tempPaths();
    const registry = new AgentHostRegistry();
    registry.register(new InMemoryHostAdapter({ hostId: "host-a" }));
    registry.register(new InMemoryHostAdapter({ hostId: "host-b" }));
    const identities = new WorkerIdentityService({ paths });
    const bindings = new WorkerBindingService({
      paths,
      identities,
      hostRegistry: registry,
    });
    await identities.registerIdentityFromAgent(sampleAgent());

    await bindings.ensureBinding("DEV-01", "host-a");
    const replaced = await bindings.replaceBinding(
      "DEV-01",
      "host-b",
      "migrate",
    );
    assert.equal(replaced.status, "replaced");
    if (replaced.status === "replaced") {
      assert.equal(replaced.revision.host.hostId, "host-b");
    }

    await bindings.markBindingLost("DEV-01", "host-crashed");
    assert.equal(await bindings.isUnbound("DEV-01"), true);
    const identity = await identities.getIdentity("DEV-01");
    assert.ok(identity);
    assert.equal(identity!.worker.workerId, "DEV-01");
  });

  it("host failure leaves identity; binding failure visible", async () => {
    const paths = await tempPaths();
    const registry = new AgentHostRegistry();
    const host = new InMemoryHostAdapter({ hostId: "host-bad" });
    registry.register(host);
    const original = host.prepareSession.bind(host);
    host.prepareSession = async () => {
      throw new Error("host down");
    };
    void original;

    const identities = new WorkerIdentityService({ paths });
    const bindings = new WorkerBindingService({
      paths,
      identities,
      hostRegistry: registry,
    });
    await identities.registerIdentityFromAgent(sampleAgent());

    const result = await bindings.ensureBinding("DEV-01", "host-bad");
    assert.equal(result.status, "binding_failed");
    if (result.status === "binding_failed") {
      assert.equal(result.identityPreserved, true);
      assert.match(result.reason, /host down/);
    }
    assert.ok(await identities.getIdentity("DEV-01"));
  });
});

describe("WP-10 compatible register()", () => {
  it("mirrors legacy register external shape; workerId ≠ platform session id", async () => {
    const paths = await tempPaths();
    const registry = new AgentHostRegistry();
    registry.register(new InMemoryHostAdapter({ hostId: "host-default" }));
    const sdk = new InMemorySdkAdapter();
    const compat = new WorkerRegistrationCompat({
      paths,
      defaultHostId: "host-default",
      hostRegistry: registry,
      sdk,
    });

    const result = await compat.register(sampleAgent("OPS-01"));
    assert.equal(result.workerId, "OPS-01");
    assert.equal(result.invokedSdkCreate, true);
    assert.equal(result.record.protocol.agent_id, "OPS-01");
    assert.ok(result.record.protocol.sdk_agent_id);
    // Platform mirrored id must not collapse into worker id when host prepared.
    assert.notEqual(result.platformSessionId, "OPS-01");
    assert.equal(result.binding.status, "bound");

    const unboundOk = await compat.identities.getIdentity("OPS-01");
    assert.ok(unboundOk);
  });
});
