"""
Dedicated test suite for tenuo._enforcement module.

This module tests the shared enforcement primitives used by all integrations:
- EnforcementResult
- DenialPolicy, DenialResult
- handle_denial()
- enforce_tool_call()
- filter_tools_by_warrant()

These tests are independent of any specific integration (LangGraph, CrewAI, etc.)
and focus on the core enforcement logic.
"""

import logging
from unittest.mock import MagicMock

import pytest

from tenuo import Authorizer, SigningKey, Warrant
from tenuo._enforcement import (
    DenialPolicy,
    DenialResult,
    EnforcementResult,
    _extract_violated_field,
    enforce_tool_call,
    filter_tools_by_warrant,
    handle_denial,
)
from tenuo.exceptions import (
    ConfigurationError,
    ConstraintViolation,
    ToolNotAuthorized,
)
from tenuo.schemas import ToolSchema

# =============================================================================
# Test Fixtures
# =============================================================================


@pytest.fixture
def signing_key():
    """Generate a fresh signing key for tests."""
    return SigningKey.generate()


@pytest.fixture
def basic_warrant(signing_key):
    """Create a basic warrant allowing 'search' tool."""
    return (
        Warrant.mint_builder()
        .capability("search")
        .holder(signing_key.public_key)
        .ttl(3600)
        .mint(signing_key)
    )


@pytest.fixture
def bound_warrant(basic_warrant, signing_key):
    """Create a bound warrant ready for enforcement."""
    return basic_warrant.bind(signing_key)


@pytest.fixture
def multi_tool_warrant(signing_key):
    """Create a warrant with multiple tools and constraints."""
    return (
        Warrant.mint_builder()
        .capability("search")
        .capability("read_file", path="/data/*")
        .capability("send_email")
        .holder(signing_key.public_key)
        .ttl(3600)
        .mint(signing_key)
    )


# =============================================================================
# EnforcementResult Tests
# =============================================================================


class TestEnforcementResult:
    """Tests for EnforcementResult dataclass."""

    def test_allowed_result_has_correct_fields(self):
        """Allowed result should have allowed=True and no denial info."""
        result = EnforcementResult(
            allowed=True,
            tool="search",
            arguments={"query": "test"},
            warrant_id="w123",
        )
        assert result.allowed is True
        assert result.tool == "search"
        assert result.arguments == {"query": "test"}
        assert result.denial_reason is None
        assert result.constraint_violated is None
        assert result.error_type is None
        assert result.warrant_id == "w123"

    def test_denied_result_has_denial_info(self):
        """Denied result should have allowed=False and denial details."""
        result = EnforcementResult(
            allowed=False,
            tool="delete_file",
            arguments={"path": "/etc/passwd"},
            denial_reason="Tool not in warrant",
            error_type="tool_not_allowed",
            warrant_id="w456",
        )
        assert result.allowed is False
        assert result.denial_reason == "Tool not in warrant"
        assert result.error_type == "tool_not_allowed"

    def test_raise_if_denied_does_nothing_when_allowed(self):
        """raise_if_denied should not raise for allowed results."""
        result = EnforcementResult(allowed=True, tool="search", arguments={})
        result.raise_if_denied()  # Should not raise

    def test_raise_if_denied_raises_tool_not_authorized(self):
        """raise_if_denied should raise ToolNotAuthorized for general denials."""
        result = EnforcementResult(
            allowed=False,
            tool="delete_file",
            arguments={},
            denial_reason="Tool not in warrant",
        )
        with pytest.raises(ToolNotAuthorized):
            result.raise_if_denied()

    def test_raise_if_denied_raises_constraint_violation(self):
        """raise_if_denied should raise ConstraintViolation when constraint_violated is set."""
        result = EnforcementResult(
            allowed=False,
            tool="read_file",
            arguments={"path": "/etc/passwd"},
            denial_reason="Path constraint violated",
            constraint_violated="path",
        )
        with pytest.raises(ConstraintViolation):
            result.raise_if_denied()


