import assert from "node:assert/strict";
import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
import test from "node:test";

import { parsePrReference, resolvePrToVideoProjectDir } from "./project-dir.mjs";
import { buildFramePackets } from "./frame-packets.mjs";
import { hasCliCommand } from "./preflight.mjs";

function write(path, contents) {
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, contents);
}

test("default project directory is durable and outside the caller repository", () => {
  const caller = mkdtempSync(join(tmpdir(), "p2v-caller-"));
  const cache = mkdtempSync(join(tmpdir(), "p2v-cache-"));
  const result = resolvePrToVideoProjectDir({
    pr: "https://github.com/EveryInc/compound-engineering-plugin/pull/1092",
    cwd: caller,
    env: { XDG_CACHE_HOME: cache, HOME: homedir() },
  });

  assert.equal(
    result,
    join(
      cache,
      "hyperframes",
      "pr-to-video",
      "everyinc",
      "compound-engineering-plugin",
      "compound-engineering-plugin-pr-1092",
    ),
  );
  assert.ok(isAbsolute(result));
  assert.ok(relative(caller, result).startsWith(".."));
});

test("explicit project directory is preserved exactly after absolute resolution", () => {
  const caller = mkdtempSync(join(tmpdir(), "p2v-explicit-caller-"));
  assert.equal(
    resolvePrToVideoProjectDir({
      pr: "EveryInc/compound-engineering-plugin#1092",
      cwd: caller,
      explicitDir: "../my-video",
      env: {},
    }),
    resolve(caller, "../my-video"),
  );
});

test("distinct owner and repository segments cannot collide in the durable cache", () => {
  const cache = mkdtempSync(join(tmpdir(), "p2v-cache-collision-"));
  const first = resolvePrToVideoProjectDir({
    pr: "foo-bar/baz#1",
    env: { XDG_CACHE_HOME: cache },
  });
  const second = resolvePrToVideoProjectDir({
    pr: "foo/bar-baz#1",
    env: { XDG_CACHE_HOME: cache },
  });

  assert.notEqual(first, second);
});

test("PR parsing sanitizes owner and repository path traversal", () => {
  assert.deepEqual(
    parsePrReference("https://github.com/EveryInc/compound-engineering-plugin/pull/1092"),
    {
      owner: "everyinc",
      repo: "compound-engineering-plugin",
      number: 1092,
    },
  );
  assert.throws(() => parsePrReference("../../outside#1092"), /valid GitHub PR reference/i);
});

