"""
Comprehensive tests for PoP enforcement, warrant types, and trust levels.

Tests cover:
- PoP enforcement in @guard decorator
- Context-based PoP (keypair context retrieval)
- Warrant type validation
- Trust level monotonicity
- Authorization failures
"""

import pytest
from tenuo import (
    Warrant,
    SigningKey,
    Pattern,
    guard,
    warrant_scope,
    key_scope,
    Exact,
)
from tenuo.exceptions import ScopeViolation
from tenuo.constraints import Constraints
from tenuo_core import Clearance


# ============================================================================
# PoP Enforcement Tests
# ============================================================================


def test_guard_requires_keypair_context():
    """Test that @guard enforces keypair context for PoP."""

    @guard(tool="test_tool")
    def protected_function(value: str) -> str:
        return f"processed: {value}"

    # Create warrant
    kp = SigningKey.generate()
    warrant = Warrant.mint(
        keypair=kp,
        capabilities=Constraints.for_tool("test_tool", {"value": Pattern("*")}),
        holder=kp.public_key,
        ttl_seconds=60,
    )

    # Should fail without keypair context
    from tenuo.exceptions import MissingSigningKey

    with warrant_scope(warrant):
        with pytest.raises(MissingSigningKey):
            protected_function(value="test")

    # Should succeed with keypair context
    with warrant_scope(warrant), key_scope(kp):
        result = protected_function(value="test")
        assert result == "processed: test"


def test_guard_with_explicit_keypair():
    """Test that @guard accepts explicit keypair parameter in decorator."""

    kp = SigningKey.generate()
    warrant = Warrant.mint(
        keypair=kp,
        capabilities=Constraints.for_tool("test_tool", {"value": Exact("allowed")}),
        holder=kp.public_key,
        ttl_seconds=60,
    )

    # Keypair passed to decorator explicitly
    @guard(tool="test_tool", keypair=kp)
    def protected_function(value: str) -> str:
        return f"processed: {value}"

    # Should work with explicit keypair in decorator
    with warrant_scope(warrant):
        result = protected_function(value="allowed")
        assert result == "processed: allowed"


def test_context_based_pop_retrieval():
    """Test that keypair context is properly retrieved and used for PoP."""

    @guard(tool="read_file")
    def read_file(path: str) -> str:
        return f"content of {path}"

    kp = SigningKey.generate()
    warrant = Warrant.mint(
        keypair=kp,
        capabilities=Constraints.for_tool("read_file", {"path": Pattern("/data/*")}),
        holder=kp.public_key,
        ttl_seconds=60,
    )

    # Set contexts
    with warrant_scope(warrant), key_scope(kp):
        # Verify contexts are set
        assert warrant_scope() is not None
        assert key_scope() is not None

        # Should work with contexts
        result = read_file(path="/data/test.txt")
        assert result == "content of /data/test.txt"


def test_pop_prevents_replay_attacks():
    """Test that PoP signatures include timestamps to prevent replay."""

    @guard(tool="delete_file")
    def delete_file(path: str) -> str:
        return f"deleted {path}"

    kp = SigningKey.generate()
    warrant = Warrant.mint(
        keypair=kp,
        capabilities=Constraints.for_tool("delete_file", {"path": Exact("/tmp/test.txt")}),
        holder=kp.public_key,
        ttl_seconds=60,
    )

    # Each call should generate a new PoP signature
    with warrant_scope(warrant), key_scope(kp):
        result1 = delete_file(path="/tmp/test.txt")
        result2 = delete_file(path="/tmp/test.txt")

        assert result1 == "deleted /tmp/test.txt"
        assert result2 == "deleted /tmp/test.txt"
        # PoP signatures are different each time (timestamp changes)