# =============================================================================
# DenialPolicy Tests
# =============================================================================


class TestDenialPolicy:
    """Tests for DenialPolicy constants."""

    def test_denial_policy_values(self):
        """DenialPolicy should have expected string values."""
        assert DenialPolicy.RAISE == "raise"
        assert DenialPolicy.LOG == "log"
        assert DenialPolicy.SKIP == "skip"

    def test_denial_policy_is_class_not_enum(self):
        """DenialPolicy should be a simple class with string constants (not Enum)."""
        # This is intentional for easier string comparison in config files
        assert isinstance(DenialPolicy.RAISE, str)
        assert isinstance(DenialPolicy.LOG, str)
        assert isinstance(DenialPolicy.SKIP, str)


# =============================================================================
# DenialResult Tests
# =============================================================================


class TestDenialResult:
    """Tests for DenialResult dataclass."""

    def test_denial_result_is_falsy(self):
        """DenialResult should be falsy (bool(denial) == False)."""
        denial = DenialResult(tool="test", reason="denied")
        assert not denial
        assert bool(denial) is False

    def test_denial_result_in_conditional(self):
        """DenialResult should work correctly in conditionals."""
        denial = DenialResult(tool="test", reason="denied")
        if denial:
            pytest.fail("DenialResult should be falsy in conditional")
        # Should reach here
        assert True

    def test_denial_result_from_enforcement(self):
        """DenialResult.from_enforcement should correctly convert EnforcementResult."""
        enforcement = EnforcementResult(
            allowed=False,
            tool="delete_file",
            arguments={"path": "/tmp"},
            denial_reason="Tool not authorized",
            error_type="tool_not_allowed",
            warrant_id="w789",
        )
        denial = DenialResult.from_enforcement(enforcement)

        assert denial.tool == "delete_file"
        assert denial.reason == "Tool not authorized"
        assert denial.error_type == "tool_not_allowed"
        assert denial.error_code == "TOOL_NOT_ALLOWED"  # Uppercased error_type
        assert denial.warrant_id == "w789"

    def test_denial_result_from_enforcement_default_reason(self):
        """DenialResult.from_enforcement should use default reason if not provided."""
        enforcement = EnforcementResult(
            allowed=False,
            tool="test",
            arguments={},
            denial_reason=None,
        )
        denial = DenialResult.from_enforcement(enforcement)
        assert denial.reason == "Authorization denied"

    def test_denial_result_from_enforcement_default_error_code(self):
        """DenialResult.from_enforcement should use DENIAL if error_type is None."""
        enforcement = EnforcementResult(
            allowed=False,
            tool="test",
            arguments={},
            error_type=None,
        )
        denial = DenialResult.from_enforcement(enforcement)
        assert denial.error_code == "DENIAL"


# =============================================================================
# handle_denial Tests
# =============================================================================


