import { mkdtemp, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
  _resetMacroRecorder,
  getMacroRecorder,
  listMacros,
  MacroRecordSchema,
  readMacro,
  redactArgs,
  summarizeResult,
  writeMacro,
} from "../../src/automation/macroSchema.js";
import { type MacroRecorderArgs, macroRecorderImpl } from "../../src/tools/cli/macroRecorder.js";
import type { ToolContext } from "../../src/tools/types.js";

const ctx = {} as ToolContext;

async function call(args: Partial<MacroRecorderArgs> & { action: MacroRecorderArgs["action"] }) {
  const full: MacroRecorderArgs = { redactSensitive: true, allowUnsafeRecording: false, ...args };
  return macroRecorderImpl(ctx, full);
}

let tmp: string;
let prevDir: string | undefined;
let prevVault: string | undefined;

beforeEach(async () => {
  _resetMacroRecorder();
  tmp = await mkdtemp(join(tmpdir(), "tdmcp-macro-"));
  prevDir = process.env.TDMCP_MACROS_DIR;
  prevVault = process.env.TDMCP_VAULT_PATH;
  process.env.TDMCP_MACROS_DIR = tmp;
});

afterEach(() => {
  if (prevDir === undefined) delete process.env.TDMCP_MACROS_DIR;
  else process.env.TDMCP_MACROS_DIR = prevDir;
  if (prevVault === undefined) delete process.env.TDMCP_VAULT_PATH;
  else process.env.TDMCP_VAULT_PATH = prevVault;
});

describe("macro_recorder round-trip", () => {
  it("captures wrapped handler calls, writes a valid record, and round-trips", async () => {
    const start = await call({ action: "start", name: "trip", redactSensitive: true });
    expect(start.isError).toBeFalsy();

    const recorder = getMacroRecorder();
    const h1 = recorder.wrapHandler("create_audio_reactive", async (_a: unknown) => ({
      content: [{ type: "text", text: "built reactive network" }],
    }));
    await h1({ container: "/project1", style: "spectrum" });

    const h2 = recorder.wrapHandler("find_td_nodes", async (_a: unknown) => ({
      content: [{ type: "text", text: "found 3 nodes" }],
    }));
    await h2({ path: "/project1" });

    const h3 = recorder.wrapHandler("create_td_node", async (_a: unknown) => {
      throw new Error("boom");
    });
    await expect(h3({ optype: "constantTOP" })).rejects.toThrow("boom");

    const stop = await call({ action: "stop", name: "trip", redactSensitive: true });
    expect(stop.isError).toBeFalsy();
    const file = join(tmp, "trip.json");
    const record = await readMacro(file);
    expect(MacroRecordSchema.safeParse(record).success).toBe(true);
    expect(record.entries).toHaveLength(3);
    expect(record.entries[0]?.tool).toBe("create_audio_reactive");
    expect(record.entries[2]?.result_summary).toMatch(/^error: boom/);
  });
});

