"""Tests for .github/scripts/npm_audit_allowlist.py — the npm-audit release-gate
allowlist filter.

This script is a security gate: it must (1) FAIL (exit 1) on any non-allowlisted
moderate-or-higher vulnerability, (2) resolve npm's transitive ``via`` chains so
a parent flagged only because of an allowlisted leaf is itself handled, and
(3) FAIL SAFE on any input that is not a genuine npm audit report (a
registry/network error returns valid JSON with no report, and npm's non-zero
exit is swallowed by the workflow's ``|| true``). These tests pin all three.

Run via subprocess to exercise the real exit-code contract the workflow relies
on, and because the allowlist is read from the environment at import time.
"""

import json
import os
import subprocess
import sys
from pathlib import Path

import pytest

SCRIPT = (
    Path(__file__).resolve().parents[2]
    / ".github"
    / "scripts"
    / "npm_audit_allowlist.py"
)

ALLOW = "GHSA-aaaa-aaaa-aaaa"  # the single allowlisted advisory under test


def run(payload, allowlist=ALLOW):
    """Run the filter with *payload* (str or dict) on stdin; return exit code."""
    stdin = payload if isinstance(payload, str) else json.dumps(payload)
    proc = subprocess.run(
        [sys.executable, str(SCRIPT)],
        input=stdin,
        capture_output=True,
        text=True,
        env={**os.environ, "AUDIT_ALLOWLIST": allowlist},
    )
    return proc.returncode


def report(vulns):
    """Wrap a vulnerabilities map in a minimal valid npm v7+ audit report."""
    return {"auditReportVersion": 2, "vulnerabilities": vulns, "metadata": {}}


def advisory(ghsa, severity="moderate"):
    return {
        "severity": severity,
        "via": [{"url": f"https://github.com/advisories/{ghsa}"}],
    }


# --------------------------------------------------------------------------
# Passes the gate (exit 0)
# --------------------------------------------------------------------------


def test_clean_report_passes():
    assert run(report({})) == 0


def test_allowlisted_leaf_passes():
    assert run(report({"js-yaml": advisory(ALLOW)})) == 0


def test_via_chain_parent_passes():
    # Parent is vulnerable ONLY because its via-string points to the
    # allowlisted leaf — must resolve and pass.
    payload = report(
        {
            "@lhci/utils": {"severity": "moderate", "via": ["js-yaml"]},
            "js-yaml": advisory(ALLOW),
        }
    )
    assert run(payload) == 0


def test_low_severity_only_passes():
    # Below the moderate threshold — ignored, like `npm audit --audit-level`.
    assert run(report({"x": advisory("GHSA-bbbb-bbbb-bbbb", "low")})) == 0


# --------------------------------------------------------------------------
# Fails the gate (exit 1) — real vulnerabilities
# --------------------------------------------------------------------------


def test_non_allowlisted_moderate_fails():
    assert run(report({"evil": advisory("GHSA-bbbb-bbbb-bbbb")})) == 1


@pytest.mark.parametrize("sev", ["high", "critical"])
def test_high_and_critical_fail(sev):
    assert run(report({"evil": advisory("GHSA-bbbb-bbbb-bbbb", sev)})) == 1


def test_coattail_mixed_via_fails():
    # A package reachable via BOTH the allowlisted leaf AND a separate
    # non-allowlisted advisory must NOT be laundered through the allowlist.
    payload = report(
        {
            "evil": {
                "severity": "moderate",
                "via": [
                    {
                        "url": "https://github.com/advisories/GHSA-bbbb-bbbb-bbbb"
                    },
                    "js-yaml",
                ],
            },
            "js-yaml": advisory(ALLOW),
        }
    )
    assert run(payload) == 1


def test_cve_only_no_ghsa_fails():
    # An advisory with no GHSA cannot be matched against a GHSA allowlist —
    # fail safe rather than silently drop it.
    payload = report(
        {
            "x": {
                "severity": "high",
                "via": [{"url": "https://nvd.nist.gov/CVE-1"}],
            }
        }
    )
    assert run(payload) == 1


def test_empty_allowlist_fails_even_on_listed_advisory():
    assert run(report({"js-yaml": advisory(ALLOW)}), allowlist="") == 1


# --------------------------------------------------------------------------
# Fail SAFE on non-report / malformed input (gate red, never silently green)
# --------------------------------------------------------------------------


def test_registry_error_object_fails():
    # The real fail-open case: npm registry/network error returns valid JSON
    # with no report, and `|| true` swallows npm's non-zero exit.
    err = {"message": "request failed, reason: ETIMEDOUT", "error": {}}
    assert run(err) == 1


def test_report_missing_audit_version_fails():
    assert run({"message": "err", "vulnerabilities": {}}) == 1


def test_report_missing_vulnerabilities_fails():
    assert run({"auditReportVersion": 2}) == 1


def test_non_dict_vulnerabilities_fails():
    assert run({"auditReportVersion": 2, "vulnerabilities": ["x"]}) == 1


def test_empty_stdin_fails():
    assert run("") == 1


def test_malformed_json_fails():
    assert run("not json at all") == 1


# --------------------------------------------------------------------------
# GHSA extraction robustness
# --------------------------------------------------------------------------


def test_ghsa_url_with_trailing_path_is_parsed_cleanly():
    # A URL carrying a trailing path/query must extract just the GHSA token,
    # so it still matches the allowlist exactly (no over- or under-match).
    payload = report(
        {
            "js-yaml": {
                "severity": "moderate",
                "via": [
                    {
                        "url": f"https://github.com/advisories/{ALLOW}/extra?ref=x"
                    }
                ],
            }
        }
    )
    assert run(payload) == 0