class TestHandleDenial:
    """Tests for handle_denial function."""

    def test_handle_denial_returns_none_when_allowed(self):
        """handle_denial should return None for allowed results."""
        result = EnforcementResult(allowed=True, tool="test", arguments={})
        denial = handle_denial(result, DenialPolicy.RAISE)
        assert denial is None

    def test_handle_denial_raise_mode_raises_default_exception(self):
        """handle_denial with RAISE policy should raise default exception."""
        result = EnforcementResult(
            allowed=False,
            tool="test",
            arguments={},
            denial_reason="Not allowed",
        )
        with pytest.raises(ToolNotAuthorized):
            handle_denial(result, DenialPolicy.RAISE)

    def test_handle_denial_raise_mode_uses_exception_factory(self):
        """handle_denial with RAISE and factory should use custom exception."""
        result = EnforcementResult(
            allowed=False,
            tool="test",
            arguments={},
        )

        class CustomError(Exception):
            pass

        def factory(r):
            return CustomError(f"Custom: {r.tool}")

        with pytest.raises(CustomError) as exc:
            handle_denial(result, DenialPolicy.RAISE, exception_factory=factory)
        assert "Custom: test" in str(exc.value)

    def test_handle_denial_log_mode_returns_denial_result(self):
        """handle_denial with LOG policy should return DenialResult."""
        result = EnforcementResult(
            allowed=False,
            tool="test",
            arguments={},
            denial_reason="Not allowed",
        )
        denial = handle_denial(result, DenialPolicy.LOG)
        assert isinstance(denial, DenialResult)
        assert denial.tool == "test"
        assert denial.reason == "Not allowed"

    def test_handle_denial_log_mode_logs_warning(self, caplog):
        """handle_denial with LOG policy should log a warning."""
        result = EnforcementResult(
            allowed=False,
            tool="test_tool",
            arguments={},
            denial_reason="Access denied",
        )
        with caplog.at_level(logging.WARNING, logger="tenuo.enforcement"):
            handle_denial(result, DenialPolicy.LOG)

        assert "test_tool" in caplog.text
        assert "Access denied" in caplog.text

    def test_handle_denial_skip_mode_returns_denial_result(self):
        """handle_denial with SKIP policy should return DenialResult."""
        result = EnforcementResult(
            allowed=False,
            tool="test",
            arguments={},
            denial_reason="Not allowed",
        )
        denial = handle_denial(result, DenialPolicy.SKIP)
        assert isinstance(denial, DenialResult)

    def test_handle_denial_skip_mode_logs_debug(self, caplog):
        """handle_denial with SKIP policy should log at debug level."""
        result = EnforcementResult(
            allowed=False,
            tool="test_tool",
            arguments={},
            denial_reason="Skipped",
        )
        with caplog.at_level(logging.DEBUG, logger="tenuo.enforcement"):
            handle_denial(result, DenialPolicy.SKIP)

        assert "test_tool" in caplog.text
        assert "Skipped" in caplog.text


# =============================================================================
# _extract_violated_field Tests
# =============================================================================


class TestExtractViolatedField:
    """Tests for _extract_violated_field helper function."""

    def test_extract_from_constraint_not_satisfied(self):
        """Should extract field from 'Constraint X not satisfied' pattern."""
        assert _extract_violated_field("Constraint 'path' not satisfied") == "path"

    def test_extract_from_constraint_violation(self):
        """Should extract field from 'X constraint violation' pattern."""
        assert _extract_violated_field("'amount' constraint violation") == "amount"

    def test_extract_from_range_exceeded(self):
        """Should extract field from 'Range exceeded for X' pattern."""
        assert _extract_violated_field("Range exceeded for 'count'") == "count"

    def test_extract_from_pattern_mismatch(self):
        """Should extract field from 'Pattern mismatch for X' pattern."""
        assert _extract_violated_field("Pattern mismatch for 'email'") == "email"

    def test_extract_from_field_pattern(self):
        """Should extract field from generic 'field X' pattern."""
        assert _extract_violated_field("Error in field 'name'") == "name"

    def test_returns_none_for_unmatched_pattern(self):
        """Should return None for unrecognized patterns."""
        assert _extract_violated_field("Some random error message") is None

    def test_returns_none_for_empty_string(self):
        """Should return None for empty string."""
        assert _extract_violated_field("") is None

    def test_returns_none_for_none_input(self):
        """Should return None for None input."""
        assert _extract_violated_field(None) is None

    def test_case_insensitive_matching(self):
        """Should match patterns case-insensitively."""
        assert _extract_violated_field("CONSTRAINT 'path' NOT SATISFIED") == "path"


# =============================================================================
# enforce_tool_call Tests
# =============================================================================


