import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { renderDocsHeadingMap } from "../../scripts/docs-list.js";
import { restorePrepackArtifacts } from "../../scripts/openclaw-postpack.mjs";
import { preparePackageChangelog } from "../../scripts/package-changelog.mjs";
import { preparePackageDocsMap, restorePackageDocsMap } from "../../scripts/package-docs-map.mjs";
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";

const tempDirs: string[] = [];
const sourceChangelog = `# Changelog

## 2026.8.1
- Current release notes with enough detail for package validation.

## 2026.7.1
- Previous release notes with enough detail for package validation.
`;

function makePackageRoot(): string {
  const root = makeTempDir(tempDirs, "openclaw-package-docs-map-");
  mkdirSync(path.join(root, "docs"), { recursive: true });
  writeFileSync(path.join(root, "docs", "page.md"), "# Package docs\n", "utf8");
  writeFileSync(path.join(root, "package.json"), '{"name":"openclaw","version":"2026.8.1"}\n');
  writeFileSync(path.join(root, "CHANGELOG.md"), sourceChangelog);
  return root;
}

afterEach(() => {
  cleanupTempDirs(tempDirs);
});

describe("package docs map", () => {
  it("materializes exact generated bytes and removes only its transient copy", async () => {
    const root = makePackageRoot();
    const mapPath = path.join(root, "docs", "docs_map.md");

    await expect(preparePackageDocsMap(root)).resolves.toBe(true);
    expect(readFileSync(mapPath, "utf8")).toBe(renderDocsHeadingMap(path.join(root, "docs")));
    await expect(restorePackageDocsMap(root)).resolves.toBe(true);
    expect(existsSync(mapPath)).toBe(false);
  });

  it("preserves an identical map that existed before packaging", async () => {
    const root = makePackageRoot();
    const mapPath = path.join(root, "docs", "docs_map.md");
    const content = renderDocsHeadingMap(path.join(root, "docs"));
    writeFileSync(mapPath, content, "utf8");

    await expect(preparePackageDocsMap(root)).resolves.toBe(false);
    await expect(restorePackageDocsMap(root)).resolves.toBe(false);
    expect(readFileSync(mapPath, "utf8")).toBe(content);
  });

  it("restores a tracked source stub after packaging", async () => {
    const root = makePackageRoot();
    const mapPath = path.join(root, "docs", "docs_map.md");
    const stub = "# Docs map source\n";
    writeFileSync(mapPath, stub, "utf8");

    await expect(preparePackageDocsMap(root)).resolves.toBe(true);
    expect(readFileSync(mapPath, "utf8")).toBe(renderDocsHeadingMap(path.join(root, "docs")));
    await expect(restorePackageDocsMap(root)).resolves.toBe(true);
    expect(readFileSync(mapPath, "utf8")).toBe(stub);
  });

  it("serializes concurrent package preparations with the receipt", async () => {
    const root = makePackageRoot();
    const mapPath = path.join(root, "docs", "docs_map.md");
    const stub = "# Docs map source\n";
    writeFileSync(mapPath, stub, "utf8");

    const results = await Promise.allSettled([
      preparePackageDocsMap(root),
      preparePackageDocsMap(root),
    ]);

    expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1);
    const rejected = results.find((result) => result.status === "rejected");
    expect(rejected).toMatchObject({
      reason: expect.objectContaining({
        code: "PACKAGE_DOCS_MAP_ACTIVE",
        message: expect.stringContaining("node scripts/openclaw-postpack.mjs"),
      }),
    });
    expect(readFileSync(mapPath, "utf8")).toBe(renderDocsHeadingMap(path.join(root, "docs")));
    await expect(restorePackageDocsMap(root)).resolves.toBe(true);
    expect(readFileSync(mapPath, "utf8")).toBe(stub);
  });

  it("refuses to remove a transient map changed after prepack", async () => {
    const root = makePackageRoot();
    const mapPath = path.join(root, "docs", "docs_map.md");
    await preparePackageDocsMap(root);
    writeFileSync(mapPath, "operator change\n", "utf8");

    await expect(restorePackageDocsMap(root)).rejects.toThrow("changed after prepack");
    expect(readFileSync(mapPath, "utf8")).toBe("operator change\n");
  });

  it("keeps the lifecycle lock until interrupted changelog state is restored", async () => {
    const root = makePackageRoot();
    const mapPath = path.join(root, "docs", "docs_map.md");
    const receiptPath = path.join(root, ".artifacts", "package-docs-map", "receipt.json");
    const changelogBackupPath = path.join(
      root,
      ".artifacts",
      "package-changelog",
      "CHANGELOG.md.prepack-backup",
    );
    const stub = "# Docs map source\n";
    writeFileSync(mapPath, stub);

    await preparePackageDocsMap(root);
    await preparePackageChangelog(root);
    const packagedChangelog = readFileSync(path.join(root, "CHANGELOG.md"), "utf8");
    writeFileSync(path.join(root, "CHANGELOG.md"), "operator change\n");

    await expect(restorePrepackArtifacts(root)).rejects.toThrow("changed since the backup");
    expect(existsSync(receiptPath)).toBe(true);
    expect(existsSync(changelogBackupPath)).toBe(true);
    expect(readFileSync(mapPath, "utf8")).toBe(renderDocsHeadingMap(path.join(root, "docs")));

    writeFileSync(path.join(root, "CHANGELOG.md"), packagedChangelog);
    await expect(restorePrepackArtifacts(root)).resolves.toBeUndefined();
    expect(readFileSync(path.join(root, "CHANGELOG.md"), "utf8")).toBe(sourceChangelog);
    expect(readFileSync(mapPath, "utf8")).toBe(stub);
    expect(existsSync(changelogBackupPath)).toBe(false);
    expect(existsSync(receiptPath)).toBe(false);
  });
});
