"""Tests for the Security Policy Engine v2 module."""

from __future__ import annotations

import json

import pytest

from mcpaudit.policy_engine_v2 import (
    PolicyV2,
    PolicyV2Action,
    PolicyV2Condition,
    PolicyV2Result,
    PolicyV2Rule,
    PolicyV2RuleResult,
    PolicyV2Verdict,
    evaluate_operator,
    evaluate_policy,
    format_policy_result_json,
    format_policy_result_text,
    get_builtin_org_policy,
    get_builtin_team_policy,
    get_supported_operators,
    merge_policies,
    merge_policy_chain,
    validate_policy,
)


# ===========================================================================
# Operators
# ===========================================================================

class TestOperators:
    def test_eq_strings(self):
        assert evaluate_operator("eq", "CRITICAL", "critical")

    def test_eq_exact(self):
        assert evaluate_operator("eq", 5, 5)

    def test_eq_false(self):
        assert not evaluate_operator("eq", "HIGH", "LOW")

    def test_neq_true(self):
        assert evaluate_operator("neq", "HIGH", "LOW")

    def test_neq_false(self):
        assert not evaluate_operator("neq", "HIGH", "high")

    def test_gt_true(self):
        assert evaluate_operator("gt", 10, 5)

    def test_gt_false(self):
        assert not evaluate_operator("gt", 3, 5)

    def test_gt_equal(self):
        assert not evaluate_operator("gt", 5, 5)

    def test_lt_true(self):
        assert evaluate_operator("lt", 3, 5)

    def test_lt_false(self):
        assert not evaluate_operator("lt", 10, 5)

    def test_gte_true(self):
        assert evaluate_operator("gte", 5, 5)
        assert evaluate_operator("gte", 10, 5)

    def test_gte_false(self):
        assert not evaluate_operator("gte", 3, 5)

    def test_lte_true(self):
        assert evaluate_operator("lte", 5, 5)
        assert evaluate_operator("lte", 3, 5)

    def test_lte_false(self):
        assert not evaluate_operator("lte", 10, 5)

    def test_in_true(self):
        assert evaluate_operator("in", "HIGH", ["HIGH", "CRITICAL"])

    def test_in_false(self):
        assert not evaluate_operator("in", "LOW", ["HIGH", "CRITICAL"])

    def test_in_non_list(self):
        assert not evaluate_operator("in", "x", "xyz")

    def test_not_in_true(self):
        assert evaluate_operator("not_in", "LOW", ["HIGH", "CRITICAL"])

    def test_not_in_false(self):
        assert not evaluate_operator("not_in", "HIGH", ["HIGH", "CRITICAL"])

    def test_not_in_non_list(self):
        assert evaluate_operator("not_in", "x", "xyz")

    def test_contains_list(self):
        assert evaluate_operator("contains", ["a", "b", "c"], "b")

    def test_contains_string(self):
        assert evaluate_operator("contains", "hello world", "world")

    def test_contains_false(self):
        assert not evaluate_operator("contains", ["a", "b"], "z")

    def test_matches_true(self):
        assert evaluate_operator("matches", "CMD-001", "^CMD-")

    def test_matches_false(self):
        assert not evaluate_operator("matches", "SQL-001", "^CMD-")

    def test_matches_invalid_regex(self):
        assert not evaluate_operator("matches", "test", "[invalid")

    def test_matches_non_string(self):
        assert not evaluate_operator("matches", 123, "^CMD-")

    def test_unknown_operator(self):
        assert not evaluate_operator("unknown_op", 1, 1)

    def test_gt_non_numeric(self):
        assert not evaluate_operator("gt", "abc", 5)

    def test_lt_non_numeric(self):
        assert not evaluate_operator("lt", "abc", 5)

    def test_get_supported_operators(self):
        ops = get_supported_operators()
        assert "eq" in ops
        assert "neq" in ops
        assert "gt" in ops
        assert "lt" in ops
        assert "gte" in ops
        assert "lte" in ops
        assert "in" in ops
        assert "not_in" in ops
        assert "contains" in ops
        assert "matches" in ops
        assert ops == sorted(ops)


