"""Tests for core/policy_engine.py (REQ-308, v1.25).

Protocol conformance, registry selection, NullEngine fallback,
unknown-engine KeyError, and the NullEngine-satisfies-Protocol
assertion (G-Q8a — proves the swap boundary is real without
implementing OPA).
"""

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

import pytest

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

import core.policy_engine as pe


class TestPolicyEngineProtocol:
    def test_null_engine_satisfies_protocol(self):
        # G-Q8a: NullEngine satisfies the PolicyEngine Protocol — proves
        # the swap boundary is real (a second engine implements it).
        eng = pe.NullEngine()
        assert isinstance(eng, pe.PolicyEngine)

    def test_null_engine_is_configured_false(self):
        assert pe.NullEngine().is_configured() is False

    def test_null_engine_evaluate_returns_skipped(self):
        out = pe.NullEngine().evaluate({}, Path("/tmp"), "cid-123")
        assert len(out) == 1
        pcr = out[0]
        assert pcr["ruleId"] == "NULL_ENGINE_INACTIVE"
        assert pcr["result"] == "skipped"
        assert pcr["engine"] == "kyverno"
        assert pcr["contractId"] == "cid-123"

    def test_null_engine_severity_is_info(self):
        out = pe.NullEngine().evaluate({}, Path("/tmp"), "cid")
        assert out[0]["severity"] == "info"


class TestRegistry:
    def test_register_and_get(self, tmp_path, monkeypatch):
        # Register a stub engine and verify get_engine() returns it.
        class StubEngine:
            name = "stub"

            def is_configured(self) -> bool:
                return True

            def evaluate(self, payload, policy_dir, contract_id):
                return [{"contractId": contract_id, "engine": "kyverno",
                         "ruleId": "STUB", "result": "pass", "severity": "info",
                         "message": "", "evaluatedAt": "t", "resourceRef": "",
                         "evidence": {}}]

        pe._REGISTRY.clear()
        pe.register("stub", StubEngine)
        monkeypatch.setattr(pe, "_load_config_policy", lambda: {"engine": "stub"})
        eng = pe.get_engine()
        assert eng.name == "stub"
        pe._REGISTRY.clear()
        pe._autoload_kyverno_json()

    def test_unknown_engine_raises_keyerror(self, monkeypatch):
        pe._REGISTRY.clear()
        monkeypatch.setattr(pe, "_load_config_policy",
                            lambda: {"engine": "nonexistent"})
        with pytest.raises(KeyError, match="Unknown policy engine"):
            pe.get_engine()
        pe._autoload_kyverno_json()

    def test_null_engine_fallback_when_policy_key_absent(self, monkeypatch):
        # G-Q4: policy key absent → NullEngine (distinct from kj-not-configured).
        monkeypatch.setattr(pe, "_load_config_policy", lambda: None)
        eng = pe.get_engine()
        assert isinstance(eng, pe.NullEngine)
        assert eng.is_configured() is False

    def test_kyverno_json_registered_via_autoload(self):
        # The autoload should register kyverno-json if the adapter file exists.
        pe._autoload_kyverno_json()
        assert "kyverno-json" in pe._REGISTRY or len(pe._REGISTRY) == 0


class TestConfigPolicyLoad:
    def test_load_config_policy_returns_dict(self):
        out = pe._load_config_policy()
        if out is not None:
            assert "engine" in out
            assert out["engine"] == "kyverno-json"

    def test_get_policy_root_is_path(self):
        root = pe.get_policy_root()
        assert isinstance(root, Path)
        assert root.name == "policies" or str(root).endswith("policies")


class TestKjNotConfiguredPath:
    """G-Q4: when policy key is present but kj is absent, the engine
    returns KJ_ENGINE_NOT_CONFIGURED (distinct from NullEngine's
    NULL_ENGINE_INACTIVE)."""

    def test_kj_not_configured_returns_distinct_ruleid(self, monkeypatch):
        # Force the registry to return KyvernoJsonEngine, then mock
        # `which kj` to return None.
        pe._autoload_kyverno_json()
        if "kyverno-json" not in pe._REGISTRY:
            pytest.skip("kyverno-json adapter not loadable in this env")
        monkeypatch.setattr(pe, "_load_config_policy",
                            lambda: {"engine": "kyverno-json"})
        eng = pe.get_engine()
        # Mock is_configured → False
        with mock.patch.object(eng, "is_configured", return_value=False):
            out = eng.evaluate({}, Path("/tmp"), "cid-456")
        assert len(out) == 1
        assert out[0]["ruleId"] == "KJ_ENGINE_NOT_CONFIGURED"
        assert out[0]["result"] == "skipped"
        assert out[0]["contractId"] == "cid-456"
        # Distinct from NullEngine
        assert out[0]["ruleId"] != "NULL_ENGINE_INACTIVE"