def test_pop_prevents_cross_tenant_misuse():
    """
    Critical security test: Tenant A mints warrant for Tenant B's agent,
    but Tenant A cannot use it because they don't possess Tenant B's private key.

    This proves PoP is strictly enforced - stolen or misdirected warrants are useless.
    """
    # Tenant A (control plane / issuer)
    tenant_a_kp = SigningKey.generate()

    # Tenant B's agent (intended holder)
    tenant_b_agent_kp = SigningKey.generate()

    # Tenant A mints a warrant FOR Tenant B's agent
    # The authorized_holder is set to Tenant B's public key
    warrant = Warrant.mint(
        keypair=tenant_a_kp,  # Signed by Tenant A
        capabilities=Constraints.for_tool("sensitive_operation", {"resource": Exact("secret-data")}),
        holder=tenant_b_agent_kp.public_key,  # For Tenant B's agent
        ttl_seconds=3600,
    )

    # Verify the warrant was created correctly
    assert warrant.authorized_holder.to_bytes() == tenant_b_agent_kp.public_key.to_bytes()
    assert warrant.issuer.to_bytes() == tenant_a_kp.public_key.to_bytes()

    # ATTACK SCENARIO: Tenant A tries to use the warrant with their OWN keypair
    # This MUST fail - they don't have Tenant B's private key

    @guard(tool="sensitive_operation")
    def sensitive_operation(resource: str) -> str:
        return f"accessed {resource}"

    # Tenant A tries to use their own keypair (wrong key)
    with warrant_scope(warrant), key_scope(tenant_a_kp):
        try:
            sensitive_operation(resource="secret-data")
            assert False, "Should have raised ScopeViolation - wrong keypair!"
        except ScopeViolation:
            # Expected: Authorization denied because PoP signature doesn't match authorized_holder
            # The specific error message may vary, but access MUST be denied
            pass  # Success - access was denied

    # SUCCESS SCENARIO: Tenant B's agent uses the warrant with THEIR keypair
    with warrant_scope(warrant), key_scope(tenant_b_agent_kp):
        result = sensitive_operation(resource="secret-data")
        assert result == "accessed secret-data"


def test_pop_signature_must_match_authorized_holder():
    """
    Verify that PoP verification strictly checks the authorized_holder.
    Using any other keypair must fail, even if it's a valid Ed25519 signature.
    """
    correct_kp = SigningKey.generate()
    wrong_kp = SigningKey.generate()

    warrant = Warrant.mint(
        keypair=correct_kp,
        capabilities=Constraints.for_tool("test_tool", {"action": Exact("test")}),
        holder=correct_kp.public_key,
        ttl_seconds=60,
    )

    args = {"action": "test"}

    # Create PoP with WRONG keypair
    wrong_pop = warrant.sign(wrong_kp, "test_tool", args)

    # Try to authorize with wrong signature - MUST fail
    result = warrant.authorize("test_tool", args, signature=bytes(wrong_pop))
    assert result is False, "Wrong keypair should NOT be accepted!"

    # Create PoP with CORRECT keypair
    correct_pop = warrant.sign(correct_kp, "test_tool", args)

    # Authorize with correct signature - should succeed
    result = warrant.authorize("test_tool", args, signature=bytes(correct_pop))
    assert result is True


# ============================================================================
# Warrant Type Tests
# ============================================================================


def test_warrant_has_depth_property():
    """Test that warrants expose depth property."""

    kp = SigningKey.generate()

    # Root warrant has depth 0
    root = Warrant.mint(
        keypair=kp, capabilities=Constraints.for_tool("test_tool", {}), holder=kp.public_key, ttl_seconds=60
    )
    assert root.depth == 0

    # Child warrant has depth 1
    child = root.attenuate(
        capabilities=Constraints.for_tool("test_tool", {}),
        signing_key=kp,  # kp signs (they hold root)
        holder=kp.public_key,
        ttl_seconds=30,
    )
    assert child.depth == 1