# ===========================================================================
# PolicyV2Condition
# ===========================================================================

class TestPolicyV2Condition:
    def test_evaluate_eq(self):
        c = PolicyV2Condition("severity", "eq", "CRITICAL")
        assert c.evaluate({"severity": "CRITICAL"})

    def test_evaluate_missing_field(self):
        c = PolicyV2Condition("missing", "gt", 5)
        assert not c.evaluate({"other": 10})

    def test_evaluate_dotted_path(self):
        c = PolicyV2Condition("finding.severity", "eq", "HIGH")
        assert c.evaluate({"finding": {"severity": "HIGH"}})

    def test_to_dict(self):
        c = PolicyV2Condition("severity", "eq", "CRITICAL")
        d = c.to_dict()
        assert d["field"] == "severity"
        assert d["operator"] == "eq"
        assert d["value"] == "CRITICAL"

    def test_from_dict(self):
        c = PolicyV2Condition.from_dict({
            "field": "severity",
            "operator": "eq",
            "value": "HIGH",
        })
        assert c.field == "severity"
        assert c.operator == "eq"
        assert c.value == "HIGH"

    def test_from_dict_defaults(self):
        c = PolicyV2Condition.from_dict({})
        assert c.field == ""
        assert c.operator == "eq"
        assert c.value is None


# ===========================================================================
# PolicyV2Rule
# ===========================================================================

class TestPolicyV2Rule:
    def test_evaluate_all_match(self):
        rule = PolicyV2Rule(
            rule_id="R1", name="Test", description="",
            conditions=[
                PolicyV2Condition("severity", "eq", "CRITICAL"),
                PolicyV2Condition("rule_id", "matches", "^CMD-"),
            ],
            action=PolicyV2Action.BLOCK,
            match_mode="all",
        )
        assert rule.evaluate({"severity": "CRITICAL", "rule_id": "CMD-001"})

    def test_evaluate_all_no_match(self):
        rule = PolicyV2Rule(
            rule_id="R1", name="Test", description="",
            conditions=[
                PolicyV2Condition("severity", "eq", "CRITICAL"),
                PolicyV2Condition("rule_id", "matches", "^CMD-"),
            ],
            action=PolicyV2Action.BLOCK,
            match_mode="all",
        )
        assert not rule.evaluate({"severity": "LOW", "rule_id": "CMD-001"})

    def test_evaluate_any_match(self):
        rule = PolicyV2Rule(
            rule_id="R1", name="Test", description="",
            conditions=[
                PolicyV2Condition("severity", "eq", "CRITICAL"),
                PolicyV2Condition("severity", "eq", "HIGH"),
            ],
            action=PolicyV2Action.WARN,
            match_mode="any",
        )
        assert rule.evaluate({"severity": "HIGH"})

    def test_evaluate_any_no_match(self):
        rule = PolicyV2Rule(
            rule_id="R1", name="Test", description="",
            conditions=[
                PolicyV2Condition("severity", "eq", "CRITICAL"),
                PolicyV2Condition("severity", "eq", "HIGH"),
            ],
            action=PolicyV2Action.WARN,
            match_mode="any",
        )
        assert not rule.evaluate({"severity": "LOW"})

    def test_evaluate_disabled(self):
        rule = PolicyV2Rule(
            rule_id="R1", name="Test", description="",
            conditions=[PolicyV2Condition("severity", "eq", "CRITICAL")],
            action=PolicyV2Action.BLOCK,
            enabled=False,
        )
        assert not rule.evaluate({"severity": "CRITICAL"})

    def test_evaluate_no_conditions(self):
        rule = PolicyV2Rule(
            rule_id="R1", name="Test", description="",
            conditions=[],
            action=PolicyV2Action.BLOCK,
        )
        assert not rule.evaluate({"severity": "CRITICAL"})

    def test_to_dict(self):
        rule = PolicyV2Rule(
            rule_id="R1", name="Test", description="desc",
            conditions=[PolicyV2Condition("severity", "eq", "HIGH")],
            action=PolicyV2Action.BLOCK,
            severity="HIGH",
            tags=["test"],
        )
        d = rule.to_dict()
        assert d["rule_id"] == "R1"
        assert d["action"] == "BLOCK"
        assert len(d["conditions"]) == 1
        assert d["tags"] == ["test"]

    def test_from_dict(self):
        d = {
            "rule_id": "R1",
            "name": "Test",
            "conditions": [{"field": "severity", "operator": "eq", "value": "HIGH"}],
            "action": "WARN",
        }
        rule = PolicyV2Rule.from_dict(d)
        assert rule.rule_id == "R1"
        assert rule.action == PolicyV2Action.WARN
        assert len(rule.conditions) == 1

    def test_from_dict_defaults(self):
        rule = PolicyV2Rule.from_dict({})
        assert rule.rule_id == ""
        assert rule.action == PolicyV2Action.BLOCK
        assert rule.match_mode == "all"
        assert rule.enabled is True


