# tests/test_deps_changed.py
"""deps_changed.py — the git-stateful TRIGGER FILTER for `deps` (Mode 1 + scope computation). Hermetic: each
test builds a TEMP git repo, commits a package.json/lockfile, stages an edit, and asserts the {run, args} verdict.
No network. Proves the user's exact case (exports-only edit → run:false) plus dep-map/lockfile localization."""
import json, os, subprocess

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CHANGED = os.path.join(ROOT, "domains", "security", "detectors", "deps", "deps_changed.py")


def _git(repo, *args):
    subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True, text=True)


def _init_repo(tmp_path):
    _git(tmp_path, "init", "-q")
    _git(tmp_path, "config", "user.email", "t@t")
    _git(tmp_path, "config", "user.name", "t")
    return tmp_path


def _decide(repo, changed):
    p = subprocess.run(["python3", CHANGED], capture_output=True, text=True, timeout=30,
                       input=json.dumps({"repo_root": str(repo), "changed": changed}))
    assert p.stdout, f"deps_changed produced no stdout (stderr: {p.stderr[:300]})"
    return json.loads(p.stdout)


def test_exports_only_edit_does_not_run(tmp_path):
    # THE USER'S CASE: a package.json edit that adds an exports subpath (zero deps changed) must NOT trigger deps.
    repo = _init_repo(tmp_path)
    pkg = repo / "package.json"
    pkg.write_text(json.dumps({"name": "x", "version": "1.0.0", "dependencies": {"lodash": "^4"}}))
    _git(repo, "add", "package.json"); _git(repo, "commit", "-qm", "init")
    pkg.write_text(json.dumps({"name": "x", "version": "1.0.0", "dependencies": {"lodash": "^4"},
                               "exports": {"./revisions": "./revisions.js"}}))
    _git(repo, "add", "package.json")
    out = _decide(repo, ["package.json"])
    assert out == {"run": False}, f"exports-only edit must NOT trigger deps; got {out}"


def test_scripts_only_edit_does_not_run(tmp_path):
    repo = _init_repo(tmp_path)
    pkg = repo / "package.json"
    pkg.write_text(json.dumps({"name": "x", "dependencies": {"lodash": "^4"}}))
    _git(repo, "add", "package.json"); _git(repo, "commit", "-qm", "init")
    pkg.write_text(json.dumps({"name": "x", "dependencies": {"lodash": "^4"}, "scripts": {"b": "tsc"}}))
    _git(repo, "add", "package.json")
    out = _decide(repo, ["package.json"])
    assert out == {"run": False}, f"scripts-only edit must NOT trigger; got {out}"


def test_dep_map_change_runs_scoped_to_workspace(tmp_path):
    repo = _init_repo(tmp_path)
    ws = repo / "packages" / "helpdesk"; ws.mkdir(parents=True)
    pkg = ws / "package.json"
    pkg.write_text(json.dumps({"name": "hd", "dependencies": {"lodash": "^4"}}))
    _git(repo, "add", "."); _git(repo, "commit", "-qm", "init")
    pkg.write_text(json.dumps({"name": "hd", "dependencies": {"lodash": "^4", "esbuild": "^0.23"}}))
    _git(repo, "add", ".")
    out = _decide(repo, ["packages/helpdesk/package.json"])
    assert out["run"] is True, f"a real dep-map change must run; got {out}"
    assert "--commit-scope" in out["args"], f"must be commit-scoped (never full-tree); got {out}"
    assert out["args"].count("--scope") == 1
    i = out["args"].index("--scope")
    assert out["args"][i + 1] == "packages/helpdesk", f"scope = the workspace dir; got {out}"


def test_new_package_with_deps_runs_scoped(tmp_path):
    # a brand-new package.json (absent at HEAD) carrying deps → run + scope it.
    repo = _init_repo(tmp_path)
    (repo / "README").write_text("x")
    _git(repo, "add", "."); _git(repo, "commit", "-qm", "init")
    ws = repo / "apps" / "consumer"; ws.mkdir(parents=True)
    (ws / "package.json").write_text(json.dumps({"name": "c", "dependencies": {"lodash": "^4"}}))
    _git(repo, "add", ".")
    out = _decide(repo, ["apps/consumer/package.json"])
    assert out["run"] is True and "apps/consumer" in out["args"], f"new pkg with deps must scope; got {out}"