def test_warrant_chain_verification():
    """Test that warrant chains are properly verified."""

    control_kp = SigningKey.generate()
    worker_kp = SigningKey.generate()

    # Root warrant
    root = Warrant.mint(
        keypair=control_kp,
        capabilities=Constraints.for_tool("file_ops", {"path": Pattern("/data/*")}),
        holder=control_kp.public_key,
        ttl_seconds=3600,
    )

    # Attenuated warrant
    child = root.attenuate(
        capabilities=Constraints.for_tool("file_ops", {"path": Pattern("/data/reports/*")}),
        signing_key=control_kp,  # control_kp signs (they hold root)
        holder=worker_kp.public_key,
        ttl_seconds=60,
    )

    # Verify chain
    assert child.depth == 1
    assert child.authorized_holder.to_bytes() == worker_kp.public_key.to_bytes()

    # Child should have narrower constraints
    @guard(tool="file_ops")
    def access_file(path: str) -> str:
        return f"accessed {path}"

    with warrant_scope(child), key_scope(worker_kp):
        # Should work for narrower path
        result = access_file(path="/data/reports/q3.pdf")
        assert result == "accessed /data/reports/q3.pdf"

        # Should fail for broader path
        with pytest.raises(ScopeViolation):
            access_file(path="/data/other/file.txt")


# ============================================================================
# Trust Level Tests
# ============================================================================


def test_clearance_monotonicity():
    """Test that clearance levels can only decrease during delegation."""

    kp = SigningKey.generate()

    # Create warrant with Internal trust level (value 30)
    root = Warrant.mint(
        keypair=kp,
        capabilities=Constraints.for_tool("test_tool", {}),
        holder=kp.public_key,
        ttl_seconds=3600,
        clearance=Clearance.INTERNAL,
    )

    assert root.clearance.value() == Clearance.INTERNAL.value()

    # Attenuate with lower trust level using builder pattern (POLA: inherit_all first)
    builder = root.grant_builder()
    builder.inherit_all()
    builder.ttl(60)
    builder.holder(kp.public_key)
    builder.clearance(Clearance.EXTERNAL)
    child = builder.grant(kp)

    assert child.clearance.value() == Clearance.EXTERNAL.value()

    # Trust level decreased (Internal -> External)
    assert child.clearance < root.clearance


def test_clearance_enforcement():
    """Test that operations respect clearance boundaries."""

    kp = SigningKey.generate()

    # Create warrant with External trust level (value 10)
    warrant = Warrant.mint(
        keypair=kp,
        capabilities=Constraints.for_tool("read_data", {"sensitivity": Exact("public")}),
        holder=kp.public_key,
        ttl_seconds=60,
        clearance=Clearance.EXTERNAL,
    )

    @guard(tool="read_data")
    def read_data(sensitivity: str) -> str:
        return f"data with sensitivity: {sensitivity}"

    with warrant_scope(warrant), key_scope(kp):
        result = read_data(sensitivity="public")
        assert result == "data with sensitivity: public"


def test_clearance_hierarchy():
    """Test that clearance levels follow the correct hierarchy."""

    # Verify trust level ordering (using string names)
    untrusted = Clearance.UNTRUSTED
    external = Clearance.EXTERNAL
    partner = Clearance.PARTNER
    internal = Clearance.INTERNAL
    privileged = Clearance.PRIVILEGED
    system = Clearance.SYSTEM

    # Verify hierarchy
    assert untrusted < external
    assert external < partner
    assert partner < internal
    assert internal < privileged
    assert privileged < system


# ============================================================================
# Context Management Tests
# ============================================================================