# ===========================================================================
# PolicyV2
# ===========================================================================

class TestPolicyV2:
    def test_create_empty(self):
        p = PolicyV2(name="test")
        assert p.rule_count == 0
        assert p.enabled_rules == []

    def test_add_rule(self):
        p = PolicyV2(name="test")
        rule = PolicyV2Rule(
            rule_id="R1", name="Test", description="",
            conditions=[], action=PolicyV2Action.BLOCK,
        )
        p.add_rule(rule)
        assert p.rule_count == 1

    def test_add_rule_replaces(self):
        p = PolicyV2(name="test")
        r1 = PolicyV2Rule(rule_id="R1", name="V1", description="",
                          conditions=[], action=PolicyV2Action.BLOCK)
        r2 = PolicyV2Rule(rule_id="R1", name="V2", description="",
                          conditions=[], action=PolicyV2Action.WARN)
        p.add_rule(r1)
        p.add_rule(r2)
        assert p.rule_count == 1
        assert p.get_rule("R1").name == "V2"

    def test_remove_rule(self):
        p = PolicyV2(name="test")
        rule = PolicyV2Rule(rule_id="R1", name="Test", description="",
                            conditions=[], action=PolicyV2Action.BLOCK)
        p.add_rule(rule)
        assert p.remove_rule("R1")
        assert p.rule_count == 0

    def test_remove_rule_not_found(self):
        p = PolicyV2(name="test")
        assert not p.remove_rule("NONEXISTENT")

    def test_get_rule(self):
        p = PolicyV2(name="test")
        rule = PolicyV2Rule(rule_id="R1", name="Test", description="",
                            conditions=[], action=PolicyV2Action.BLOCK)
        p.add_rule(rule)
        assert p.get_rule("R1") is not None
        assert p.get_rule("R999") is None

    def test_enabled_rules(self):
        p = PolicyV2(name="test")
        p.add_rule(PolicyV2Rule(rule_id="R1", name="E", description="",
                                conditions=[], action=PolicyV2Action.BLOCK, enabled=True))
        p.add_rule(PolicyV2Rule(rule_id="R2", name="D", description="",
                                conditions=[], action=PolicyV2Action.BLOCK, enabled=False))
        assert len(p.enabled_rules) == 1

    def test_to_dict(self):
        p = PolicyV2(name="test", level="org", metadata={"key": "val"})
        d = p.to_dict()
        assert d["name"] == "test"
        assert d["level"] == "org"
        assert d["metadata"]["key"] == "val"

    def test_from_dict(self):
        d = {"name": "test", "level": "team", "rules": []}
        p = PolicyV2.from_dict(d)
        assert p.name == "test"
        assert p.level == "team"

    def test_to_json_from_json(self):
        p = PolicyV2(name="test", version="2.0")
        j = p.to_json()
        p2 = PolicyV2.from_json(j)
        assert p2.name == "test"
        assert p2.version == "2.0"


# ===========================================================================
# evaluate_policy
# ===========================================================================

