import { test, describe } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execSync } from "node:child_process";
import { checkLibraryDecisions, checkRenovatePr } from "./check.mjs";

/** Create a temp git repo with one initial commit so HEAD exists. */
function makeRepo() {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "libcheck-"));
  const g = (cmd) => execSync(cmd, { cwd: dir, stdio: "pipe" });
  g("git init");
  g("git config user.email test@test.com");
  g("git config user.name Test");
  g("git config commit.gpgsign false");
  fs.writeFileSync(path.join(dir, ".gitkeep"), "");
  g("git add .gitkeep");
  g("git commit -m init");
  return { dir, g };
}

/** Write a package.json under relDir and commit it as the baseline. */
function commitPkg(dir, g, relDir, pkg) {
  const pkgDir = path.join(dir, relDir);
  fs.mkdirSync(pkgDir, { recursive: true });
  fs.writeFileSync(
    path.join(pkgDir, "package.json"),
    JSON.stringify(pkg, null, 2),
  );
  g(`git add ${relDir}/package.json`);
  g("git commit -m add-pkg");
}

/** Overwrite a package.json and stage the result (no new commit). */
function stagePkg(dir, g, relDir, pkg) {
  fs.writeFileSync(
    path.join(dir, relDir, "package.json"),
    JSON.stringify(pkg, null, 2),
  );
  g(`git add ${relDir}/package.json`);
}

function traceFm(depName, decision) {
  return `package: ${depName}
version: "^1.0.0"
tier: feature
decision: ${decision}
date: 2026-05-14
deciders: [alice]
adr: null
filter-results:
  license: MIT
  types: native
  maintenance: active
  boundary-fit: pass
  shadow-check: pass
  eu-residency: ok
  cve-scan: clean
  named-consumer: pass
verification-commands:
  - pnpm audit --audit-level=moderate`;
}

/** Write a trace file and stage it. */
function stageTrace(dir, g, depName, decision = "approved") {
  const traceDir = path.join(dir, "docs", "library-decisions");
  fs.mkdirSync(traceDir, { recursive: true });
  const file = `2026-05-14-${depName}.md`;
  fs.writeFileSync(
    path.join(traceDir, file),
    `---\n${traceFm(depName, decision)}\n---\n\n`,
  );
  g(`git add docs/library-decisions/${file}`);
}

