import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
  buildConfigDocBaseline,
  collectConfigDocBaselineEntries,
  dedupeConfigDocBaselineEntries,
  normalizeConfigDocBaselineHelpPath,
  renderConfigDocBaselineStatefile,
  writeConfigDocBaselineStatefile,
} from "./doc-baseline.js";

describe("config doc baseline", () => {
  const tempRoots: string[] = [];

  afterEach(async () => {
    await Promise.all(
      tempRoots.splice(0).map(async (tempRoot) => {
        await fs.rm(tempRoot, { recursive: true, force: true });
      }),
    );
  });

  it("is deterministic across repeated runs", async () => {
    const first = await renderConfigDocBaselineStatefile();
    const second = await renderConfigDocBaselineStatefile();

    expect(second.json).toBe(first.json);
    expect(second.jsonl).toBe(first.jsonl);
  });

  it("normalizes array and record paths to wildcard form", async () => {
    const baseline = await buildConfigDocBaseline();
    const paths = new Set(baseline.entries.map((entry) => entry.path));

    expect(paths.has("session.sendPolicy.rules.*.match.keyPrefix")).toBe(true);
    expect(paths.has("env.*")).toBe(true);
    expect(normalizeConfigDocBaselineHelpPath("agents.list[].skills")).toBe("agents.list.*.skills");
  });

  it("includes core, channel, and plugin config metadata", async () => {
    const baseline = await buildConfigDocBaseline();
    const byPath = new Map(baseline.entries.map((entry) => [entry.path, entry]));

    expect(byPath.get("gateway.auth.token")).toMatchObject({
      kind: "core",
      sensitive: true,
    });
    expect(byPath.get("channels.telegram.botToken")).toMatchObject({
      kind: "channel",
      sensitive: true,
    });
    expect(byPath.get("plugins.entries.voice-call.config.twilio.authToken")).toMatchObject({
      kind: "plugin",
      sensitive: true,
    });
  });

  it("preserves help text and tags from merged schema hints", async () => {
    const baseline = await buildConfigDocBaseline();
    const byPath = new Map(baseline.entries.map((entry) => [entry.path, entry]));
    const tokenEntry = byPath.get("gateway.auth.token");

    expect(tokenEntry?.help).toContain("gateway access");
    expect(tokenEntry?.tags).toContain("auth");
    expect(tokenEntry?.tags).toContain("security");
  });

  it("matches array help hints that still use [] notation", async () => {
    const baseline = await buildConfigDocBaseline();
    const byPath = new Map(baseline.entries.map((entry) => [entry.path, entry]));

    expect(byPath.get("session.sendPolicy.rules.*.match.keyPrefix")).toMatchObject({
      help: expect.stringContaining("prefer rawKeyPrefix when exact full-key matching is required"),
      sensitive: false,
    });
  });

  it("walks union branches for nested config keys", async () => {
    const baseline = await buildConfigDocBaseline();
    const byPath = new Map(baseline.entries.map((entry) => [entry.path, entry]));

    expect(byPath.get("bindings.*")).toMatchObject({
      hasChildren: true,
    });
    expect(byPath.get("bindings.*.type")).toBeDefined();
    expect(byPath.get("bindings.*.match.channel")).toBeDefined();
    expect(byPath.get("bindings.*.match.peer.id")).toBeDefined();
  });

  it("merges tuple item metadata instead of dropping earlier entries", () => {
    const entries = dedupeConfigDocBaselineEntries(
      collectConfigDocBaselineEntries(
        {
          type: "array",
          items: [
            {
              type: "string",
              enum: ["alpha"],
            },
            {
              type: "number",
              enum: [42],
            },
          ],
        },
        {},
        "tupleValues",
      ),
    );
    const tupleEntry = new Map(entries.map((entry) => [entry.path, entry])).get("tupleValues.*");

    expect(tupleEntry).toMatchObject({
      type: ["number", "string"],
    });
    expect(tupleEntry?.enumValues).toEqual(expect.arrayContaining([42, "alpha"]));
    expect(tupleEntry?.enumValues).toHaveLength(2);
  });

  it("supports check mode for stale generated artifacts", async () => {
    const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-doc-baseline-"));
    tempRoots.push(tempRoot);

    const initial = await writeConfigDocBaselineStatefile({
      repoRoot: tempRoot,
      jsonPath: "docs/.generated/config-baseline.json",
      statefilePath: "docs/.generated/config-baseline.jsonl",
    });
    expect(initial.wrote).toBe(true);

    const current = await writeConfigDocBaselineStatefile({
      repoRoot: tempRoot,
      jsonPath: "docs/.generated/config-baseline.json",
      statefilePath: "docs/.generated/config-baseline.jsonl",
      check: true,
    });
    expect(current.changed).toBe(false);

    await fs.writeFile(
      path.join(tempRoot, "docs/.generated/config-baseline.json"),
      '{"generatedBy":"broken","entries":[]}\n',
      "utf8",
    );
    await fs.writeFile(
      path.join(tempRoot, "docs/.generated/config-baseline.jsonl"),
      '{"recordType":"meta","generatedBy":"broken","totalPaths":0}\n',
      "utf8",
    );

    const stale = await writeConfigDocBaselineStatefile({
      repoRoot: tempRoot,
      jsonPath: "docs/.generated/config-baseline.json",
      statefilePath: "docs/.generated/config-baseline.jsonl",
      check: true,
    });
    expect(stale.changed).toBe(true);
    expect(stale.wrote).toBe(false);
  });
});