class TestEvaluatePolicy:
    def _make_policy(self, rules):
        p = PolicyV2(name="test")
        p.rules = rules
        return p

    def test_block_critical(self):
        policy = self._make_policy([
            PolicyV2Rule(
                rule_id="P1", name="Block critical", description="",
                conditions=[PolicyV2Condition("severity", "eq", "CRITICAL")],
                action=PolicyV2Action.BLOCK,
                severity="CRITICAL",
            ),
        ])
        findings = [{"severity": "CRITICAL", "rule_id": "CMD-001"}]
        result = evaluate_policy(findings, policy)
        assert not result.ci_pass
        assert result.block_count == 1

    def test_warn_medium(self):
        policy = self._make_policy([
            PolicyV2Rule(
                rule_id="P1", name="Warn medium", description="",
                conditions=[PolicyV2Condition("severity", "eq", "MEDIUM")],
                action=PolicyV2Action.WARN,
                severity="MEDIUM",
            ),
        ])
        findings = [{"severity": "MEDIUM", "rule_id": "VAL-001"}]
        result = evaluate_policy(findings, policy)
        assert result.ci_pass
        assert result.warn_count == 1

    def test_allow_info(self):
        policy = self._make_policy([
            PolicyV2Rule(
                rule_id="P1", name="Allow info", description="",
                conditions=[PolicyV2Condition("severity", "eq", "INFO")],
                action=PolicyV2Action.ALLOW,
                severity="INFO",
            ),
        ])
        findings = [{"severity": "INFO", "rule_id": "LOG-001"}]
        result = evaluate_policy(findings, policy)
        assert result.ci_pass
        assert result.allow_count == 1

    def test_require_satisfied(self):
        policy = self._make_policy([
            PolicyV2Rule(
                rule_id="P1", name="Require auth", description="",
                conditions=[PolicyV2Condition("title", "contains", "auth")],
                action=PolicyV2Action.REQUIRE,
                severity="HIGH",
            ),
        ])
        findings = [{"severity": "INFO", "title": "auth check present"}]
        result = evaluate_policy(findings, policy)
        assert result.ci_pass
        assert result.required_missing_count == 0

    def test_require_missing(self):
        policy = self._make_policy([
            PolicyV2Rule(
                rule_id="P1", name="Require auth", description="",
                conditions=[PolicyV2Condition("title", "contains", "auth")],
                action=PolicyV2Action.REQUIRE,
                severity="HIGH",
            ),
        ])
        findings = [{"severity": "LOW", "title": "no match here"}]
        result = evaluate_policy(findings, policy)
        assert not result.ci_pass
        assert result.required_missing_count == 1

    def test_require_missing_no_findings(self):
        policy = self._make_policy([
            PolicyV2Rule(
                rule_id="P1", name="Require auth", description="",
                conditions=[PolicyV2Condition("title", "contains", "auth")],
                action=PolicyV2Action.REQUIRE,
                severity="HIGH",
            ),
        ])
        result = evaluate_policy([], policy)
        assert not result.ci_pass
        assert result.required_missing_count == 1

    def test_multiple_findings(self):
        policy = self._make_policy([
            PolicyV2Rule(
                rule_id="P1", name="Block critical", description="",
                conditions=[PolicyV2Condition("severity", "eq", "CRITICAL")],
                action=PolicyV2Action.BLOCK,
            ),
        ])
        findings = [
            {"severity": "CRITICAL", "rule_id": "CMD-001"},
            {"severity": "LOW", "rule_id": "LOG-001"},
        ]
        result = evaluate_policy(findings, policy)
        assert result.block_count == 1
        assert len(result.passed) == 1

    def test_empty_findings(self):
        policy = self._make_policy([
            PolicyV2Rule(
                rule_id="P1", name="Block critical", description="",
                conditions=[PolicyV2Condition("severity", "eq", "CRITICAL")],
                action=PolicyV2Action.BLOCK,
            ),
        ])
        result = evaluate_policy([], policy)
        assert result.ci_pass
        assert result.block_count == 0

    def test_empty_policy(self):
        policy = self._make_policy([])
        result = evaluate_policy([{"severity": "CRITICAL"}], policy)
        assert result.ci_pass
        assert result.total_evaluated == 0

    def test_disabled_rule_skipped(self):
        policy = self._make_policy([
            PolicyV2Rule(
                rule_id="P1", name="Disabled", description="",
                conditions=[PolicyV2Condition("severity", "eq", "CRITICAL")],
                action=PolicyV2Action.BLOCK,
                enabled=False,
            ),
        ])
        result = evaluate_policy([{"severity": "CRITICAL"}], policy)
        assert result.ci_pass

    def test_result_to_dict(self):
        policy = self._make_policy([
            PolicyV2Rule(
                rule_id="P1", name="Block", description="",
                conditions=[PolicyV2Condition("severity", "eq", "CRITICAL")],
                action=PolicyV2Action.BLOCK,
            ),
        ])
        result = evaluate_policy([{"severity": "CRITICAL"}], policy)
        d = result.to_dict()
        assert "ci_pass" in d
        assert "blocked" in d
        assert d["block_count"] == 1

    def test_rule_result_to_dict(self):
        rr = PolicyV2RuleResult(
            rule_id="P1", rule_name="Test",
            action=PolicyV2Action.BLOCK,
            verdict=PolicyV2Verdict.BLOCKED,
            severity="HIGH", message="blocked",
        )
        d = rr.to_dict()
        assert d["verdict"] == "BLOCKED"

    def test_allow_no_match_passes(self):
        policy = self._make_policy([
            PolicyV2Rule(
                rule_id="P1", name="Allow info", description="",
                conditions=[PolicyV2Condition("severity", "eq", "INFO")],
                action=PolicyV2Action.ALLOW,
            ),
        ])
        result = evaluate_policy([{"severity": "HIGH"}], policy)
        assert result.allow_count == 0
        assert len(result.passed) == 1