class TestEnforceToolCall:
    """Tests for enforce_tool_call function."""

    def test_requires_bound_warrant(self, basic_warrant):
        """enforce_tool_call should reject plain Warrant (not BoundWarrant)."""
        with pytest.raises(ConfigurationError) as exc:
            enforce_tool_call("search", {}, basic_warrant)  # Not bound!
        assert "Expected BoundWarrant" in str(exc.value)

    def test_allowed_tool_returns_success(self, bound_warrant, signing_key):
        """Should return allowed=True for authorized tool."""
        result = enforce_tool_call("search", {"query": "test"}, bound_warrant, trusted_roots=[signing_key.public_key])
        assert result.allowed is True
        assert result.tool == "search"
        assert result.arguments == {"query": "test"}
        assert result.warrant_id is not None

    def test_unlisted_tool_denied_by_rust_core(self, bound_warrant, signing_key):
        """Unlisted tool should be denied by Rust core."""
        result = enforce_tool_call("delete_file", {}, bound_warrant, trusted_roots=[signing_key.public_key])
        assert result.allowed is False
        assert "delete_file" in result.denial_reason or result.error_type == "tool_not_allowed"

    def test_application_allowlist_restricts_tools(self, bound_warrant):
        """Application allowed_tools should restrict beyond warrant."""
        # Warrant allows "search", but application only allows "other_tool"
        result = enforce_tool_call(
            "search",
            {},
            bound_warrant,
            allowed_tools=["other_tool"],
        )
        assert result.allowed is False
        assert "not in allowed list" in result.denial_reason
        assert result.error_type == "tool_not_allowed"

    def test_application_allowlist_passes_through_to_rust(self, bound_warrant, signing_key):
        """When tool is in application allowlist, Rust core still validates."""
        result = enforce_tool_call(
            "search",
            {"query": "test"},
            bound_warrant,
            allowed_tools=["search"],
            trusted_roots=[signing_key.public_key],
        )
        assert result.allowed is True

    def test_verify_mode_requires_precomputed_signature(self, bound_warrant):
        """verify_mode='verify' should require precomputed_signature."""
        with pytest.raises(ConfigurationError) as exc:
            enforce_tool_call(
                "search",
                {},
                bound_warrant,
                verify_mode="verify",
                # Missing precomputed_signature
            )
        assert "precomputed_signature is required" in str(exc.value)

    def test_expired_warrant_denied(self, signing_key):
        """Expired warrant should be denied by Rust core."""
        import time

        # Create warrant with very short TTL
        warrant = (
            Warrant.mint_builder()
            .capability("search")
            .holder(signing_key.public_key)
            .ttl(1)  # 1 second
            .mint(signing_key)
        )
        bound = warrant.bind(signing_key)

        # Wait for warrant to expire
        time.sleep(1.5)

        result = enforce_tool_call("search", {}, bound, trusted_roots=[signing_key.public_key])
        assert result.allowed is False
        # Error type should indicate expiration
        assert result.error_type in ("expired", "authorization_failed")

    def test_constraint_violation_denied(self, signing_key):
        """Constraint violation should be denied with details."""
        from tenuo import Pattern

        # Create warrant with path constraint
        warrant = (
            Warrant.mint_builder()
            .capability("read_file", path=Pattern("/data/*"))
            .holder(signing_key.public_key)
            .ttl(3600)
            .mint(signing_key)
        )
        bound = warrant.bind(signing_key)

        result = enforce_tool_call(
            "read_file",
            {"path": "/etc/passwd"},  # Violates /data/* constraint
            bound,
            trusted_roots=[signing_key.public_key],
        )
        assert result.allowed is False
        # Should capture constraint violation details

    def test_critical_tool_without_constraints_denied(self, signing_key):
        """Critical tools should require relevant constraints."""
        # Create warrant for delete_file (critical) without constraints
        warrant = (
            Warrant.mint_builder()
            .capability("delete_file")  # No path constraint
            .holder(signing_key.public_key)
            .ttl(3600)
            .mint(signing_key)
        )
        bound = warrant.bind(signing_key)

        # Define delete_file as critical with path constraint requirement
        schemas = {
            "delete_file": ToolSchema(
                risk_level="critical",
                recommended_constraints=["path"],
            )
        }

        result = enforce_tool_call(
            "delete_file",
            {"path": "/tmp/test"},
            bound,
            schemas=schemas,
            trusted_roots=[signing_key.public_key],
        )
        assert result.allowed is False
        assert result.error_type == "policy_violation"
        assert "path" in result.denial_reason

    def test_result_includes_warrant_id(self, bound_warrant, signing_key):
        """Result should include warrant_id for audit correlation."""
        result = enforce_tool_call("search", {}, bound_warrant, trusted_roots=[signing_key.public_key])
        # warrant_id should be present (even on success or failure)
        assert result.warrant_id is not None

    def test_tenuo_error_fails_closed(self, bound_warrant, signing_key):
        """TenuoError exceptions should result in denial (fail closed).

        This tests the error handling path for Tenuo-specific errors.
        The actual RuntimeError path is harder to test due to Rust bindings.
        """
        # Test by triggering an actual TenuoError condition
        # Using a tool not in warrant is the cleanest way
        result = enforce_tool_call("definitely_not_in_warrant", {}, bound_warrant, trusted_roots=[signing_key.public_key])
        assert result.allowed is False
        # Should be denied, demonstrating fail-closed behavior