describe("checkLibraryDecisions", () => {
  test("new feature-tier dep without trace → exit 1", () => {
    const { dir, g } = makeRepo();
    commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
    stagePkg(dir, g, "packages/feat-a", {
      dependencies: { "new-lib": "^1.0.0" },
    });

    const errs = checkLibraryDecisions(dir);

    assert.equal(errs.length, 1);
    assert.equal(errs[0].dep, "new-lib");
    assert.equal(errs[0].reason, "no-trace");
  });

  test("new feature-tier dep with approved trace staged → exit 0", () => {
    const { dir, g } = makeRepo();
    commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
    stagePkg(dir, g, "packages/feat-a", {
      dependencies: { "new-lib": "^1.0.0" },
    });
    stageTrace(dir, g, "new-lib", "approved");

    assert.deepEqual(checkLibraryDecisions(dir), []);
  });

  test("new feature-tier dep with rejected-decision trace staged → exit 1", () => {
    const { dir, g } = makeRepo();
    commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
    stagePkg(dir, g, "packages/feat-a", {
      dependencies: { "new-lib": "^1.0.0" },
    });
    stageTrace(dir, g, "new-lib", "rejected");

    const errs = checkLibraryDecisions(dir);

    assert.equal(errs.length, 1);
    assert.equal(errs[0].dep, "new-lib");
    assert.equal(errs[0].reason, "not-approved");
    assert.equal(errs[0].decision, "rejected");
  });

  test("new app-tier dep → exit 0", () => {
    const { dir, g } = makeRepo();
    commitPkg(dir, g, "apps/web", { dependencies: {} });
    stagePkg(dir, g, "apps/web", { dependencies: { "new-lib": "^1.0.0" } });

    assert.deepEqual(checkLibraryDecisions(dir), []);
  });

  test("new devDependency → exit 0", () => {
    const { dir, g } = makeRepo();
    commitPkg(dir, g, "packages/feat-a", {});
    stagePkg(dir, g, "packages/feat-a", {
      devDependencies: { "test-lib": "^1.0.0" },
    });

    assert.deepEqual(checkLibraryDecisions(dir), []);
  });

  test("multi-file diff with mixed pass/fail → exit 1 with per-package report", () => {
    const { dir, g } = makeRepo();
    commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
    commitPkg(dir, g, "packages/feat-b", { dependencies: {} });
    stagePkg(dir, g, "packages/feat-a", {
      dependencies: { "lib-a": "^1.0.0" },
    });
    stagePkg(dir, g, "packages/feat-b", {
      dependencies: { "lib-b": "^1.0.0" },
    });
    stageTrace(dir, g, "lib-a", "approved"); // feat-a passes; no trace for lib-b

    const errs = checkLibraryDecisions(dir);

    assert.equal(errs.length, 1);
    assert.equal(errs[0].pkgJson, "packages/feat-b/package.json");
    assert.equal(errs[0].dep, "lib-b");
    assert.equal(errs[0].reason, "no-trace");
  });

  test("peerDependencies-only change → exit 0", () => {
    const { dir, g } = makeRepo();
    commitPkg(dir, g, "packages/feat-a", {});
    stagePkg(dir, g, "packages/feat-a", {
      peerDependencies: { react: "^18.0.0" },
    });

    assert.deepEqual(checkLibraryDecisions(dir), []);
  });

  test("new workspace-protocol dep (internal monorepo package) → exit 0", () => {
    const { dir, g } = makeRepo();
    commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
    stagePkg(dir, g, "packages/feat-a", {
      dependencies: { "@repo/core-shared": "workspace:*" },
    });

    assert.deepEqual(checkLibraryDecisions(dir), []);
  });

  test("new feature-tier dep with already-committed approved trace → exit 0", () => {
    const { dir, g } = makeRepo();
    // Commit trace first (simulates an existing workspace-approved library)
    stageTrace(dir, g, "existing-lib", "approved");
    g("git commit -m add-trace");
    // Now add a new package that depends on the same library
    commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
    stagePkg(dir, g, "packages/feat-a", {
      dependencies: { "existing-lib": "^1.0.0" },
    });

    // No staged trace needed — the committed trace is the fallback
    assert.deepEqual(checkLibraryDecisions(dir), []);
  });

  test("new feature-tier dep with already-committed rejected trace → exit 1", () => {
    const { dir, g } = makeRepo();
    stageTrace(dir, g, "bad-lib", "rejected");
    g("git commit -m add-trace");
    commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
    stagePkg(dir, g, "packages/feat-a", {
      dependencies: { "bad-lib": "^1.0.0" },
    });

    const errs = checkLibraryDecisions(dir);
    assert.equal(errs.length, 1);
    assert.equal(errs[0].dep, "bad-lib");
    assert.equal(errs[0].reason, "not-approved");
  });

  test("--staged-against mode: new feature-tier dep without trace → exit 1", () => {
    const { dir, g } = makeRepo();
    // Baseline commit: feature package with no deps
    commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
    // Second commit: adds new-lib — no trace file committed alongside it
    commitPkg(dir, g, "packages/feat-a", {
      dependencies: { "new-lib": "^1.0.0" },
    });

    // HEAD has new-lib; HEAD~1 doesn't — no trace in the diff → exit 1
    const errs = checkLibraryDecisions(dir, { stagedAgainst: "HEAD~1" });

    assert.equal(errs.length, 1);
    assert.equal(errs[0].dep, "new-lib");
    assert.equal(errs[0].reason, "no-trace");
  });
});

// ---------------------------------------------------------------------------
// checkRenovatePr — integration tests
// ---------------------------------------------------------------------------

function makeTempDir() {
  return fs.mkdtempSync(path.join(os.tmpdir(), "renprc-"));
}

function writeFixture(dir, relPath, content) {
  const full = path.join(dir, relPath);
  fs.mkdirSync(path.dirname(full), { recursive: true });
  fs.writeFileSync(full, content);
}

function featurePkg(dir, name, deps) {
  writeFixture(
    dir,
    `packages/${name}/package.json`,
    JSON.stringify({ dependencies: deps }, null, 2),
  );
}

function appPkg(dir, name, deps) {
  writeFixture(
    dir,
    `apps/${name}/package.json`,
    JSON.stringify({ dependencies: deps }, null, 2),
  );
}

