// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// Tests for dep-review.js — run with: node --test .github/scripts/dep-review.test.js

"use strict";

const { describe, it } = require("node:test");
const assert = require("node:assert/strict");

const {
  parseExternalDeps,
  fmtSource,
  diffDeps,
  buildSummary,
  parseCratePathMap,
  parseInternalDepGraph,
  checkContainment,
  diffContainmentViolations,
  buildPolicySummary,
} = require("./dep-review.js");

// -- fixtures --

const LOCK_MINIMAL = `
# This file is automatically @generated by Cargo.
version = 4

[[package]]
name = "my_crate"
version = "0.0.0"
dependencies = [
 "serde",
 "helper",
]

[[package]]
name = "helper"
version = "0.0.0"

[[package]]
name = "serde"
version = "1.0.200"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abc"

[[package]]
name = "serde_derive"
version = "1.0.200"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "def"
`;

const MANIFEST_MINIMAL = `
[workspace]
members = ["my_crate", "helper", "lib_a", "lib_b"]

[workspace.dependencies]
my_crate = { path = "app/my_crate" }
helper = { path = "support/helper" }
lib_a = { path = "support/lib_a" }
lib_b = { path = "vm/lib_b" }
serde = "1.0"
`;

const POLICY = {
  containment: [
    {
      prefix: "support/",
      description: "support/ crates must not depend on crates outside support/",
    },
  ],
};

// -- parseExternalDeps --

describe("parseExternalDeps", () => {
  it("extracts external packages (those with source)", () => {
    const deps = parseExternalDeps(LOCK_MINIMAL);
    assert.equal(deps.size, 2);
    assert.ok(
      deps.has("serde\t1.0.200\tregistry+https://github.com/rust-lang/crates.io-index")
    );
    assert.ok(
      deps.has("serde_derive\t1.0.200\tregistry+https://github.com/rust-lang/crates.io-index")
    );
  });

  it("ignores internal packages (no source)", () => {
    const deps = parseExternalDeps(LOCK_MINIMAL);
    for (const key of deps) {
      assert.ok(!key.startsWith("my_crate\t"));
      assert.ok(!key.startsWith("helper\t"));
    }
  });

  it("handles empty lockfile", () => {
    const deps = parseExternalDeps("version = 4\n");
    assert.equal(deps.size, 0);
  });

  it("handles git sources", () => {
    const lock = `
[[package]]
name = "foo"
version = "0.1.0"
source = "git+https://github.com/org/foo?branch=main#abc123"
`;
    const deps = parseExternalDeps(lock);
    assert.equal(deps.size, 1);
    const key = [...deps][0];
    assert.ok(key.startsWith("foo\t0.1.0\tgit+"));
  });

  it("tracks multiple versions of the same crate", () => {
    const lock = `
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaa"

[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb"
`;
    const deps = parseExternalDeps(lock);
    assert.equal(deps.size, 2);
    assert.ok(
      deps.has("windows-sys\t0.48.0\tregistry+https://github.com/rust-lang/crates.io-index")
    );
    assert.ok(
      deps.has("windows-sys\t0.52.0\tregistry+https://github.com/rust-lang/crates.io-index")
    );
  });
});

// -- fmtSource --

describe("fmtSource", () => {
  it("returns empty for registry source", () => {
    assert.equal(fmtSource("registry+https://github.com/rust-lang/crates.io-index"), "");
  });

  it("formats git source with URL", () => {
    const result = fmtSource("git+https://github.com/org/repo?branch=main#abc123");
    assert.equal(result, " (https://github.com/org/repo?branch=main)");
  });

  it("passes through unknown sources", () => {
    assert.equal(fmtSource("something-else"), " (something-else)");
  });
});

// -- diffDeps --