# =============================================================================
# filter_tools_by_warrant Tests
# =============================================================================


class TestFilterToolsByWarrant:
    """Tests for filter_tools_by_warrant function."""

    def test_requires_bound_warrant(self, basic_warrant):
        """filter_tools_by_warrant should reject plain Warrant."""
        tools = [MagicMock(name="search")]
        with pytest.raises(ConfigurationError):
            filter_tools_by_warrant(tools, basic_warrant)  # Not bound!

    def test_filters_to_allowed_tools(self, signing_key):
        """Should filter to only tools in warrant."""
        warrant = (
            Warrant.mint_builder()
            .capability("search")
            .capability("read_file")
            .holder(signing_key.public_key)
            .ttl(3600)
            .mint(signing_key)
        )
        bound = warrant.bind(signing_key)

        tool1 = MagicMock()
        tool1.name = "search"
        tool2 = MagicMock()
        tool2.name = "read_file"
        tool3 = MagicMock()
        tool3.name = "delete_file"  # Not in warrant

        filtered = filter_tools_by_warrant([tool1, tool2, tool3], bound)

        assert len(filtered) == 2
        assert tool1 in filtered
        assert tool2 in filtered
        assert tool3 not in filtered

    def test_returns_all_when_no_restrictions(self, signing_key):
        """When warrant has wildcard tool access, all matching tools pass."""
        # Create warrant with wildcard tool access
        warrant = (
            Warrant.mint_builder()
            .capability("*")  # All tools via wildcard
            .holder(signing_key.public_key)
            .ttl(3600)
            .mint(signing_key)
        )
        bound = warrant.bind(signing_key)

        tool1 = MagicMock()
        tool1.name = "*"  # Will match the wildcard in warrant's tools list
        tool2 = MagicMock()
        tool2.name = "specific_tool"

        # Note: The warrant.tools property returns ["*"] for wildcard warrants.
        # filter_tools_by_warrant does exact matching, so only "*" named tools pass.
        # For UX filtering, this is acceptable - the Rust core handles wildcard matching.
        filtered = filter_tools_by_warrant([tool1, tool2], bound)

        # The wildcard behavior in filter_tools_by_warrant is for UX only.
        # If tools list is ["*"], only tools literally named "*" match.
        # This test validates that behavior is consistent.
        tools_in_warrant = bound.tools
        if tools_in_warrant is None:
            # No restrictions
            assert len(filtered) == 2
        else:
            # Exact matching applies
            assert tool1 in filtered or len(filtered) == 0

    def test_custom_name_extractor(self, bound_warrant):
        """Should use custom get_name function if provided."""

        class CustomTool:
            def __init__(self, tool_id):
                self.tool_id = tool_id

        tools = [CustomTool("search"), CustomTool("delete")]
        filtered = filter_tools_by_warrant(
            tools,
            bound_warrant,
            get_name=lambda t: t.tool_id,
        )

        # bound_warrant only allows "search"
        assert len(filtered) == 1
        assert filtered[0].tool_id == "search"

    def test_fallback_name_extraction(self, bound_warrant):
        """Should fallback to __name__ if no .name attribute."""

        def search():
            pass

        def delete():
            pass

        tools = [search, delete]
        filtered = filter_tools_by_warrant(tools, bound_warrant)

        # bound_warrant allows "search"
        assert len(filtered) == 1
        assert filtered[0].__name__ == "search"