# ===========================================================================
# merge_policies
# ===========================================================================

class TestMergePolicies:
    def test_child_overrides_parent(self):
        parent = PolicyV2(name="org", level="org", rules=[
            PolicyV2Rule(rule_id="R1", name="Parent V1", description="",
                         conditions=[], action=PolicyV2Action.BLOCK),
        ])
        child = PolicyV2(name="team", level="team", rules=[
            PolicyV2Rule(rule_id="R1", name="Child V1", description="",
                         conditions=[], action=PolicyV2Action.WARN),
        ])
        merged = merge_policies(parent, child)
        assert merged.rule_count == 1
        assert merged.get_rule("R1").action == PolicyV2Action.WARN

    def test_parent_rules_inherited(self):
        parent = PolicyV2(name="org", level="org", rules=[
            PolicyV2Rule(rule_id="R1", name="P1", description="",
                         conditions=[], action=PolicyV2Action.BLOCK),
            PolicyV2Rule(rule_id="R2", name="P2", description="",
                         conditions=[], action=PolicyV2Action.WARN),
        ])
        child = PolicyV2(name="team", level="team", rules=[
            PolicyV2Rule(rule_id="R1", name="C1", description="",
                         conditions=[], action=PolicyV2Action.ALLOW),
        ])
        merged = merge_policies(parent, child)
        assert merged.rule_count == 2
        assert merged.get_rule("R1").action == PolicyV2Action.ALLOW
        assert merged.get_rule("R2").action == PolicyV2Action.WARN

    def test_merge_metadata(self):
        parent = PolicyV2(name="org", metadata={"env": "prod", "team": "a"})
        child = PolicyV2(name="team", metadata={"team": "b"})
        merged = merge_policies(parent, child)
        assert merged.metadata["env"] == "prod"
        assert merged.metadata["team"] == "b"

    def test_merge_preserves_child_name(self):
        parent = PolicyV2(name="org", level="org")
        child = PolicyV2(name="team", level="team")
        merged = merge_policies(parent, child)
        assert merged.name == "team"
        assert merged.level == "team"
        assert merged.parent == "org"

    def test_merge_child_name_empty_uses_parent(self):
        parent = PolicyV2(name="org")
        child = PolicyV2(name="")
        merged = merge_policies(parent, child)
        assert merged.name == "org"


