/**
 * WP-05 Shadow Worker Binding — identity + binding services.
 */

import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, mkdir, readFile } 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 { SwitchboardPaths } from "../paths/SwitchboardPaths.ts";
import {
  WorkerIdentityService,
  mapAgentToWorker,
  WorkerBindingService,
  isShadowExternalSessionId,
} from "../binding/index.ts";
import {
  WorkerIdentityService as FromSwitchboardIdentity,
  WorkerBindingService as FromSwitchboardBinding,
  mapAgentToWorker as FromSwitchboardMap,
} from "../index.ts";

async function tempPersist(): Promise<{ root: string; paths: SwitchboardPaths }> {
  const root = await mkdtemp(join(tmpdir(), "sb-wp05-"));
  const persistDir = join(root, ".codeflowmu", "state");
  await mkdir(persistDir, { recursive: true });
  return { root, paths: 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",
    labels: { team: "core" },
  };
}

describe("WP-05 Shadow Worker Binding", () => {
  it("maps Agent → WorkerIdentity", () => {
    const worker = mapAgentToWorker(sampleAgent());
    assert.equal(worker.workerId, "DEV-01");
    assert.equal(worker.roleId, "DEV");
    assert.equal(worker.layer, "worker");
    assert.equal(worker.workspaceId, "D:/codeflowmu-core-dev");
    assert.equal(worker.labels?.team, "core");
    assert.equal(FromSwitchboardMap(sampleAgent("QA-01")).workerId, "QA-01");
  });

  it("registers identity without opening Host Session (separation)", async () => {
    const { paths } = await tempPersist();
    const identities = new WorkerIdentityService({ paths, mode: "shadow" });
    const { identity, record, path } = await identities.registerFromAgent(
      sampleAgent(),
    );
    assert.equal(identity.workerId, "DEV-01");
    assert.equal(record.sessionOpened, false);
    assert.equal(record.mode, "shadow");

    const raw = await readFile(path, "utf-8");
    assert.match(raw, /codeflowmu\.worker-identity\/v1/);
    assert.match(raw, /"sessionOpened": false/);

    const loaded = await identities.get("DEV-01");
    assert.equal(loaded?.worker.workerId, "DEV-01");
    assert.equal(loaded?.sessionOpened, false);
  });

  it("allows unbound workers (identity without binding)", async () => {
    const { paths } = await tempPersist();
    const identities = new WorkerIdentityService({ paths });
    const bindings = new WorkerBindingService({ paths, mode: "shadow" });

    await identities.registerFromAgent(sampleAgent("OPS-01"));
    assert.equal(await bindings.isUnbound("OPS-01"), true);
    assert.equal(await bindings.isBound("OPS-01"), false);
    assert.equal(await bindings.latest("OPS-01"), null);
  });

  it("writes readable Shadow Binding files without sdk.send", async () => {
    const { paths } = await tempPersist();
    const sdk = new InMemorySdkAdapter();
    const sendCalls: unknown[] = [];
    const originalSend = sdk.send.bind(sdk);
    sdk.send = async (...args: Parameters<typeof sdk.send>) => {
      sendCalls.push(args);
      return originalSend(...args);
    };

    const identities = new WorkerIdentityService({ paths });
    const bindings = new WorkerBindingService({ paths, mode: "shadow" });
    await identities.registerFromAgent(sampleAgent());

    const result = await bindings.shadowBind({
      workerId: "DEV-01",
      hostId: "host-cursor",
      reason: "wp-05-shadow-sample",
    });

    assert.equal(result.openedHostSession, false);
    assert.equal(result.invokedSdkSend, false);
    assert.equal(bindings.sdkSendAttempts, 0);
    assert.equal(sendCalls.length, 0);
    assert.ok(isShadowExternalSessionId(result.revision.host.externalSessionId));

    const md = await readFile(result.path, "utf-8");
    assert.match(md, /Worker Binding/);
    assert.match(md, /"schema": "codeflowmu\.worker-binding\/v1"/);
    assert.match(md, /wp-05-shadow-sample/);

    assert.equal(await bindings.isBound("DEV-01"), true);
    const latest = await bindings.latest("DEV-01");
    assert.equal(latest?.state, "ready");
    assert.equal(latest?.host.metadata?.shadow, "true");
  });

  it("unbind closes binding and forbids openHostSession on the service", async () => {
    const { paths } = await tempPersist();
    const bindings = new WorkerBindingService({ paths, mode: "shadow" });
    await bindings.shadowBind({ workerId: "DEV-01", hostId: "host-local" });
    const unbound = await bindings.unbind("DEV-01", "test-unbind");
    assert.ok(!("alreadyUnbound" in unbound));
    assert.equal(unbound.revision.state, "closed");
    assert.equal(await bindings.isUnbound("DEV-01"), true);

    assert.throws(
      () => bindings.openHostSession(),
      /forbidden in Shadow WP-05/,
    );
  });

  it("exports services from switchboard public surface", () => {
    assert.equal(typeof FromSwitchboardIdentity, "function");
    assert.equal(typeof FromSwitchboardBinding, "function");
  });
});