# =============================================================================
# Integration Tests (enforce_tool_call with real warrants)
# =============================================================================


class TestEnforcementIntegration:
    """Integration tests ensuring enforcement works end-to-end."""

    def test_full_flow_allowed(self, signing_key):
        """Complete flow for allowed tool call."""
        warrant = (
            Warrant.mint_builder()
            .capability("search")
            .holder(signing_key.public_key)
            .ttl(3600)
            .mint(signing_key)
        )
        bound = warrant.bind(signing_key)

        result = enforce_tool_call("search", {"query": "AI papers"}, bound, trusted_roots=[signing_key.public_key])

        assert result.allowed is True
        assert result.tool == "search"
        assert result.arguments == {"query": "AI papers"}
        assert result.error_type is None
        assert result.denial_reason is None

    def test_full_flow_denied_constraint(self, signing_key):
        """Complete flow for denied tool call (constraint violation)."""
        from tenuo import Pattern

        warrant = (
            Warrant.mint_builder()
            .capability("read_file", path=Pattern("/data/*"))
            .holder(signing_key.public_key)
            .ttl(3600)
            .mint(signing_key)
        )
        bound = warrant.bind(signing_key)

        result = enforce_tool_call("read_file", {"path": "/etc/passwd"}, bound, trusted_roots=[signing_key.public_key])

        assert result.allowed is False
        # Rust core should catch the constraint violation

    def test_wildcard_tool_access(self, signing_key):
        """Wildcard capability should allow any tool."""
        warrant = (
            Warrant.mint_builder()
            .capability("*")
            .holder(signing_key.public_key)
            .ttl(3600)
            .mint(signing_key)
        )
        bound = warrant.bind(signing_key)

        result = enforce_tool_call("any_tool_name", {"arg": "value"}, bound, trusted_roots=[signing_key.public_key])
        assert result.allowed is True

    def test_wrong_signing_key_denied(self, signing_key):
        """Using wrong signing key should fail PoP."""
        warrant = (
            Warrant.mint_builder()
            .capability("search")
            .holder(signing_key.public_key)
            .ttl(3600)
            .mint(signing_key)
        )

        # Bind with DIFFERENT key
        wrong_key = SigningKey.generate()
        bound = warrant.bind(wrong_key)

        result = enforce_tool_call("search", {}, bound, trusted_roots=[signing_key.public_key])
        assert result.allowed is False


# =============================================================================
# Security Invariants
# =============================================================================


class TestSecurityInvariants:
    """Tests for critical security properties."""

    def test_fail_closed_on_unexpected_error(self, bound_warrant, signing_key):
        """System should deny on unexpected errors (fail closed).

        We verify this by checking that unauthorized tools are denied,
        demonstrating the fail-closed principle.
        """
        # Test with a tool not in the warrant
        result = enforce_tool_call("unknown_tool_xyz", {}, bound_warrant, trusted_roots=[signing_key.public_key])
        assert result.allowed is False
        # The system should deny rather than allow by default

    def test_denial_result_cannot_be_truthy(self):
        """DenialResult must always be falsy - security invariant."""
        denial = DenialResult(tool="x", reason="y")
        # This is critical - if DenialResult becomes truthy, security checks break
        assert not denial
        assert not bool(denial)

    def test_enforcement_result_immutable_behavior(self):
        """EnforcementResult fields should not be modified after creation."""
        result = EnforcementResult(
            allowed=False,
            tool="test",
            arguments={"key": "value"},
        )
        # Dataclass is frozen-like behavior via convention
        # Verify values remain stable
        assert result.allowed is False
        assert result.tool == "test"


