"""
Engine unit tests

Tests the PolicyEngine evaluate() function.
These tests are consistent across Go, TypeScript, and Python SDKs.
"""

import pytest

from highflame_policy import (
    PolicyEngine,
    EntityType,
    ActionType,
    ValidationLimits,
    EngineOptions,
    InputValidationError,
    DEFAULT_LIMITS,
)


PERMIT_ALL_POLICY = """permit(principal, action, resource);"""

DENY_ALL_POLICY = """forbid(principal, action, resource);"""

# Simple context-based policy without action constraint for testing context evaluation
CONTEXT_BASED_POLICY = """
@id("allow-production")
permit(
  principal,
  action,
  resource
)
when { context.environment == "production" };

@id("deny-all")
forbid(principal, action, resource);
"""


class TestBasicEvaluation:
    """Test basic policy evaluation."""

    def test_should_allow_when_permit_policy_matches(self):
        engine = PolicyEngine()
        engine.load_policy(PERMIT_ALL_POLICY)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test-scanner",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
        )

        assert decision.effect == "Allow"

    def test_should_deny_when_forbid_policy_matches(self):
        engine = PolicyEngine()
        engine.load_policy(DENY_ALL_POLICY)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test-scanner",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
        )

        assert decision.effect == "Deny"

    def test_should_deny_when_no_policies_match(self):
        engine = PolicyEngine()
        engine.load_policy("")  # No policies

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test-scanner",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
        )

        assert decision.effect == "Deny"


class TestContextBasedEvaluation:
    """Test context-based policy evaluation."""

    def test_should_allow_when_context_matches_permit_condition(self):
        # Use simple permit policy to test context evaluation in isolation
        simple_permit_policy = """
            permit(principal, action, resource)
            when { context.environment == "production" };
        """
        engine = PolicyEngine()
        engine.load_policy(simple_permit_policy)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="palisade",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
            context={"environment": "production"},
        )

        assert decision.effect == "Allow"

    def test_should_deny_when_context_does_not_match_permit_condition(self):
        engine = PolicyEngine()
        engine.load_policy(CONTEXT_BASED_POLICY)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="palisade",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
            context={"environment": "development"},
        )

        assert decision.effect == "Deny"

    def test_should_deny_when_context_is_missing_required_field(self):
        engine = PolicyEngine()
        engine.load_policy(CONTEXT_BASED_POLICY)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="palisade",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
            context={},  # Missing environment
        )

        assert decision.effect == "Deny"


class TestInputValidation:
    """Test input validation."""

    def test_should_accept_valid_context(self):
        engine = PolicyEngine()
        engine.load_policy(PERMIT_ALL_POLICY)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
            context={
                "environment": "production",
                "severity": "HIGH",
                "count": 42,
                "enabled": True,
            },
        )

        assert decision.effect == "Allow"

    def test_should_reject_context_with_too_many_keys(self):
        engine = PolicyEngine(
            options=EngineOptions(limits=ValidationLimits(max_context_keys=5))
        )
        engine.load_policy(PERMIT_ALL_POLICY)

        big_context = {f"key{i}": "value" for i in range(10)}

        with pytest.raises(InputValidationError):
            engine.evaluate(
                principal_type=EntityType.SCANNER,
                principal_id="test",
                action=ActionType.SCAN_ARTIFACT,
                resource_type=EntityType.ARTIFACT,
                resource_id="/model.safetensors",
                context=big_context,
            )

    def test_should_reject_context_with_too_long_strings(self):
        engine = PolicyEngine(
            options=EngineOptions(limits=ValidationLimits(max_string_length=100))
        )
        engine.load_policy(PERMIT_ALL_POLICY)

        long_string = "x" * 200

        with pytest.raises(InputValidationError):
            engine.evaluate(
                principal_type=EntityType.SCANNER,
                principal_id="test",
                action=ActionType.SCAN_ARTIFACT,
                resource_type=EntityType.ARTIFACT,
                resource_id="/model.safetensors",
                context={"value": long_string},
            )

    def test_should_reject_deeply_nested_context(self):
        engine = PolicyEngine(
            options=EngineOptions(limits=ValidationLimits(max_nesting_depth=3))
        )
        engine.load_policy(PERMIT_ALL_POLICY)

        deep_context = {
            "level1": {
                "level2": {
                    "level3": {
                        "level4": {
                            "level5": "too deep",
                        },
                    },
                },
            },
        }

        with pytest.raises(InputValidationError):
            engine.evaluate(
                principal_type=EntityType.SCANNER,
                principal_id="test",
                action=ActionType.SCAN_ARTIFACT,
                resource_type=EntityType.ARTIFACT,
                resource_id="/model.safetensors",
                context=deep_context,
            )

    def test_should_allow_skipping_validation(self):
        engine = PolicyEngine(
            options=EngineOptions(
                skip_validation=True,
                limits=ValidationLimits(max_context_keys=1),
            )
        )
        engine.load_policy(PERMIT_ALL_POLICY)

        # This would normally fail validation
        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
            context={"key1": "value1", "key2": "value2", "key3": "value3"},
        )

        assert decision.effect == "Allow"