class TestMergePolicyChain:
    def test_empty_chain(self):
        result = merge_policy_chain([])
        assert result.rule_count == 0

    def test_single_policy(self):
        p = PolicyV2(name="only", rules=[
            PolicyV2Rule(rule_id="R1", name="T", description="",
                         conditions=[], action=PolicyV2Action.BLOCK),
        ])
        result = merge_policy_chain([p])
        assert result.rule_count == 1

    def test_three_level_chain(self):
        org = PolicyV2(name="org", rules=[
            PolicyV2Rule(rule_id="R1", name="org-r1", description="",
                         conditions=[], action=PolicyV2Action.BLOCK),
            PolicyV2Rule(rule_id="R2", name="org-r2", description="",
                         conditions=[], action=PolicyV2Action.BLOCK),
        ])
        team = PolicyV2(name="team", rules=[
            PolicyV2Rule(rule_id="R1", name="team-r1", description="",
                         conditions=[], action=PolicyV2Action.WARN),
        ])
        project = PolicyV2(name="project", rules=[
            PolicyV2Rule(rule_id="R3", name="proj-r3", description="",
                         conditions=[], action=PolicyV2Action.ALLOW),
        ])
        result = merge_policy_chain([org, team, project])
        assert result.rule_count == 3
        assert result.get_rule("R1").name == "team-r1"
        assert result.get_rule("R2").name == "org-r2"
        assert result.get_rule("R3").name == "proj-r3"


# ===========================================================================
# validate_policy
# ===========================================================================

class TestValidatePolicy:
    def test_valid_policy(self):
        p = PolicyV2(name="test", level="org", rules=[
            PolicyV2Rule(
                rule_id="R1", name="Rule 1", description="",
                conditions=[PolicyV2Condition("severity", "eq", "HIGH")],
                action=PolicyV2Action.BLOCK,
            ),
        ])
        errors = validate_policy(p)
        assert len(errors) == 0

    def test_missing_name(self):
        p = PolicyV2(name="", level="org")
        errors = validate_policy(p)
        assert any("name" in e for e in errors)

    def test_invalid_level(self):
        p = PolicyV2(name="test", level="galaxy")
        errors = validate_policy(p)
        assert any("level" in e for e in errors)

    def test_duplicate_rule_ids(self):
        p = PolicyV2(name="test", rules=[
            PolicyV2Rule(rule_id="R1", name="A", description="",
                         conditions=[PolicyV2Condition("x", "eq", 1)],
                         action=PolicyV2Action.BLOCK),
            PolicyV2Rule(rule_id="R1", name="B", description="",
                         conditions=[PolicyV2Condition("x", "eq", 1)],
                         action=PolicyV2Action.WARN),
        ])
        errors = validate_policy(p)
        assert any("Duplicate" in e for e in errors)

    def test_missing_rule_id(self):
        p = PolicyV2(name="test", rules=[
            PolicyV2Rule(rule_id="", name="A", description="",
                         conditions=[], action=PolicyV2Action.BLOCK),
        ])
        errors = validate_policy(p)
        assert any("rule_id" in e for e in errors)

    def test_missing_rule_name(self):
        p = PolicyV2(name="test", rules=[
            PolicyV2Rule(rule_id="R1", name="", description="",
                         conditions=[PolicyV2Condition("x", "eq", 1)],
                         action=PolicyV2Action.BLOCK),
        ])
        errors = validate_policy(p)
        assert any("name" in e for e in errors)

    def test_unsupported_operator(self):
        p = PolicyV2(name="test", rules=[
            PolicyV2Rule(rule_id="R1", name="Rule", description="",
                         conditions=[PolicyV2Condition("x", "bananaSplit", 1)],
                         action=PolicyV2Action.BLOCK),
        ])
        errors = validate_policy(p)
        assert any("operator" in e for e in errors)

    def test_no_conditions(self):
        p = PolicyV2(name="test", rules=[
            PolicyV2Rule(rule_id="R1", name="Rule", description="",
                         conditions=[],
                         action=PolicyV2Action.BLOCK),
        ])
        errors = validate_policy(p)
        assert any("conditions" in e.lower() for e in errors)

    def test_missing_condition_field(self):
        p = PolicyV2(name="test", rules=[
            PolicyV2Rule(rule_id="R1", name="Rule", description="",
                         conditions=[PolicyV2Condition("", "eq", 1)],
                         action=PolicyV2Action.BLOCK),
        ])
        errors = validate_policy(p)
        assert any("field" in e for e in errors)

    def test_invalid_match_mode(self):
        p = PolicyV2(name="test", rules=[
            PolicyV2Rule(rule_id="R1", name="Rule", description="",
                         conditions=[PolicyV2Condition("x", "eq", 1)],
                         action=PolicyV2Action.BLOCK,
                         match_mode="none"),
        ])
        errors = validate_policy(p)
        assert any("match_mode" in e for e in errors)