test("#1092 packets contain selected excerpts but never the full diff", () => {
  const project = mkdtempSync(join(tmpdir(), "p2v-packets-"));
  const largeDiff = `diff --git a/noise b/noise\n${"+unselected noise\n".repeat(10_000)}`;
  write(join(project, "capture", "diff.patch"), largeDiff);
  write(join(project, "frame.md"), "# compact frame tokens\n");
  write(
    join(project, "STORYBOARD.md"),
    `---\nformat: 1920x1080\n---\n\n## Frame 1 — Diff\n\n- duration: 4s\n- src: compositions/frames/01-diff.html\n- focal: code-diff\n- blueprint: compose\n- rules: text-reveal\n\n### Source excerpt\n\n\`\`\`diff\n-oldCall()\n+newCall({ attested: true })\n\`\`\`\n\n## Frame 2 — Impact\n\n- duration: 3s\n- src: compositions/frames/02-impact.html\n- blueprint: number-lockup\n- rules: counting-dynamic-scale\n`,
  );

  const result = buildFramePackets({
    projectDir: project,
    storyboardPath: join(project, "STORYBOARD.md"),
    outDir: join(project, ".hyperframes", "frame-packets"),
    maxPacketBytes: 32_000,
  });

  assert.equal(result.length, 2);
  const codePacket = readFileSync(result[0].path, "utf8");
  assert.match(codePacket, /newCall\(\{ attested: true \}\)/);
  assert.doesNotMatch(codePacket, /unselected noise/);
  assert.doesNotMatch(codePacket, /code-scroll/);
  assert.ok(Buffer.byteLength(codePacket) < 32_000);
  assert.ok(result.every((packet) => packet.path.endsWith(".md")));

  const role = readFileSync(join(project, ".hyperframes", "frame-packets", "_role.md"), "utf8");
  assert.match(role, /# Frame worker — core contract/);
  assert.match(role, /# Frame worker — PR-to-video delta/);
});

test("packet validation is atomic and leaves no partial output on overflow", () => {
  const project = mkdtempSync(join(tmpdir(), "p2v-packets-atomic-"));
  const outDir = join(project, ".hyperframes", "frame-packets");
  write(join(project, "frame.md"), "# frame\n");
  write(
    join(project, "STORYBOARD.md"),
    `---\nformat: 1920x1080\n---\n\n## Frame 1 — Intro\n\n- duration: 2s\n- src: compositions/frames/01-intro.html\n\n## Frame 2 — Diff\n\n- duration: 4s\n- src: compositions/frames/02-diff.html\n- focal: code-diff\n\n### Source excerpt\n\n\`\`\`diff\n${"+oversized line\n".repeat(300)}\`\`\`\n`,
  );

  assert.throws(
    () => buildFramePackets({ projectDir: project, outDir, maxPacketBytes: 2_000 }),
    /limit 2000/,
  );
  assert.equal(existsSync(outDir), false);
});

test("code frames without an upstream-selected excerpt fail before dispatch", () => {
  const project = mkdtempSync(join(tmpdir(), "p2v-packets-missing-"));
  write(join(project, "frame.md"), "# frame\n");
  write(
    join(project, "STORYBOARD.md"),
    `---\nformat: 1920x1080\n---\n\n## Frame 1 — Diff\n\n- duration: 4s\n- src: compositions/frames/01-diff.html\n- focal: code-diff\n`,
  );

  assert.throws(
    () =>
      buildFramePackets({
        projectDir: project,
        storyboardPath: join(project, "STORYBOARD.md"),
        outDir: join(project, ".hyperframes", "frame-packets"),
      }),
    /Source excerpt/i,
  );
});

// The other half of this skill's frame-packets delta. The excerpt guard above is
// pinned; the code-vocabulary injection was not — deleting `codeVocabularySection`
// outright left all 455 skills tests green, so the section a code worker reads to
// pick its registry block could have been dropped silently.
test("a code frame carries the vocabulary excerpt for the block it names", () => {
  const project = mkdtempSync(join(tmpdir(), "p2v-packets-vocab-"));
  write(join(project, "frame.md"), "# frame\n");
  write(
    join(project, "STORYBOARD.md"),
    `---\nformat: 1920x1080\n---\n\n## Frame 1 — Diff\n\n- duration: 4s\n- src: compositions/frames/01-diff.html\n- focal: code-diff\n\n### Source excerpt\n\n\`\`\`diff\n-oldCall()\n+newCall()\n\`\`\`\n`,
  );

  const [packet] = buildFramePackets({ projectDir: project });
  const contents = readFileSync(packet.path, "utf8");

  assert.match(contents, /## Code block excerpt \(code-diff\)/);
  assert.match(contents, /`code-diff`/);
});

test("a code block the vocabulary does not describe still names itself for install", () => {
  const project = mkdtempSync(join(tmpdir(), "p2v-packets-vocab-miss-"));
  write(join(project, "frame.md"), "# frame\n");
  write(
    join(project, "STORYBOARD.md"),
    `---\nformat: 1920x1080\n---\n\n## Frame 1 — Diff\n\n- duration: 4s\n- src: compositions/frames/01-diff.html\n- focal: code-not-in-the-vocabulary\n\n### Source excerpt\n\n\`\`\`diff\n-oldCall()\n+newCall()\n\`\`\`\n`,
  );

  const [packet] = buildFramePackets({ projectDir: project });

  assert.match(
    readFileSync(packet.path, "utf8"),
    /## Code block\n\nUse registry block `code-not-in-the-vocabulary`\./,
  );
});

test("a mechanism frame gets no code-block section at all", () => {
  const project = mkdtempSync(join(tmpdir(), "p2v-packets-mechanism-"));
  write(join(project, "frame.md"), "# frame\n");
  write(
    join(project, "STORYBOARD.md"),
    `---\nformat: 1920x1080\n---\n\n## Frame 1 — Mechanism\n\n- duration: 4s\n- src: compositions/frames/01-mechanism.html\n- focal: the request-lifecycle flow\n`,
  );

  const [packet] = buildFramePackets({ projectDir: project });

  assert.doesNotMatch(readFileSync(packet.path, "utf8"), /## Code block/);
});

test("CLI capability detection rejects skills newer than the available command surface", () => {
  const stableHelp = `Project:\n  lint  Validate a composition\n  snapshot  Capture frames\n\nUnknown command check`;
  const currentHelp = `Project:\n  lint  Validate a composition\n  check Run the full project validation gate\n  snapshot  Capture frames`;

  assert.equal(hasCliCommand(stableHelp, "check"), false);
  assert.equal(hasCliCommand(currentHelp, "check"), true);
});
