"""Tests for adapters/kyverno-json/kyverno_json_engine.py (REQ-309, v1.25).

PCR schema validity (jsonschema validation), defensive parsing
(malformed output → error PCR, never exception), is_configured()
guard, severity annotation reading (G-Q10a), and pytest.skip when
kj is absent.
"""

import json
import os
import sys
from pathlib import Path
from unittest import mock

import jsonschema
import pytest

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

# Load the engine module by file path (the dir has a hyphen).
import importlib.util
_ENGINE_PATH = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "kyverno_json_engine.py"
_spec = importlib.util.spec_from_file_location("kyverno_json_engine", _ENGINE_PATH)
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
KyvernoJsonEngine = _mod.KyvernoJsonEngine
_to_pcr = _mod._to_pcr
_load_policy_severities = _mod._load_policy_severities

PCR_SCHEMA_PATH = Path(__file__).resolve().parent.parent / "schemas" / "policy_check_result.schema.json"


def _load_pcr_schema():
    with open(PCR_SCHEMA_PATH, "r", encoding="utf-8") as fh:
        return json.load(fh)


PCR_SCHEMA = _load_pcr_schema()


def _kj_installed() -> bool:
    """Return True if the kj binary is on PATH."""
    return _mod._which_kj() is not None


def _smoke_policy_dir() -> Path:
    return Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "policies"


class TestToPcr:
    def test_pass_entry(self):
        entry = {"policy": "require-contract-id", "rule": "require-id",
                 "result": "pass", "message": "ok", "resource": "res-1"}
        pcr = _to_pcr(entry, "cid", "high")
        assert pcr["contractId"] == "cid"
        assert pcr["engine"] == "kyverno"
        assert pcr["ruleId"] == "KJ_require-contract-id/require-id"
        assert pcr["result"] == "pass"
        assert pcr["severity"] == "high"
        assert pcr["resourceRef"] == "res-1"

    def test_fail_entry(self):
        entry = {"policy": "forbid-public-ingress", "rule": "no-public",
                 "result": "fail", "message": "public ingress not allowed",
                 "resource": "s3/x"}
        pcr = _to_pcr(entry, "cid", "critical")
        assert pcr["result"] == "fail"
        assert pcr["severity"] == "critical"
        assert pcr["message"] == "public ingress not allowed"

    def test_skip_entry(self):
        entry = {"policy": "p", "rule": "r", "result": "skip"}
        pcr = _to_pcr(entry, "cid", "info")
        assert pcr["result"] == "skipped"

    def test_unknown_result_becomes_error(self):
        entry = {"policy": "p", "rule": "r", "result": "garbled"}
        pcr = _to_pcr(entry, "cid", "info")
        assert pcr["result"] == "error"

    def test_pcr_validates_against_schema(self):
        entry = {"policy": "p", "rule": "r", "result": "pass",
                 "message": "ok", "resource": "r"}
        pcr = _to_pcr(entry, "cid-uuid", "medium")
        jsonschema.validate(pcr, PCR_SCHEMA)


class TestSeverityAnnotation:
    """G-Q10a: severity is read from the policy's metadata.annotation."""

    def test_policy_with_severity_annotation(self, tmp_path):
        policy = {
            "apiVersion": "json.kyverno.io/v1alpha1",
            "kind": "ValidatingPolicy",
            "metadata": {
                "name": "test-sev",
                "annotations": {"nova.cloudinit.dev/severity": "high"},
            },
            "spec": {"rules": [{"name": "r", "validate": {"assert": {"all": []}}}]},
        }
        p = tmp_path / "test-sev.json"
        p.write_text(json.dumps(policy))
        sevs = _load_policy_severities(tmp_path)
        assert sevs.get("test-sev") == "high"

    def test_policy_without_severity_defaults_info(self, tmp_path):
        policy = {
            "apiVersion": "json.kyverno.io/v1alpha1",
            "kind": "ValidatingPolicy",
            "metadata": {"name": "no-sev"},
            "spec": {"rules": [{"name": "r", "validate": {"assert": {"all": []}}}]},
        }
        p = tmp_path / "no-sev.json"
        p.write_text(json.dumps(policy))
        sevs = _load_policy_severities(tmp_path)
        assert sevs.get("no-sev") == "info"

    def test_underscore_files_skipped(self, tmp_path):
        # _smoke.json starts with _ — should be skipped.
        (tmp_path / "_smoke.json").write_text("{}")
        sevs = _load_policy_severities(tmp_path)
        assert sevs == {}