# ===========================================================================
# Built-in policies
# ===========================================================================

class TestBuiltinPolicies:
    def test_org_policy_valid(self):
        p = get_builtin_org_policy()
        assert p.name == "mcpaudit-org-default"
        assert p.level == "org"
        assert p.rule_count > 0
        errors = validate_policy(p)
        assert len(errors) == 0

    def test_team_policy_valid(self):
        p = get_builtin_team_policy()
        assert p.name == "mcpaudit-team-default"
        assert p.level == "team"
        assert p.rule_count > 0
        errors = validate_policy(p)
        assert len(errors) == 0

    def test_org_blocks_critical(self):
        p = get_builtin_org_policy()
        result = evaluate_policy(
            [{"severity": "CRITICAL", "rule_id": "CMD-001", "title": "injection"}],
            p,
        )
        assert not result.ci_pass
        assert result.block_count >= 1

    def test_org_warns_medium(self):
        p = get_builtin_org_policy()
        # Only medium severity, no CMD/SQL pattern
        result = evaluate_policy(
            [{"severity": "MEDIUM", "rule_id": "VAL-001", "title": "validation"}],
            p,
        )
        assert result.warn_count >= 1

    def test_merge_org_team(self):
        org = get_builtin_org_policy()
        team = get_builtin_team_policy()
        merged = merge_policies(org, team)
        assert merged.rule_count > 0
        # Team rules + inherited org rules
        assert merged.get_rule("TEAM-001") is not None
        assert merged.get_rule("ORG-001") is not None


# ===========================================================================
# Formatters
# ===========================================================================

class TestFormatters:
    def _make_result(self):
        policy = PolicyV2(name="test", rules=[
            PolicyV2Rule(
                rule_id="P1", name="Block", description="",
                conditions=[PolicyV2Condition("severity", "eq", "CRITICAL")],
                action=PolicyV2Action.BLOCK, severity="CRITICAL",
            ),
        ])
        return evaluate_policy([{"severity": "CRITICAL"}], policy)

    def test_format_text(self):
        result = self._make_result()
        text = format_policy_result_text(result)
        assert "FAIL" in text
        assert "BLOCKED" in text or "Blocked" in text

    def test_format_json(self):
        result = self._make_result()
        j = format_policy_result_json(result)
        data = json.loads(j)
        assert "ci_pass" in data
        assert data["block_count"] == 1

    def test_format_text_pass(self):
        policy = PolicyV2(name="test", rules=[
            PolicyV2Rule(
                rule_id="P1", name="Warn", description="",
                conditions=[PolicyV2Condition("severity", "eq", "MEDIUM")],
                action=PolicyV2Action.WARN,
            ),
        ])
        result = evaluate_policy([{"severity": "LOW"}], policy)
        text = format_policy_result_text(result)
        assert "PASS" in text


# ===========================================================================
# Enums
# ===========================================================================

class TestEnums:
    def test_action_values(self):
        assert PolicyV2Action.BLOCK.value == "BLOCK"
        assert PolicyV2Action.WARN.value == "WARN"
        assert PolicyV2Action.ALLOW.value == "ALLOW"
        assert PolicyV2Action.REQUIRE.value == "REQUIRE"

    def test_verdict_values(self):
        assert PolicyV2Verdict.BLOCKED.value == "BLOCKED"
        assert PolicyV2Verdict.WARNED.value == "WARNED"
        assert PolicyV2Verdict.ALLOWED.value == "ALLOWED"
        assert PolicyV2Verdict.REQUIRED_MISSING.value == "REQUIRED_MISSING"
        assert PolicyV2Verdict.PASSED.value == "PASSED"
        assert PolicyV2Verdict.NOT_EVALUATED.value == "NOT_EVALUATED"