# =============================================================================
# enforce_tool_call with Authorizer (full chain verification in verify mode)
# =============================================================================


class TestEnforceToolCallWithAuthorizer:
    """Tests for enforce_tool_call() with the authorizer= parameter.

    This covers the production path where a Remote PEP / FastAPI server
    receives a precomputed PoP signature and delegates full chain verification
    to a configured Authorizer instance.
    """

    @pytest.fixture
    def keypair(self):
        return SigningKey.generate()

    @pytest.fixture
    def authorizer(self, keypair):
        return Authorizer(trusted_roots=[keypair.public_key])

    @pytest.fixture
    def warrant_and_bound(self, keypair):
        warrant = (
            Warrant.mint_builder()
            .capability("search")
            .holder(keypair.public_key)
            .ttl(3600)
            .mint(keypair)
        )
        bound = warrant.bind(keypair)
        return warrant, bound

    def test_authorizer_verify_mode_success(self, keypair, authorizer, warrant_and_bound):
        """verify_mode='verify' with authorizer succeeds for valid PoP."""
        import time
        warrant, bound = warrant_and_bound
        pop_sig = warrant.sign(keypair, "search", {}, int(time.time()))

        result = enforce_tool_call(
            "search",
            {},
            bound,
            verify_mode="verify",
            precomputed_signature=bytes(pop_sig),
            authorizer=authorizer,
        )
        assert result.allowed is True
        assert result.tool == "search"

    def test_authorizer_verify_mode_rejects_untrusted_issuer(self, keypair):
        """Authorizer rejects warrant from untrusted issuer."""
        import time
        other_kp = SigningKey.generate()
        auth = Authorizer(trusted_roots=[other_kp.public_key])

        warrant = (
            Warrant.mint_builder()
            .capability("search")
            .holder(keypair.public_key)
            .ttl(3600)
            .mint(keypair)
        )
        bound = warrant.bind(keypair)
        pop_sig = warrant.sign(keypair, "search", {}, int(time.time()))

        result = enforce_tool_call(
            "search",
            {},
            bound,
            verify_mode="verify",
            precomputed_signature=bytes(pop_sig),
            authorizer=auth,
        )
        assert result.allowed is False
        assert result.error_type == "authorization_failed"
        assert "trusted" in result.denial_reason.lower() or "issuer" in result.denial_reason.lower()

    def test_authorizer_verify_mode_expired_warrant(self, keypair, authorizer):
        """Expired warrant is properly detected via authorizer path."""
        import time
        warrant = (
            Warrant.mint_builder()
            .capability("search")
            .holder(keypair.public_key)
            .ttl(1)
            .mint(keypair)
        )
        bound = warrant.bind(keypair)
        pop_sig = warrant.sign(keypair, "search", {}, int(time.time()))
        time.sleep(1.5)

        result = enforce_tool_call(
            "search",
            {},
            bound,
            verify_mode="verify",
            precomputed_signature=bytes(pop_sig),
            authorizer=authorizer,
        )
        assert result.allowed is False
        assert result.error_type == "expired"

    def test_authorizer_verify_mode_wrong_tool(self, keypair, authorizer, warrant_and_bound):
        """Authorizer rejects PoP for a tool not in the warrant."""
        import time
        warrant, bound = warrant_and_bound
        pop_sig = warrant.sign(keypair, "delete", {}, int(time.time()))

        result = enforce_tool_call(
            "delete",
            {},
            bound,
            verify_mode="verify",
            precomputed_signature=bytes(pop_sig),
            authorizer=authorizer,
        )
        assert result.allowed is False
        assert result.error_type == "authorization_failed"