class TestIsConfigured:
    def test_is_configured_returns_bool(self):
        eng = KyvernoJsonEngine()
        assert isinstance(eng.is_configured(), bool)

    def test_is_configured_false_when_kj_absent(self, monkeypatch):
        monkeypatch.setattr(_mod, "_which_kj", lambda: None)
        eng = KyvernoJsonEngine()
        assert eng.is_configured() is False


class TestEvaluateNotConfigured:
    """When kj is absent, evaluate() returns KJ_ENGINE_NOT_CONFIGURED."""

    def test_evaluate_returns_skipped_when_not_configured(self, monkeypatch):
        monkeypatch.setattr(_mod, "_which_kj", lambda: None)
        eng = KyvernoJsonEngine()
        out = eng.evaluate({"id": "x"}, Path("/tmp/policies"), "cid-1")
        assert len(out) == 1
        assert out[0]["ruleId"] == "KJ_ENGINE_NOT_CONFIGURED"
        assert out[0]["result"] == "skipped"
        jsonschema.validate(out[0], PCR_SCHEMA)


class TestEvaluateWithKj:
    """Tests that run the real kj binary. Skip when kj is not installed."""

    @pytest.fixture(autouse=True)
    def _require_kj(self):
        if not _kj_installed():
            pytest.skip("kj not installed (scripts/install-kyverno-json.sh)")

    def test_smoke_policy_round_trip(self, tmp_path):
        eng = KyvernoJsonEngine()
        if not eng.is_configured():
            pytest.skip("kj not configured")
        # Use the real smoke policy dir.
        out = eng.evaluate({"id": "msvc"}, _smoke_policy_dir(), "cid-smoke")
        assert isinstance(out, list)
        assert len(out) >= 1
        for pcr in out:
            jsonschema.validate(pcr, PCR_SCHEMA)
            assert pcr["engine"] == "kyverno"
            assert pcr["contractId"] == "cid-smoke"

    def test_no_results_returns_pass(self, tmp_path):
        # An empty policy dir → no results → KJ_NO_RESULTS pass PCR.
        eng = KyvernoJsonEngine()
        empty_dir = tmp_path / "empty"
        empty_dir.mkdir()
        out = eng.evaluate({"id": "x"}, empty_dir, "cid-empty")
        assert len(out) == 1
        assert out[0]["ruleId"] == "KJ_NO_RESULTS"
        assert out[0]["result"] == "pass"


class TestDefensiveParsing:
    """Malformed kyverno-json output → error PCR, never exception."""

    def test_malformed_output_produces_error_pcr(self, monkeypatch):
        eng = KyvernoJsonEngine()
        # Mock is_configured → True, then mock subprocess to return
        # garbage output.
        monkeypatch.setattr(_mod, "_which_kj", lambda: "/fake/kj")
        monkeypatch.setattr(eng, "is_configured", lambda: True)

        class FakeProc:
            returncode = 0
            stdout = "not valid json {"
            stderr = ""

        def fake_run(*a, **kw):
            return FakeProc()

        monkeypatch.setattr(_mod.subprocess, "run", fake_run)
        out = eng.evaluate({"id": "x"}, _smoke_policy_dir(), "cid-bad")
        assert len(out) == 1
        assert out[0]["result"] == "error"
        assert out[0]["ruleId"] == "KJ_ENGINE_ERROR"
        jsonschema.validate(out[0], PCR_SCHEMA)

    def test_missing_policy_dir_produces_error_pcr(self, monkeypatch):
        eng = KyvernoJsonEngine()
        monkeypatch.setattr(_mod, "_which_kj", lambda: "/fake/kj")
        monkeypatch.setattr(eng, "is_configured", lambda: True)
        out = eng.evaluate({"id": "x"}, Path("/nonexistent/dir"), "cid-miss")
        assert len(out) == 1
        assert out[0]["result"] == "error"
        assert "not found" in out[0]["message"]