describe("diffDeps", () => {
  it("detects added crates", () => {
    const base = new Set();
    const pr = new Set(["foo\t1.0.0\tsrc"]);
    const { added } = diffDeps(base, pr);
    assert.equal(added.length, 1);
    assert.equal(added[0].name, "foo");
  });

  it("detects version changes as additions", () => {
    const base = new Set(["foo\t1.0.0\tsrc"]);
    const pr = new Set(["foo\t2.0.0\tsrc"]);
    const { added } = diffDeps(base, pr);
    assert.equal(added.length, 1);
    assert.equal(added[0].name, "foo");
    assert.equal(added[0].version, "2.0.0");
  });

  it("does not report removals", () => {
    const base = new Set(["foo\t1.0.0\tsrc"]);
    const pr = new Set();
    const { added } = diffDeps(base, pr);
    assert.equal(added.length, 0);
  });

  it("reports nothing when identical", () => {
    const base = new Set(["foo\t1.0.0\tsrc"]);
    const pr = new Set(["foo\t1.0.0\tsrc"]);
    const { added } = diffDeps(base, pr);
    assert.equal(added.length, 0);
  });

  it("detects new version alongside existing version", () => {
    const base = new Set(["foo\t1.0.0\tsrc"]);
    const pr = new Set(["foo\t1.0.0\tsrc", "foo\t2.0.0\tsrc"]);
    const { added } = diffDeps(base, pr);
    assert.equal(added.length, 1);
    assert.equal(added[0].version, "2.0.0");
  });
});

// -- buildSummary --

describe("buildSummary", () => {
  it("includes added crates", () => {
    const summary = buildSummary({
      added: [{ name: "foo", version: "1.0.0", source: "registry+x" }],
    });
    assert.ok(summary.includes("**New external crate versions:**"));
    assert.ok(summary.includes("`foo` 1.0.0"));
  });
});

// -- parseCratePathMap --

describe("parseCratePathMap", () => {
  it("extracts path-based dependencies", () => {
    const map = parseCratePathMap(MANIFEST_MINIMAL);
    assert.equal(map.get("my_crate"), "app/my_crate");
    assert.equal(map.get("helper"), "support/helper");
    assert.equal(map.get("lib_a"), "support/lib_a");
    assert.equal(map.get("lib_b"), "vm/lib_b");
  });

  it("ignores non-path dependencies", () => {
    const map = parseCratePathMap(MANIFEST_MINIMAL);
    assert.ok(!map.has("serde"));
  });

  it("handles complex inline tables", () => {
    const toml = `foo = { path = "a/b", features = ["x"], default-features = false }`;
    const map = parseCratePathMap(toml);
    assert.equal(map.get("foo"), "a/b");
  });
});

// -- parseInternalDepGraph --

describe("parseInternalDepGraph", () => {
  it("extracts internal crates and their dependencies", () => {
    const { graph, internalCrates } = parseInternalDepGraph(LOCK_MINIMAL);
    assert.ok(internalCrates.has("my_crate"));
    assert.ok(internalCrates.has("helper"));
    assert.ok(!internalCrates.has("serde"));
    assert.deepEqual(graph.get("my_crate"), ["serde", "helper"]);
    assert.deepEqual(graph.get("helper"), []);
  });
});

// -- checkContainment --

describe("checkContainment", () => {
  it("flags violations when support/ crate depends on non-support/ crate", () => {
    // helper (support/helper) depends on lib_b (vm/lib_b)
    const lock = `
[[package]]
name = "helper"
version = "0.0.0"
dependencies = [
 "lib_b",
]

[[package]]
name = "lib_b"
version = "0.0.0"
`;
    const { graph, internalCrates } = parseInternalDepGraph(lock);
    const pathMap = parseCratePathMap(MANIFEST_MINIMAL);
    const violations = checkContainment(graph, internalCrates, pathMap, POLICY);
    assert.equal(violations.length, 1);
    assert.equal(violations[0].crate, "helper");
    assert.equal(violations[0].dep, "lib_b");
  });

  it("allows support/ crate depending on other support/ crate", () => {
    const lock = `
[[package]]
name = "helper"
version = "0.0.0"
dependencies = [
 "lib_a",
]

[[package]]
name = "lib_a"
version = "0.0.0"
`;
    const { graph, internalCrates } = parseInternalDepGraph(lock);
    const pathMap = parseCratePathMap(MANIFEST_MINIMAL);
    const violations = checkContainment(graph, internalCrates, pathMap, POLICY);
    assert.equal(violations.length, 0);
  });

  it("ignores non-support/ crates depending on anything", () => {
    const lock = `
[[package]]
name = "my_crate"
version = "0.0.0"
dependencies = [
 "helper",
 "lib_b",
]

[[package]]
name = "helper"
version = "0.0.0"

[[package]]
name = "lib_b"
version = "0.0.0"
`;
    const { graph, internalCrates } = parseInternalDepGraph(lock);
    const pathMap = parseCratePathMap(MANIFEST_MINIMAL);
    const violations = checkContainment(graph, internalCrates, pathMap, POLICY);
    assert.equal(violations.length, 0);
  });
});