def test_context_isolation():
    """Test that contexts are properly isolated between calls."""

    kp1 = SigningKey.generate()
    kp2 = SigningKey.generate()

    warrant1 = Warrant.mint(
        keypair=kp1, capabilities=Constraints.for_tool("tool1", {}), holder=kp1.public_key, ttl_seconds=60
    )

    warrant2 = Warrant.mint(
        keypair=kp2, capabilities=Constraints.for_tool("tool2", {}), holder=kp2.public_key, ttl_seconds=60
    )

    # Set first context
    with warrant_scope(warrant1), key_scope(kp1):
        assert warrant_scope() is not None
        assert key_scope() is not None

    # Context should be cleared
    assert warrant_scope() is None
    assert key_scope() is None

    # Set second context
    with warrant_scope(warrant2), key_scope(kp2):
        # Should be different contexts
        assert warrant_scope() is not None
        assert key_scope() is not None


def test_nested_contexts():
    """Test that nested contexts work correctly."""

    kp = SigningKey.generate()

    warrant1 = Warrant.mint(
        keypair=kp, capabilities=Constraints.for_tool("tool1", {}), holder=kp.public_key, ttl_seconds=60
    )

    warrant2 = Warrant.mint(
        keypair=kp, capabilities=Constraints.for_tool("tool2", {}), holder=kp.public_key, ttl_seconds=60
    )

    # Outer context
    with warrant_scope(warrant1):
        outer_warrant = warrant_scope()
        assert outer_warrant is not None

        # Inner context
        with warrant_scope(warrant2):
            inner_warrant = warrant_scope()
            assert inner_warrant is not None
            # Inner context takes precedence

        # Back to outer context
        restored_warrant = warrant_scope()
        assert restored_warrant is not None


# ============================================================================
# Authorization Failure Tests
# ============================================================================


def test_authorization_fails_without_warrant():
    """Test that authorization fails when no warrant is in context."""

    @guard(tool="test_tool")
    def protected_function(value: str) -> str:
        return f"processed: {value}"

    kp = SigningKey.generate()

    # Should fail without warrant context
    with key_scope(kp):
        with pytest.raises(ScopeViolation, match="No warrant"):
            protected_function(value="test")


def test_authorization_fails_with_wrong_tool():
    """Test that authorization fails when warrant tool doesn't match."""

    @guard(tool="correct_tool")
    def protected_function(value: str) -> str:
        return f"processed: {value}"

    kp = SigningKey.generate()

    # Create warrant for wrong tool
    warrant = Warrant.mint(
        keypair=kp,
        capabilities=Constraints.for_tool("wrong_tool", {"value": Pattern("*")}),
        holder=kp.public_key,
        ttl_seconds=60,
    )

    with warrant_scope(warrant), key_scope(kp):
        with pytest.raises(ScopeViolation):
            protected_function(value="test")


def test_authorization_fails_with_constraint_violation():
    """Test that authorization fails when constraints are violated."""

    @guard(tool="read_file")
    def read_file(path: str) -> str:
        return f"content of {path}"

    kp = SigningKey.generate()

    # Create warrant with specific path constraint
    warrant = Warrant.mint(
        keypair=kp,
        capabilities=Constraints.for_tool("read_file", {"path": Pattern("/allowed/*")}),
        holder=kp.public_key,
        ttl_seconds=60,
    )

    with warrant_scope(warrant), key_scope(kp):
        # Should work for allowed path
        result = read_file(path="/allowed/file.txt")
        assert result == "content of /allowed/file.txt"

        # Should fail for disallowed path
        with pytest.raises(ScopeViolation):
            read_file(path="/forbidden/file.txt")


def test_authorization_fails_with_expired_warrant():
    """Test that authorization fails when warrant has expired."""

    @guard(tool="test_tool")
    def protected_function(value: str) -> str:
        return f"processed: {value}"

    kp = SigningKey.generate()

    # Create warrant with very short TTL
    warrant = Warrant.mint(
        keypair=kp,
        capabilities=Constraints.for_tool("test_tool", {"value": Pattern("*")}),
        holder=kp.public_key,
        ttl_seconds=0,  # Expires immediately
    )

    import time

    time.sleep(1)  # Wait for expiration

    with warrant_scope(warrant), key_scope(kp):
        with pytest.raises(ScopeViolation):
            protected_function(value="test")
