import assert from "node:assert/strict";
import test from "node:test";
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import {
  finalResponse,
  TextActivityBatcher,
  plainText,
  redactSensitive,
  summarizeTool,
} from "./device-lab-pager.ts";

test("text activity batches by character threshold without splitting Unicode", () => {
  const sent: string[] = [];
  const batch = new TextActivityBatcher((text) => sent.push(text), 10_000, 5);
  batch.add("你🙂abc");
  assert.deepEqual(sent, ["你🙂abc"]);
});

test("text activity flushes on the throttle timer", async () => {
  const sent: string[] = [];
  const batch = new TextActivityBatcher((text) => sent.push(text), 15, 100);
  batch.add("live output");
  assert.deepEqual(sent, []);
  await new Promise((resolve) => setTimeout(resolve, 30));
  assert.deepEqual(sent, ["live output"]);
});

test("plain text removes noisy markdown and redacts credentials", () => {
  const value = plainText("## Result\n**ready** with token=super-secret-value");
  assert.equal(value, "Result\nready with token=<redacted>");
  assert.equal(redactSensitive("Authorization: Bearer abc.def.ghi"), "Authorization=<redacted>");
  assert.equal(redactSensitive("Authorization: Basic dXNlcjpwYXNz"), "Authorization=<redacted>");
  assert.equal(redactSensitive("run --password private-value"), "run --password=<redacted>");
});

test("tool summaries keep only a short safe action", () => {
  assert.equal(
    summarizeTool("read", { path: "server/device_gateway/app.py", contents: "ignored" }),
    "Reading\nserver/device_gateway/app.py",
  );
  const command = summarizeTool("bash", { cmd: "curl -H Authorization:Bearer-secret https://example" });
  assert.match(command, /^Running\n/);
  assert.doesNotMatch(command, /Bearer-secret/);
});

test("final reply excludes commentary, tools and prior assistant messages", () => {
  assert.equal(finalResponse([
    {role:"assistant",content:[{type:"text",text:"Intermediate"}]},
    {role:"toolResult",content:[{type:"text",text:"tool output"}]},
    {role:"assistant",content:[{type:"thinking",thinking:"hidden"},{type:"text",text:"Final reply."}]},
  ]),"Final reply.");
  assert.equal(finalResponse([]),"");
});

test("registered Pi handlers emit live activity and settled final independently", async t => {
  const dir = mkdtempSync(join(tmpdir(), "pager-downlink-"));
  const saved = process.env.DEVICE_LAB_CREDENTIALS_FILE;
  process.env.DEVICE_LAB_CREDENTIALS_FILE = join(dir, "credentials.json");
  writeFileSync(process.env.DEVICE_LAB_CREDENTIALS_FILE, JSON.stringify({ pi_token: "test-only" }));
  const sent: any[] = [];
  t.mock.method(globalThis, "fetch", async (url: string, options: any) => {
    assert.ok(url.endsWith("/integrations/pi/events"));
    assert.equal(options.headers["x-device-lab-pi-token"], "test-only");
    sent.push(JSON.parse(options.body));
    return new Response("{}", { status: 200 });
  });
  try {
    // Fresh module gives this contract test its own token cache.
    const { default: register } = await import("./device-lab-pager.ts?downlink-test");
    const handlers = new Map<string, any>();
    register({ on: (name: string, handler: any) => handlers.set(name, handler) } as any);
    const ctx: any = { cwd: "/tmp/public-test", sessionManager: {
      getSessionId: () => "selected-session", getSessionName: () => "Public test",
    } };
    const emit = (name: string, event: any = {}) => handlers.get(name)(event, ctx);
    // A: no message_update at all. agent_end captures; agent_settled publishes once.
    await emit("agent_start");
    emit("agent_end", { messages: [{ role: "assistant", content: [{ type: "text", text: "你好！" }] }] });
    assert.deepEqual(sent.map(p => p.event), ["agent_start"]);
    await emit("agent_settled");
    await emit("agent_settled");
    assert.equal(sent.length, 2);
    assert.equal(sent[1].final_text, "你好！");
    assert.equal(sent[1].turn_id, sent[0].turn_id);
    // B: plain text, tool start/end, threshold flush, then a different final reply.
    sent.length = 0;
    await emit("agent_start");
    emit("message_update", { assistantMessageEvent: { type: "text_delta", delta: "正在检查。" } });
    assert.equal(sent.length, 1);
    await new Promise(resolve => setTimeout(resolve, 400));
    assert.equal(sent[1].activity, "正在检查。");
    emit("tool_execution_start", { toolName: "bash", args: { command: "sleep 3" } });
    emit("tool_execution_end", { toolName: "bash", isError: false });
    emit("message_update", { assistantMessageEvent: { type: "text_delta", delta: "x".repeat(120) } });
    assert.deepEqual(sent.slice(2).map(p => p.activity), ["Running\nsleep 3", "Completed\nbash", "x".repeat(120)]);
    emit("agent_end", { messages: [{ role: "assistant", content: [{ type: "text", text: "权威最终结果。" }] }] });
    await emit("agent_settled");
    assert.equal(sent.at(-1).final_text, "权威最终结果。");
    assert.ok(sent.every(p => p.session_id === "selected-session"));
    // C: failure is separate from useful final text and preserves secret redaction.
    await emit("agent_start");
    emit("agent_end", { messages: [{ role: "assistant", stopReason: "error", content: [
      { type: "text", text: "依赖不可用。token=private-value" },
    ] }] });
    await emit("agent_settled");
    assert.equal(sent.at(-1).failed, true);
    assert.equal(sent.at(-1).final_text, "依赖不可用。token=<redacted>");
    await emit("session_shutdown");
  } finally {
    if (saved === undefined) delete process.env.DEVICE_LAB_CREDENTIALS_FILE;
    else process.env.DEVICE_LAB_CREDENTIALS_FILE = saved;
    rmSync(dir, { recursive: true, force: true });
  }
});