// -- diffContainmentViolations --

describe("diffContainmentViolations", () => {
  const v1 = { crate: "a", dep: "b", cratePath: "support/a", depPath: "vm/b", rule: "test" };
  const v2 = { crate: "c", dep: "d", cratePath: "support/c", depPath: "vm/d", rule: "test" };

  it("returns only new violations", () => {
    const result = diffContainmentViolations([v1], [v1, v2]);
    assert.equal(result.length, 1);
    assert.equal(result[0].crate, "c");
  });

  it("returns empty when no new violations", () => {
    const result = diffContainmentViolations([v1, v2], [v1]);
    assert.equal(result.length, 0);
  });

  it("returns empty when identical", () => {
    const result = diffContainmentViolations([v1], [v1]);
    assert.equal(result.length, 0);
  });
});

// -- buildPolicySummary --

describe("buildPolicySummary", () => {
  it("returns empty for no violations", () => {
    assert.equal(buildPolicySummary([]), "");
  });

  it("formats violations", () => {
    const summary = buildPolicySummary([
      { crate: "a", cratePath: "support/a", dep: "b", depPath: "vm/b", rule: "no cross" },
    ]);
    assert.ok(summary.includes("`a` (support/a) → `b` (vm/b)"));
    assert.ok(summary.includes("no cross"));
  });
});

// -- integration: full lockfile round-trip --

describe("integration", () => {
  it("no issues when base and PR are identical", () => {
    const baseDeps = parseExternalDeps(LOCK_MINIMAL);
    const prDeps = parseExternalDeps(LOCK_MINIMAL);
    const diff = diffDeps(baseDeps, prDeps);
    assert.equal(diff.added.length, 0);

    const pathMap = parseCratePathMap(MANIFEST_MINIMAL);
    const baseGraph = parseInternalDepGraph(LOCK_MINIMAL);
    const prGraph = parseInternalDepGraph(LOCK_MINIMAL);
    const baseV = checkContainment(baseGraph.graph, baseGraph.internalCrates, pathMap, POLICY);
    const prV = checkContainment(prGraph.graph, prGraph.internalCrates, pathMap, POLICY);
    const newV = diffContainmentViolations(baseV, prV);
    assert.equal(newV.length, 0);
  });

  it("detects added external dep + new containment violation together", () => {
    const prLock = LOCK_MINIMAL + `
[[package]]
name = "new_ext"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "xyz"

[[package]]
name = "lib_a"
version = "0.0.0"
dependencies = [
 "lib_b",
]

[[package]]
name = "lib_b"
version = "0.0.0"
`;

    // External diff
    const baseDeps = parseExternalDeps(LOCK_MINIMAL);
    const prDeps = parseExternalDeps(prLock);
    const diff = diffDeps(baseDeps, prDeps);
    assert.equal(diff.added.length, 1);
    assert.equal(diff.added[0].name, "new_ext");

    // Containment
    const pathMap = parseCratePathMap(MANIFEST_MINIMAL);
    const baseGraph = parseInternalDepGraph(LOCK_MINIMAL);
    const prGraph = parseInternalDepGraph(prLock);
    const baseV = checkContainment(baseGraph.graph, baseGraph.internalCrates, pathMap, POLICY);
    const prV = checkContainment(prGraph.graph, prGraph.internalCrates, pathMap, POLICY);
    const newV = diffContainmentViolations(baseV, prV);
    assert.equal(newV.length, 1);
    assert.equal(newV[0].crate, "lib_a");
    assert.equal(newV[0].dep, "lib_b");
  });
});