class TestComplexContextTypes:
    """Test complex context types."""

    def test_should_handle_array_context_values(self):
        engine = PolicyEngine()
        engine.load_policy(PERMIT_ALL_POLICY)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
            context={"threats": ["malware", "backdoor", "injection"]},
        )

        assert decision.effect == "Allow"

    def test_should_handle_nested_object_context(self):
        engine = PolicyEngine()
        engine.load_policy(PERMIT_ALL_POLICY)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
            context={
                "metadata": {
                    "format": "safetensors",
                    "size": 1024,
                },
            },
        )

        assert decision.effect == "Allow"

    def test_should_handle_empty_context(self):
        # Cedar doesn't have null values - this tests that empty context works
        engine = PolicyEngine()
        engine.load_policy(PERMIT_ALL_POLICY)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
            context={},
        )

        assert decision.effect == "Allow"

    def test_should_handle_boolean_context_values(self):
        engine = PolicyEngine()
        engine.load_policy(PERMIT_ALL_POLICY)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
            context={"is_signed": True, "is_malicious": False},
        )

        assert decision.effect == "Allow"

    def test_should_handle_integer_context_values(self):
        # Cedar uses Long type for numbers (integers only, no floats)
        engine = PolicyEngine()
        engine.load_policy(PERMIT_ALL_POLICY)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
            context={"severity_score": 7, "count": 100},
        )

        assert decision.effect == "Allow"


class TestErrorHandling:
    """Test error handling."""

    def test_should_handle_invalid_policy_syntax_gracefully(self):
        engine = PolicyEngine()
        invalid_policy = "permit(principal, action, resource"  # Missing closing paren

        # Should not raise - just have empty policies
        try:
            engine.load_policy(invalid_policy)
        except Exception:
            pass  # May or may not error depending on cedarpy behavior

        # Evaluation should still work
        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
        )

        # Should deny when no valid policies
        assert decision.effect == "Deny"


class TestDeterminingPolicyAnnotations:
    """Test enriched determining policy annotations."""

    def test_should_include_all_annotations_in_determining_policy(self):
        engine = PolicyEngine()
        annotated_policy = """
            @id("block-secrets")
            @name("Block Secret Leaks")
            @severity("critical")
            @reject_message("Secrets detected in content")
            forbid(principal, action, resource)
            when { context.contains_secrets == true };
        """
        engine.load_policy(annotated_policy)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
            context={"contains_secrets": True},
        )

        assert decision.effect == "Deny"
        assert len(decision.determining_policies) == 1

        dp = decision.determining_policies[0]
        assert dp.id == "block-secrets"
        assert dp.annotations["name"] == "Block Secret Leaks"
        assert dp.annotations["severity"] == "critical"
        assert dp.annotations["reject_message"] == "Secrets detected in content"

    def test_should_return_empty_annotations_for_unannotated_policies(self):
        engine = PolicyEngine()
        engine.load_policy(PERMIT_ALL_POLICY)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
        )

        assert decision.effect == "Allow"
        assert len(decision.determining_policies) > 0
        # Unannotated policies should have empty annotations
        assert decision.determining_policies[0].annotations == {}

    def test_should_include_id_in_annotations_map(self):
        engine = PolicyEngine()
        policy = """
            @id("my-rule")
            @name("My Rule")
            permit(principal, action, resource);
        """
        engine.load_policy(policy)

        decision = engine.evaluate(
            principal_type=EntityType.SCANNER,
            principal_id="test",
            action=ActionType.SCAN_ARTIFACT,
            resource_type=EntityType.ARTIFACT,
            resource_id="/model.safetensors",
        )

        dp = decision.determining_policies[0]
        assert dp.id == "my-rule"
        assert dp.annotations["id"] == "my-rule"
        assert dp.annotations["name"] == "My Rule"


class TestDefaultLimits:
    """Test default limits."""

    def test_should_have_consistent_default_values(self):
        assert DEFAULT_LIMITS.max_context_keys == 100
        assert DEFAULT_LIMITS.max_string_length == 1_000_000
        assert DEFAULT_LIMITS.max_nesting_depth == 10
        assert DEFAULT_LIMITS.max_context_size_bytes == 10_000_000