def test_lockfile_only_runs_commit_scope_zero_scope(tmp_path):
    # pnpm update / dedupe / post-merge install: ONLY the lockfile moves, no manifest dep-map change.
    # Run, but commit-scoped with ZERO --scope → Mode 2 blocks nothing at pre-commit; full-tree is CI's job.
    repo = _init_repo(tmp_path)
    (repo / "package.json").write_text(json.dumps({"name": "x", "dependencies": {"lodash": "^4"}}))
    lock = repo / "pnpm-lock.yaml"; lock.write_text("lockfileVersion: '9.0'\n")
    _git(repo, "add", "."); _git(repo, "commit", "-qm", "init")
    lock.write_text("lockfileVersion: '9.0'\n# bumped\n")
    _git(repo, "add", ".")
    out = _decide(repo, ["pnpm-lock.yaml"])
    assert out["run"] is True and out["args"] == ["--commit-scope"], \
        f"lockfile-only must run commit-scoped with ZERO --scope; got {out}"


def test_dep_add_changes_manifest_and_lock_scopes_workspace(tmp_path):
    # `pnpm add X` to a workspace: BOTH the workspace manifest dep-map and the lockfile change → scope the workspace
    # (the manifest dep-map change wins; lockfile presence does not widen to full-tree).
    repo = _init_repo(tmp_path)
    ws = repo / "packages" / "auth"; ws.mkdir(parents=True)
    (ws / "package.json").write_text(json.dumps({"name": "auth", "dependencies": {"lodash": "^4"}}))
    (repo / "pnpm-lock.yaml").write_text("lockfileVersion: '9.0'\n")
    _git(repo, "add", "."); _git(repo, "commit", "-qm", "init")
    (ws / "package.json").write_text(json.dumps({"name": "auth", "dependencies": {"lodash": "^4", "jsonwebtoken": "^9"}}))
    (repo / "pnpm-lock.yaml").write_text("lockfileVersion: '9.0'\n# jsonwebtoken added\n")
    _git(repo, "add", ".")
    out = _decide(repo, ["packages/auth/package.json", "pnpm-lock.yaml"])
    assert out["run"] is True and "--commit-scope" in out["args"]
    i = out["args"].index("--scope")
    assert out["args"][i + 1] == "packages/auth", f"pnpm-add must scope to the touched workspace; got {out}"


def test_absolute_changed_paths_still_scope_no_false_clean(tmp_path):
    # DEFENSE-IN-DEPTH: a caller passing ABSOLUTE paths must NOT silently degrade to lockfile-only (= false-clean
    # on a real dep change). decide() relativizes against repo_root, so the dep-map change is still localized.
    repo = _init_repo(tmp_path)
    ws = repo / "packages" / "helpdesk"; ws.mkdir(parents=True)
    pkg = ws / "package.json"
    pkg.write_text(json.dumps({"name": "hd", "dependencies": {"lodash": "^4"}}))
    _git(repo, "add", "."); _git(repo, "commit", "-qm", "init")
    pkg.write_text(json.dumps({"name": "hd", "dependencies": {"lodash": "^4", "esbuild": "^0.23"}}))
    _git(repo, "add", ".")
    out = _decide(repo, [str(pkg)])  # ABSOLUTE path
    assert out["run"] is True and "--commit-scope" in out["args"], f"absolute path must still run scoped; got {out}"
    i = out["args"].index("--scope")
    assert out["args"][i + 1] == "packages/helpdesk", f"absolute path must relativize to the workspace; got {out}"


def test_nothing_relevant_does_not_run(tmp_path):
    repo = _init_repo(tmp_path)
    out = _decide(repo, [])
    assert out == {"run": False}, f"no dep-relevant change must NOT run; got {out}"