function traceFixture(dir, depName, lastRevalidated) {
  const lr = lastRevalidated == null ? "null" : lastRevalidated;
  const fm = `package: ${depName}
version: "^1.0.0"
tier: feature
decision: approved
date: 2026-05-14
lastRevalidated: ${lr}
deciders: [alice]
adr: null
filter-results:
  license: MIT
  types: native
  maintenance: active
  boundary-fit: pass
  shadow-check: pass
  eu-residency: ok
  cve-scan: clean
  named-consumer: pass
  socketRisk: clean
verification-commands:
  - pnpm audit`;
  writeFixture(
    dir,
    `docs/library-decisions/2026-05-14-${depName}.md`,
    `---\n${fm}\n---\n\n`,
  );
}

function buildLockfileDiff(pkg, fromVer, toVer) {
  return [
    "--- a/pnpm-lock.yaml",
    "+++ b/pnpm-lock.yaml",
    "@@ -10,7 +10,7 @@ packages:",
    `-  ${pkg}@${fromVer}: {}`,
    `+  ${pkg}@${toVer}: {}`,
    "",
  ].join("\n");
}

describe("checkRenovatePr", () => {
  test("minor bump on feature-tier dep → pass", () => {
    const dir = makeTempDir();
    featurePkg(dir, "feat-a", { "some-lib": "^1.0.0" });

    const errs = checkRenovatePr(dir, {
      branch: "renovate/some-lib-1.1.0",
      diff: buildLockfileDiff("some-lib", "1.0.0", "1.1.0"),
      today: "2026-05-14",
    });

    assert.deepEqual(errs, []);
  });

  test("major bump + fresh lastRevalidated → pass", () => {
    const dir = makeTempDir();
    featurePkg(dir, "feat-a", { "some-lib": "^2.0.0" });
    traceFixture(dir, "some-lib", "2026-05-14");

    const errs = checkRenovatePr(dir, {
      branch: "renovate/some-lib-2.0.0",
      diff: buildLockfileDiff("some-lib", "1.2.3", "2.0.0"),
      today: "2026-05-14",
    });

    assert.deepEqual(errs, []);
  });

  test("major bump + stale lastRevalidated → fail with pointer", () => {
    const dir = makeTempDir();
    featurePkg(dir, "feat-a", { "some-lib": "^1.0.0" });
    traceFixture(dir, "some-lib", "2026-01-01");

    const errs = checkRenovatePr(dir, {
      branch: "renovate/some-lib-2.0.0",
      diff: buildLockfileDiff("some-lib", "1.2.3", "2.0.0"),
      today: "2026-05-14",
    });

    assert.equal(errs.length, 1);
    assert.equal(errs[0].dep, "some-lib");
    assert.equal(errs[0].from, "1.2.3");
    assert.equal(errs[0].to, "2.0.0");
    assert.equal(errs[0].reason, "stale");
    assert.equal(errs[0].lastRevalidated, "2026-01-01");
    assert.ok(typeof errs[0].tracePath === "string");
    assert.ok(errs[0].tracePath.includes("some-lib"));
  });

  test("major bump on app-tier dep → pass", () => {
    const dir = makeTempDir();
    // dep only in apps/, not in packages/ → app-tier exemption
    appPkg(dir, "web", { "some-lib": "^1.0.0" });

    const errs = checkRenovatePr(dir, {
      branch: "renovate/some-lib-2.0.0",
      diff: buildLockfileDiff("some-lib", "1.2.3", "2.0.0"),
      today: "2026-05-14",
    });

    assert.deepEqual(errs, []);
  });

  test("patch bump in Renovate branch → pass", () => {
    const dir = makeTempDir();
    featurePkg(dir, "feat-a", { "some-lib": "^1.0.0" });

    const errs = checkRenovatePr(dir, {
      branch: "renovate/some-lib-1.2.4",
      diff: buildLockfileDiff("some-lib", "1.2.3", "1.2.4"),
      today: "2026-05-14",
    });

    assert.deepEqual(errs, []);
  });

  test("non-Renovate branch with major bump → pass", () => {
    const dir = makeTempDir();
    featurePkg(dir, "feat-a", { "some-lib": "^1.0.0" });

    const errs = checkRenovatePr(dir, {
      branch: "main",
      diff: buildLockfileDiff("some-lib", "1.2.3", "2.0.0"),
      today: "2026-05-14",
    });

    assert.deepEqual(errs, []);
  });
});