describe("redaction", () => {
  it("redacts execute_python_script.script to length marker", () => {
    const out = redactArgs("execute_python_script", {
      script: "import td\nprint(1)",
      target: "/project1",
    });
    expect(out.script).toBe("[redacted: 18 chars]");
    expect(out.target).toBe("/project1");
  });

  it("redacts vault paths to last two segments", () => {
    process.env.TDMCP_VAULT_PATH = "/Users/x/Vault";
    const out = redactArgs("vault_note", { path: "/Users/x/Vault/Notes/Sub/file.md" });
    expect(out.path).toBe("…/Sub/file.md");
  });

  it("redacts nested credential-like tool args by key, including short passwords", () => {
    const out = redactArgs("extend_data_source_fabric", {
      mqtt: {
        password: "pw",
        api_key: "key-123",
        apiKey: "key-456",
        stream_key: "live:abc123",
        headers: [{ Authorization: "Bearer 123:ABC" }],
      },
      token: "123:ABC",
      label: "keep",
    });

    expect(out).toMatchObject({
      mqtt: {
        password: "[redacted]",
        api_key: "[redacted]",
        apiKey: "[redacted]",
        stream_key: "[redacted]",
        headers: [{ Authorization: "[redacted]" }],
      },
      token: "[redacted]",
      label: "keep",
    });
  });

  it("rejects unredacted recordings without explicit unsafe acknowledgement", async () => {
    const result = await call({ action: "start", name: "raw", redactSensitive: false });

    expect(result.isError).toBe(true);
    const text = result.content?.[0];
    expect(text && "text" in text ? text.text : "").toContain("allowUnsafeRecording");
  });

  it("preserves script only with explicit unsafe recording acknowledgement", async () => {
    await call({
      action: "start",
      name: "raw",
      redactSensitive: false,
      allowUnsafeRecording: true,
    });
    const recorder = getMacroRecorder();
    const h = recorder.wrapHandler("execute_python_script", async () => ({
      content: [{ type: "text", text: "ok" }],
    }));
    await h({ script: "import td\nprint(1)", target: "/project1" });
    await call({
      action: "stop",
      name: "raw",
      redactSensitive: false,
      allowUnsafeRecording: true,
    });
    const record = await readMacro(join(tmp, "raw.json"));
    expect(record.entries[0]?.args.script).toBe("import td\nprint(1)");
  });
});

describe("control surface", () => {
  it("rejects double-start", async () => {
    await call({ action: "start", name: "first", redactSensitive: true });
    const second = await call({ action: "start", name: "second", redactSensitive: true });
    expect(second.isError).toBe(true);
    const text = second.content?.[0];
    expect(text && "text" in text ? text.text : "").toContain("first");
  });

  it("rejects stop without start", async () => {
    const stop = await call({ action: "stop", name: "x", redactSensitive: true });
    expect(stop.isError).toBe(true);
    const text = stop.content?.[0];
    expect(text && "text" in text ? text.text : "").toContain("no active recording");
  });

  it("list enumerates only .json files", async () => {
    await writeFile(join(tmp, "a.json"), "{}", "utf8");
    await writeFile(join(tmp, "b.txt"), "x", "utf8");
    const r = await call({ action: "list", redactSensitive: true });
    expect(r.isError).toBeFalsy();
    const sc = r.structuredContent as { macros: string[] };
    expect(sc.macros).toEqual(["a.json"]);
  });

  it("load rejects an invalid macro file", async () => {
    const bad = join(tmp, "bad.json");
    await writeFile(bad, JSON.stringify({ name: "x", entries: [] }), "utf8");
    const r = await call({ action: "load", file: bad, redactSensitive: true });
    expect(r.isError).toBe(true);
    const text = r.content?.[0];
    expect(text && "text" in text ? text.text : "").toContain("schema_version");
  });

  it("does not record macro_recorder self-calls", async () => {
    await call({ action: "start", name: "self", redactSensitive: true });
    const recorder = getMacroRecorder();
    const h = recorder.wrapHandler("macro_recorder", async () => ({
      content: [{ type: "text", text: "ok" }],
    }));
    await h({ action: "list" });
    const stop = await call({ action: "stop", name: "self", redactSensitive: true });
    expect(stop.isError).toBeFalsy();
    const record = await readMacro(join(tmp, "self.json"));
    expect(record.entries).toHaveLength(0);
  });

  it("clips long result summaries to a single line ≤240 chars", () => {
    const long = `${"x".repeat(500)}\nmore\nlines`;
    const out = summarizeResult({ content: [{ type: "text", text: long }] });
    expect(out).toBeDefined();
    expect((out as string).length).toBeLessThanOrEqual(240);
    expect(out as string).not.toMatch(/[\r\n]/);
  });
});

describe("helpers", () => {
  it("writeMacro + listMacros round-trip", async () => {
    await writeMacro(join(tmp, "h.json"), {
      schema_version: 1,
      name: "h",
      created_at: new Date().toISOString(),
      tdmcp_version: "0.0.0",
      entries: [],
    });
    const macros = await listMacros(tmp);
    expect(macros).toContain("h.json");
    const raw = JSON.parse(await readFile(join(tmp, "h.json"), "utf8"));
    expect(raw.schema_version).toBe(1);
  });
});
