"""
Comprehensive tests for ID Wispera Python SDK parity modules:
  - Policy engine
  - Delegation chain management
  - Secure sharing
  - Credential locations registry
  - Provisioning providers

All tests run without external dependencies (no network calls, no filesystem
access for credential scanning).
"""

from datetime import datetime, timedelta
from unittest.mock import patch

import pytest

from id_wispera.types import (
    Passport,
    PassportStatus,
    VisaType,
    CredentialType,
    Platform,
    DelegationHop,
    RiskLevel,
    ShareScope,
    ShareOptions,
    PolicyEffect,
    PolicyCondition,
    PolicyRule,
    PolicyDecision,
    ValidationResult,
)


# ============================================================================
# Helpers -- reusable passport factories
# ============================================================================


def _make_passport(**overrides) -> Passport:
    """Create a minimal valid Passport with sensible defaults.

    Any keyword argument will override the default value.
    """
    defaults = dict(
        name="Test API Key",
        credential_type=CredentialType.API_KEY,
        credential_value="sk-test-abc123",
        visa_type=VisaType.ACCESS,
        platforms=[Platform.OPENAI],
        human_owner="alice@example.com",
        scope=["read", "write"],
        status=PassportStatus.ACTIVE,
        valid_from=datetime(2025, 1, 1),
        valid_until=datetime(2025, 3, 1),
        delegation_chain=[],
        tags=["test"],
    )
    defaults.update(overrides)
    return Passport(**defaults)


def _make_delegation_hop(
    from_entity: str,
    to: str,
    granted_at: datetime | None = None,
    scope: list[str] | None = None,
    expires_at: datetime | None = None,
) -> DelegationHop:
    """Create a DelegationHop with sensible defaults."""
    return DelegationHop(
        from_entity=from_entity,
        to=to,
        granted_at=granted_at or datetime.utcnow(),
        scope=scope,
        expires_at=expires_at,
    )


# ############################################################################
#
#  1. POLICY MODULE TESTS
#
# ############################################################################

from id_wispera.policy import (
    DEFAULT_POLICY_RULES,
    evaluate_policy,
    validate_passport,
    create_policy_rule,
    merge_policy_rules,
    get_enabled_rules,
    has_valid_human_owner,
    is_within_validity_period,
    has_excessive_delegation,
    has_privileged_access,
    PolicyBuilder,
    policy,
)


class TestDefaultPolicyRules:
    """Tests for DEFAULT_POLICY_RULES constant."""

    def test_default_rules_is_a_list(self):
        assert isinstance(DEFAULT_POLICY_RULES, list)

    def test_default_rules_not_empty(self):
        assert len(DEFAULT_POLICY_RULES) > 0

    def test_each_rule_is_a_policy_rule(self):
        for rule in DEFAULT_POLICY_RULES:
            assert isinstance(rule, PolicyRule)

    def test_known_rule_ids_present(self):
        ids = {r.id for r in DEFAULT_POLICY_RULES}
        assert "require-human-owner" in ids
        assert "max-validity-90-days" in ids
        assert "max-delegation-depth-3" in ids
        assert "privilege-visa-requires-approval" in ids

    def test_all_default_rules_enabled(self):
        for rule in DEFAULT_POLICY_RULES:
            assert rule.enabled is True

    def test_require_human_owner_rule_is_deny(self):
        rule = next(r for r in DEFAULT_POLICY_RULES if r.id == "require-human-owner")
        assert rule.effect == PolicyEffect.DENY

    def test_privilege_visa_rule_is_require_approval(self):
        rule = next(
            r for r in DEFAULT_POLICY_RULES
            if r.id == "privilege-visa-requires-approval"
        )
        assert rule.effect == PolicyEffect.REQUIRE_APPROVAL


class TestEvaluatePolicy:
    """Tests for evaluate_policy()."""

    def test_allowed_passport_with_allow_only_rules(self):
        """With only ALLOW-effect rules, evaluation permits the passport."""
        allow_rule = PolicyRule(
            id="allow-all",
            name="Allow All",
            description="Explicitly allow",
            condition=PolicyCondition(),
            effect=PolicyEffect.ALLOW,
            priority=100,
            enabled=True,
        )
        passport = _make_passport()
        decision = evaluate_policy(passport, "access", rules=[allow_rule])
        assert isinstance(decision, PolicyDecision)
        assert decision.allowed is True
        assert decision.effect == PolicyEffect.ALLOW

    def test_default_rules_always_match_and_deny(self):
        """Default rules use a fallthrough-True design: all rules match every
        passport, so the DENY-effect rules always apply.  A well-formed
        passport is still denied by the default rule set because _condition_matches
        returns True for every rule."""
        passport = _make_passport()
        decision = evaluate_policy(passport, "access")
        assert isinstance(decision, PolicyDecision)
        assert decision.effect == PolicyEffect.DENY

    def test_denied_passport_missing_human_owner(self):
        passport = _make_passport(human_owner="")
        decision = evaluate_policy(passport, "access")
        assert decision.allowed is False
        assert decision.effect == PolicyEffect.DENY

    def test_denied_passport_whitespace_human_owner(self):
        passport = _make_passport(human_owner="   ")
        decision = evaluate_policy(passport, "access")
        assert decision.allowed is False

    def test_privilege_visa_requires_approval_isolated(self):
        """When evaluated with only the privilege-visa rule, a PRIVILEGE
        passport gets REQUIRE_APPROVAL."""
        privilege_rule = next(
            r for r in DEFAULT_POLICY_RULES
            if r.id == "privilege-visa-requires-approval"
        )
        passport = _make_passport(visa_type=VisaType.PRIVILEGE)
        decision = evaluate_policy(passport, "access", rules=[privilege_rule])
        assert decision.effect == PolicyEffect.REQUIRE_APPROVAL
        assert decision.allowed is False

    def test_excessive_delegation_denied(self):
        hops = [
            _make_delegation_hop(f"entity-{i}", f"entity-{i + 1}")
            for i in range(4)
        ]
        passport = _make_passport(delegation_chain=hops)
        decision = evaluate_policy(passport, "access")
        assert decision.allowed is False
        assert decision.effect == PolicyEffect.DENY

    def test_excessive_validity_period_denied(self):
        passport = _make_passport(
            valid_from=datetime(2025, 1, 1),
            valid_until=datetime(2025, 7, 1),  # ~181 days > 90
        )
        decision = evaluate_policy(passport, "access")
        assert decision.allowed is False
        assert decision.effect == PolicyEffect.DENY

    def test_custom_rules_override_defaults(self):
        custom_rules = [
            PolicyRule(
                id="block-all",
                name="Block All",
                description="Deny everything for testing",
                condition=PolicyCondition(),
                effect=PolicyEffect.DENY,
                priority=200,
                enabled=True,
            )
        ]
        passport = _make_passport()
        decision = evaluate_policy(passport, "access", rules=custom_rules)
        assert decision.allowed is False
        assert decision.effect == PolicyEffect.DENY

    def test_empty_rules_allows_everything(self):
        passport = _make_passport(human_owner="")
        decision = evaluate_policy(passport, "access", rules=[])
        assert decision.allowed is True
        assert decision.effect == PolicyEffect.ALLOW

    def test_matched_rules_populated(self):
        passport = _make_passport(visa_type=VisaType.PRIVILEGE)
        decision = evaluate_policy(passport, "access")
        assert len(decision.matched_rules) > 0
        rule_ids = [r.id for r in decision.matched_rules]
        assert "privilege-visa-requires-approval" in rule_ids

    def test_reason_populated_on_deny(self):
        passport = _make_passport(human_owner="")
        decision = evaluate_policy(passport, "access")
        assert len(decision.reason) > 0
        assert "Require Human Owner" in decision.reason

    def test_reason_populated_on_allow(self):
        """With no matching deny/require-approval rules, the reason includes
        'allowed'."""
        passport = _make_passport()
        decision = evaluate_policy(passport, "access", rules=[])
        assert "allowed" in decision.reason.lower()

    def test_deny_takes_precedence_over_require_approval(self):
        """When both DENY and REQUIRE_APPROVAL match, DENY wins."""
        passport = _make_passport(
            visa_type=VisaType.PRIVILEGE,
            human_owner="",
        )
        decision = evaluate_policy(passport, "access")
        assert decision.effect == PolicyEffect.DENY

    def test_higher_priority_evaluated_first(self):
        """Rules are sorted by priority descending before evaluation."""
        low_priority = PolicyRule(
            id="low",
            name="Low Priority Allow",
            description="Allow at low priority",
            condition=PolicyCondition(),
            effect=PolicyEffect.ALLOW,
            priority=10,
            enabled=True,
        )
        high_priority = PolicyRule(
            id="high",
            name="High Priority Deny",
            description="Deny at high priority",
            condition=PolicyCondition(),
            effect=PolicyEffect.DENY,
            priority=100,
            enabled=True,
        )
        passport = _make_passport()
        decision = evaluate_policy(passport, "access", rules=[low_priority, high_priority])
        assert decision.effect == PolicyEffect.DENY

    def test_disabled_rule_not_evaluated(self):
        rule = PolicyRule(
            id="disabled-deny",
            name="Disabled Deny",
            description="Should not fire",
            condition=PolicyCondition(),
            effect=PolicyEffect.DENY,
            priority=200,
            enabled=False,
        )
        passport = _make_passport()
        decision = evaluate_policy(passport, "access", rules=[rule])
        assert decision.allowed is True

    def test_no_valid_until_skips_validity_violation(self):
        """When valid_until is None, validate_passport should not produce a
        validity-period violation, even though the rule still matches in
        evaluate_policy (due to fallthrough-True)."""
        passport = _make_passport(valid_until=None)
        # Use validate_passport to verify the *specific* violation is absent
        violations = validate_passport(passport)
        assert not any(
            "validity period" in v.violation.lower() for v in violations
        )

    def test_scope_condition_matches(self):
        rule = PolicyRule(
            id="scope-check",
            name="Scope Check",
            description="Deny if scope includes 'write'",
            condition=PolicyCondition(scopes=["write"]),
            effect=PolicyEffect.DENY,
            priority=100,
            enabled=True,
        )
        passport = _make_passport(scope=["read", "write"])
        decision = evaluate_policy(passport, "access", rules=[rule])
        assert decision.effect == PolicyEffect.DENY

    def test_scope_condition_does_not_match(self):
        rule = PolicyRule(
            id="scope-check",
            name="Scope Check",
            description="Deny if scope includes 'admin'",
            condition=PolicyCondition(scopes=["admin"]),
            effect=PolicyEffect.DENY,
            priority=100,
            enabled=True,
        )
        passport = _make_passport(scope=["read", "write"])
        decision = evaluate_policy(passport, "access", rules=[rule])
        assert decision.allowed is True

    def test_tags_condition_matches(self):
        rule = PolicyRule(
            id="tag-check",
            name="Tag Check",
            description="Deny if tagged 'production'",
            condition=PolicyCondition(tags=["production"]),
            effect=PolicyEffect.DENY,
            priority=100,
            enabled=True,
        )
        passport = _make_passport(tags=["production", "critical"])
        decision = evaluate_policy(passport, "access", rules=[rule])
        assert decision.effect == PolicyEffect.DENY

    def test_credential_type_condition_matches(self):
        rule = PolicyRule(
            id="cred-type-check",
            name="Credential Type Check",
            description="Deny private keys",
            condition=PolicyCondition(
                credential_types=[CredentialType.PRIVATE_KEY]
            ),
            effect=PolicyEffect.DENY,
            priority=100,
            enabled=True,
        )
        passport = _make_passport(credential_type=CredentialType.PRIVATE_KEY)
        decision = evaluate_policy(passport, "access", rules=[rule])
        assert decision.effect == PolicyEffect.DENY

    def test_platform_condition_matches(self):
        rule = PolicyRule(
            id="platform-check",
            name="Platform Check",
            description="Deny AWS credentials",
            condition=PolicyCondition(platforms=[Platform.AWS]),
            effect=PolicyEffect.DENY,
            priority=100,
            enabled=True,
        )
        passport = _make_passport(platforms=[Platform.AWS])
        decision = evaluate_policy(passport, "access", rules=[rule])
        assert decision.effect == PolicyEffect.DENY


class TestValidatePassport:
    """Tests for validate_passport()."""

    def test_valid_passport_returns_empty_violations(self):
        passport = _make_passport()
        violations = validate_passport(passport)
        assert isinstance(violations, list)
        assert len(violations) == 0

    def test_missing_human_owner_violation(self):
        passport = _make_passport(human_owner="")
        violations = validate_passport(passport)
        assert len(violations) > 0
        assert any("human owner" in v.violation.lower() for v in violations)

    def test_excessive_delegation_violation(self):
        hops = [
            _make_delegation_hop(f"e-{i}", f"e-{i + 1}")
            for i in range(4)
        ]
        passport = _make_passport(delegation_chain=hops)
        violations = validate_passport(passport)
        assert any("delegation depth" in v.violation.lower() for v in violations)

    def test_excessive_validity_period_violation(self):
        passport = _make_passport(
            valid_from=datetime(2025, 1, 1),
            valid_until=datetime(2025, 7, 1),
        )
        violations = validate_passport(passport)
        assert any("validity period" in v.violation.lower() for v in violations)

    def test_privilege_visa_violation(self):
        passport = _make_passport(visa_type=VisaType.PRIVILEGE)
        violations = validate_passport(passport)
        assert any("approval" in v.violation.lower() for v in violations)

    def test_violations_contain_rule_reference(self):
        passport = _make_passport(human_owner="")
        violations = validate_passport(passport)
        for v in violations:
            assert isinstance(v, ValidationResult)
            assert isinstance(v.rule, PolicyRule)
            assert v.severity in ("error", "warning")

    def test_deny_violation_has_error_severity(self):
        passport = _make_passport(human_owner="")
        violations = validate_passport(passport)
        owner_violation = next(
            v for v in violations if "human owner" in v.violation.lower()
        )
        assert owner_violation.severity == "error"

    def test_require_approval_violation_has_warning_severity(self):
        passport = _make_passport(visa_type=VisaType.PRIVILEGE)
        violations = validate_passport(passport)
        approval_violation = next(
            v for v in violations if "approval" in v.violation.lower()
        )
        assert approval_violation.severity == "warning"

    def test_custom_rules(self):
        custom = [
            PolicyRule(
                id="custom-max-delegation-1",
                name="Strict Delegation",
                description="Max 1 delegation",
                condition=PolicyCondition(max_delegation_depth=1),
                effect=PolicyEffect.DENY,
                priority=100,
                enabled=True,
            )
        ]
        hops = [
            _make_delegation_hop("a", "b"),
            _make_delegation_hop("b", "c"),
        ]
        passport = _make_passport(delegation_chain=hops)
        violations = validate_passport(passport, rules=custom)
        assert len(violations) > 0
        assert any("delegation depth" in v.violation.lower() for v in violations)

    def test_disabled_rules_skipped(self):
        custom = [
            PolicyRule(
                id="disabled",
                name="Disabled Rule",
                description="Should not fire",
                condition=PolicyCondition(require_human_owner=True),
                effect=PolicyEffect.DENY,
                priority=100,
                enabled=False,
            )
        ]
        passport = _make_passport(human_owner="")
        violations = validate_passport(passport, rules=custom)
        assert len(violations) == 0


class TestCreatePolicyRule:
    """Tests for create_policy_rule()."""

    def test_creates_rule_with_all_fields(self):
        rule = create_policy_rule(
            id="my-rule",
            name="My Rule",
            description="A test rule",
            condition=PolicyCondition(require_human_owner=True),
            effect=PolicyEffect.DENY,
            priority=75,
            enabled=True,
        )
        assert isinstance(rule, PolicyRule)
        assert rule.id == "my-rule"
        assert rule.name == "My Rule"
        assert rule.description == "A test rule"
        assert rule.effect == PolicyEffect.DENY
        assert rule.priority == 75
        assert rule.enabled is True

    def test_auto_generates_id_when_not_provided(self):
        rule = create_policy_rule(
            name="Auto ID",
            description="No explicit ID",
            condition=PolicyCondition(),
            effect=PolicyEffect.ALLOW,
        )
        assert rule.id.startswith("rule-")
        assert len(rule.id) > 5

    def test_default_priority_is_50(self):
        rule = create_policy_rule(
            name="Default Priority",
            description="Uses default",
            condition=PolicyCondition(),
            effect=PolicyEffect.ALLOW,
        )
        assert rule.priority == 50

    def test_default_enabled_is_true(self):
        rule = create_policy_rule(
            name="Default Enabled",
            description="Uses default",
            condition=PolicyCondition(),
            effect=PolicyEffect.ALLOW,
        )
        assert rule.enabled is True


class TestMergePolicyRules:
    """Tests for merge_policy_rules()."""

    def test_merge_deduplicates_by_id(self):
        rule1 = PolicyRule(
            id="shared-id",
            name="First",
            description="First version",
            condition=PolicyCondition(),
            effect=PolicyEffect.ALLOW,
        )
        rule2 = PolicyRule(
            id="shared-id",
            name="Second",
            description="Second version (override)",
            condition=PolicyCondition(),
            effect=PolicyEffect.DENY,
        )
        merged = merge_policy_rules([rule1], [rule2])
        assert len(merged) == 1
        assert merged[0].name == "Second"
        assert merged[0].effect == PolicyEffect.DENY

    def test_merge_preserves_unique_rules(self):
        rule_a = PolicyRule(
            id="a", name="A", description="Rule A",
            condition=PolicyCondition(), effect=PolicyEffect.ALLOW,
        )
        rule_b = PolicyRule(
            id="b", name="B", description="Rule B",
            condition=PolicyCondition(), effect=PolicyEffect.DENY,
        )
        merged = merge_policy_rules([rule_a], [rule_b])
        assert len(merged) == 2
        ids = {r.id for r in merged}
        assert ids == {"a", "b"}

    def test_merge_empty_sets(self):
        merged = merge_policy_rules([], [])
        assert merged == []

    def test_merge_single_set(self):
        rule = PolicyRule(
            id="solo", name="Solo", description="Only rule",
            condition=PolicyCondition(), effect=PolicyEffect.ALLOW,
        )
        merged = merge_policy_rules([rule])
        assert len(merged) == 1

    def test_later_set_overrides_earlier(self):
        early = PolicyRule(
            id="x", name="Early", description="Early",
            condition=PolicyCondition(), effect=PolicyEffect.ALLOW,
            priority=10,
        )
        late = PolicyRule(
            id="x", name="Late", description="Late",
            condition=PolicyCondition(), effect=PolicyEffect.DENY,
            priority=90,
        )
        merged = merge_policy_rules([early], [late])
        assert merged[0].priority == 90


class TestGetEnabledRules:
    """Tests for get_enabled_rules()."""

    def test_filters_disabled_rules(self):
        enabled_rule = PolicyRule(
            id="enabled", name="Enabled", description="enabled",
            condition=PolicyCondition(), effect=PolicyEffect.ALLOW, enabled=True,
        )
        disabled_rule = PolicyRule(
            id="disabled", name="Disabled", description="disabled",
            condition=PolicyCondition(), effect=PolicyEffect.DENY, enabled=False,
        )
        result = get_enabled_rules([enabled_rule, disabled_rule])
        assert len(result) == 1
        assert result[0].id == "enabled"

    def test_returns_empty_for_all_disabled(self):
        rules = [
            PolicyRule(
                id=f"d{i}", name=f"D{i}", description="disabled",
                condition=PolicyCondition(), effect=PolicyEffect.ALLOW, enabled=False,
            )
            for i in range(3)
        ]
        assert get_enabled_rules(rules) == []

    def test_returns_all_when_all_enabled(self):
        rules = [
            PolicyRule(
                id=f"e{i}", name=f"E{i}", description="enabled",
                condition=PolicyCondition(), effect=PolicyEffect.ALLOW, enabled=True,
            )
            for i in range(3)
        ]
        assert len(get_enabled_rules(rules)) == 3


class TestPolicyPredicates:
    """Tests for the individual predicate functions."""

    # -- has_valid_human_owner --

    def test_has_valid_human_owner_true(self):
        passport = _make_passport(human_owner="alice@example.com")
        assert has_valid_human_owner(passport) is True

    def test_has_valid_human_owner_false_empty(self):
        passport = _make_passport(human_owner="")
        assert has_valid_human_owner(passport) is False

    def test_has_valid_human_owner_false_whitespace(self):
        passport = _make_passport(human_owner="   ")
        assert has_valid_human_owner(passport) is False

    # -- is_within_validity_period --

    def test_is_within_validity_period_true(self):
        now = datetime.utcnow()
        passport = _make_passport(
            valid_from=now - timedelta(days=1),
            valid_until=now + timedelta(days=30),
        )
        assert is_within_validity_period(passport) is True

    def test_is_within_validity_period_false_expired(self):
        passport = _make_passport(
            valid_from=datetime(2020, 1, 1),
            valid_until=datetime(2020, 2, 1),
        )
        assert is_within_validity_period(passport) is False

    def test_is_within_validity_period_false_not_yet_valid(self):
        future = datetime.utcnow() + timedelta(days=30)
        passport = _make_passport(
            valid_from=future,
            valid_until=future + timedelta(days=60),
        )
        assert is_within_validity_period(passport) is False

    def test_is_within_validity_period_no_expiry(self):
        now = datetime.utcnow()
        passport = _make_passport(
            valid_from=now - timedelta(days=1),
            valid_until=None,
        )
        assert is_within_validity_period(passport) is True

    # -- has_excessive_delegation --

    def test_has_excessive_delegation_false(self):
        passport = _make_passport(delegation_chain=[])
        assert has_excessive_delegation(passport) is False

    def test_has_excessive_delegation_true(self):
        hops = [_make_delegation_hop(f"a{i}", f"a{i + 1}") for i in range(4)]
        passport = _make_passport(delegation_chain=hops)
        assert has_excessive_delegation(passport) is True

    def test_has_excessive_delegation_custom_max(self):
        hops = [_make_delegation_hop("a", "b"), _make_delegation_hop("b", "c")]
        passport = _make_passport(delegation_chain=hops)
        assert has_excessive_delegation(passport, max_depth=1) is True
        assert has_excessive_delegation(passport, max_depth=2) is False
        assert has_excessive_delegation(passport, max_depth=3) is False

    # -- has_privileged_access --

    def test_has_privileged_access_true(self):
        passport = _make_passport(visa_type=VisaType.PRIVILEGE)
        assert has_privileged_access(passport) is True

    def test_has_privileged_access_false(self):
        passport = _make_passport(visa_type=VisaType.ACCESS)
        assert has_privileged_access(passport) is False

    def test_has_privileged_access_false_data(self):
        passport = _make_passport(visa_type=VisaType.DATA)
        assert has_privileged_access(passport) is False


class TestPolicyBuilder:
    """Tests for the PolicyBuilder fluent API."""

    def test_basic_build(self):
        rule = (
            PolicyBuilder("test-rule")
            .name("Test Rule")
            .description("A test rule")
            .deny()
            .build()
        )
        assert isinstance(rule, PolicyRule)
        assert rule.id == "test-rule"
        assert rule.name == "Test Rule"
        assert rule.effect == PolicyEffect.DENY

    def test_fluent_chaining(self):
        rule = (
            policy("fluent-test")
            .name("Fluent Rule")
            .description("Tests fluent chaining")
            .for_visa_types(VisaType.PRIVILEGE)
            .for_platforms(Platform.OPENAI)
            .for_credential_types(CredentialType.API_KEY)
            .require_human_owner()
            .max_delegation_depth(2)
            .max_validity_days(30)
            .deny()
            .priority(95)
            .build()
        )
        assert rule.id == "fluent-test"
        assert rule.priority == 95
        assert rule.effect == PolicyEffect.DENY
        assert rule.condition.visa_types == [VisaType.PRIVILEGE]
        assert rule.condition.platforms == [Platform.OPENAI]
        assert rule.condition.credential_types == [CredentialType.API_KEY]
        assert rule.condition.require_human_owner is True
        assert rule.condition.max_delegation_depth == 2
        assert rule.condition.max_validity_days == 30

    def test_require_approval_effect(self):
        rule = (
            policy("approval")
            .name("Approval Rule")
            .description("Needs approval")
            .require_approval()
            .build()
        )
        assert rule.effect == PolicyEffect.REQUIRE_APPROVAL

    def test_allow_effect(self):
        rule = (
            policy("allow")
            .name("Allow Rule")
            .description("Explicitly allow")
            .allow()
            .build()
        )
        assert rule.effect == PolicyEffect.ALLOW

    def test_disabled_builder(self):
        rule = (
            policy("disabled-builder")
            .name("Disabled")
            .description("Disabled rule")
            .disabled()
            .build()
        )
        assert rule.enabled is False

    def test_build_without_name_raises(self):
        builder = PolicyBuilder("no-name").description("Missing name")
        with pytest.raises(ValueError, match="requires id, name, and description"):
            builder.build()

    def test_build_without_description_raises(self):
        builder = PolicyBuilder("no-desc").name("No Description")
        with pytest.raises(ValueError, match="requires id, name, and description"):
            builder.build()

    def test_custom_condition(self):
        custom_fn = lambda p: len(p.scope) > 5
        rule = (
            policy("custom-cond")
            .name("Custom Condition")
            .description("Tests custom condition function")
            .with_custom_condition(custom_fn)
            .deny()
            .build()
        )
        assert rule.condition.custom is custom_fn

    def test_policy_helper_function(self):
        """policy() should return a PolicyBuilder."""
        builder = policy("helper-test")
        assert isinstance(builder, PolicyBuilder)


# ############################################################################
#
#  2. DELEGATION MODULE TESTS
#
# ############################################################################

from id_wispera.delegation import (
    get_delegation_depth,
    get_current_holder,
    get_original_issuer,
    get_all_delegation_parties,
    is_in_delegation_chain,
    get_delegation_path,
    has_expired_delegation,
    get_effective_scope,
    is_scope_permitted,
    validate_delegation_chain,
)


class TestGetDelegationDepth:
    """Tests for get_delegation_depth()."""

    def test_zero_depth(self):
        passport = _make_passport(delegation_chain=[])
        assert get_delegation_depth(passport) == 0

    def test_single_hop(self):
        hops = [_make_delegation_hop("alice", "bot-1")]
        passport = _make_passport(delegation_chain=hops)
        assert get_delegation_depth(passport) == 1

    def test_multiple_hops(self):
        hops = [
            _make_delegation_hop("alice", "bot-1"),
            _make_delegation_hop("bot-1", "bot-2"),
            _make_delegation_hop("bot-2", "bot-3"),
        ]
        passport = _make_passport(delegation_chain=hops)
        assert get_delegation_depth(passport) == 3


class TestGetCurrentHolder:
    """Tests for get_current_holder()."""

    def test_no_delegation_returns_human_owner(self):
        passport = _make_passport(
            human_owner="alice@example.com",
            delegation_chain=[],
        )
        assert get_current_holder(passport) == "alice@example.com"

    def test_with_delegation_returns_last_to(self):
        hops = [
            _make_delegation_hop("alice", "bot-1"),
            _make_delegation_hop("bot-1", "bot-2"),
        ]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        assert get_current_holder(passport) == "bot-2"

    def test_single_delegation(self):
        hops = [_make_delegation_hop("alice", "agent-x")]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        assert get_current_holder(passport) == "agent-x"


class TestGetOriginalIssuer:
    """Tests for get_original_issuer()."""

    def test_returns_human_owner(self):
        passport = _make_passport(human_owner="original-owner")
        assert get_original_issuer(passport) == "original-owner"

    def test_ignores_delegation_chain(self):
        hops = [_make_delegation_hop("alice", "bot-1")]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        assert get_original_issuer(passport) == "alice"


class TestGetAllDelegationParties:
    """Tests for get_all_delegation_parties()."""

    def test_no_delegation_returns_just_owner(self):
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=[],
        )
        parties = get_all_delegation_parties(passport)
        assert parties == ["alice"]

    def test_with_delegations(self):
        hops = [
            _make_delegation_hop("alice", "bot-1"),
            _make_delegation_hop("bot-1", "bot-2"),
        ]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        parties = get_all_delegation_parties(passport)
        assert "alice" in parties
        assert "bot-1" in parties
        assert "bot-2" in parties

    def test_no_duplicates(self):
        hops = [
            _make_delegation_hop("alice", "bot-1"),
            _make_delegation_hop("bot-1", "alice"),  # circular but tests dedup
        ]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        parties = get_all_delegation_parties(passport)
        assert parties.count("alice") == 1


class TestIsInDelegationChain:
    """Tests for is_in_delegation_chain()."""

    def test_owner_is_in_chain(self):
        passport = _make_passport(human_owner="alice")
        assert is_in_delegation_chain(passport, "alice") is True

    def test_delegate_to_is_in_chain(self):
        hops = [_make_delegation_hop("alice", "bot-1")]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        assert is_in_delegation_chain(passport, "bot-1") is True

    def test_delegate_from_is_in_chain(self):
        hops = [_make_delegation_hop("alice", "bot-1")]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        assert is_in_delegation_chain(passport, "alice") is True

    def test_unknown_entity_not_in_chain(self):
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=[],
        )
        assert is_in_delegation_chain(passport, "bob") is False


class TestGetDelegationPath:
    """Tests for get_delegation_path()."""

    def test_no_delegation_path(self):
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=[],
        )
        assert get_delegation_path(passport) == "alice"

    def test_single_hop_path(self):
        hops = [_make_delegation_hop("alice", "bot-1")]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        assert get_delegation_path(passport) == "alice -> bot-1"

    def test_multi_hop_path(self):
        hops = [
            _make_delegation_hop("alice", "bot-1"),
            _make_delegation_hop("bot-1", "bot-2"),
            _make_delegation_hop("bot-2", "bot-3"),
        ]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        assert get_delegation_path(passport) == "alice -> bot-1 -> bot-2 -> bot-3"


class TestHasExpiredDelegation:
    """Tests for has_expired_delegation()."""

    def test_no_delegations(self):
        passport = _make_passport(delegation_chain=[])
        assert has_expired_delegation(passport) is False

    def test_no_expired_delegations(self):
        future = datetime.utcnow() + timedelta(days=30)
        hops = [_make_delegation_hop("alice", "bot-1", expires_at=future)]
        passport = _make_passport(delegation_chain=hops)
        assert has_expired_delegation(passport) is False

    def test_has_expired_delegation(self):
        past = datetime.utcnow() - timedelta(days=1)
        hops = [_make_delegation_hop("alice", "bot-1", expires_at=past)]
        passport = _make_passport(delegation_chain=hops)
        assert has_expired_delegation(passport) is True

    def test_no_expiry_means_not_expired(self):
        hops = [_make_delegation_hop("alice", "bot-1", expires_at=None)]
        passport = _make_passport(delegation_chain=hops)
        assert has_expired_delegation(passport) is False

    def test_mixed_expired_and_valid(self):
        past = datetime.utcnow() - timedelta(days=1)
        future = datetime.utcnow() + timedelta(days=30)
        hops = [
            _make_delegation_hop("alice", "bot-1", expires_at=future),
            _make_delegation_hop("bot-1", "bot-2", expires_at=past),
        ]
        passport = _make_passport(delegation_chain=hops)
        assert has_expired_delegation(passport) is True


class TestGetEffectiveScope:
    """Tests for get_effective_scope()."""

    def test_no_delegation_returns_passport_scope(self):
        passport = _make_passport(
            scope=["read", "write", "admin"],
            delegation_chain=[],
        )
        result = get_effective_scope(passport)
        assert set(result) == {"read", "write", "admin"}

    def test_scope_narrowing(self):
        """Each delegation hop can narrow scope via intersection."""
        hops = [
            _make_delegation_hop("alice", "bot-1", scope=["read", "write"]),
        ]
        passport = _make_passport(
            scope=["read", "write", "admin"],
            delegation_chain=hops,
        )
        result = get_effective_scope(passport)
        assert set(result) == {"read", "write"}

    def test_progressive_narrowing(self):
        hops = [
            _make_delegation_hop("alice", "bot-1", scope=["read", "write"]),
            _make_delegation_hop("bot-1", "bot-2", scope=["read"]),
        ]
        passport = _make_passport(
            scope=["read", "write", "admin"],
            delegation_chain=hops,
        )
        result = get_effective_scope(passport)
        assert set(result) == {"read"}

    def test_delegation_without_scope_does_not_narrow(self):
        hops = [
            _make_delegation_hop("alice", "bot-1", scope=None),
        ]
        passport = _make_passport(
            scope=["read", "write"],
            delegation_chain=hops,
        )
        result = get_effective_scope(passport)
        assert set(result) == {"read", "write"}

    def test_empty_delegation_scope_does_not_narrow(self):
        hops = [
            _make_delegation_hop("alice", "bot-1", scope=[]),
        ]
        passport = _make_passport(
            scope=["read", "write"],
            delegation_chain=hops,
        )
        result = get_effective_scope(passport)
        assert set(result) == {"read", "write"}

    def test_disjoint_scope_narrows_to_empty(self):
        hops = [
            _make_delegation_hop("alice", "bot-1", scope=["delete"]),
        ]
        passport = _make_passport(
            scope=["read", "write"],
            delegation_chain=hops,
        )
        result = get_effective_scope(passport)
        assert result == []


class TestIsScopePermitted:
    """Tests for is_scope_permitted()."""

    def test_permitted(self):
        passport = _make_passport(scope=["read", "write"], delegation_chain=[])
        assert is_scope_permitted(passport, "read") is True

    def test_not_permitted(self):
        passport = _make_passport(scope=["read", "write"], delegation_chain=[])
        assert is_scope_permitted(passport, "admin") is False

    def test_narrowed_by_delegation(self):
        hops = [
            _make_delegation_hop("alice", "bot-1", scope=["read"]),
        ]
        passport = _make_passport(
            scope=["read", "write"],
            delegation_chain=hops,
        )
        assert is_scope_permitted(passport, "read") is True
        assert is_scope_permitted(passport, "write") is False


class TestValidateDelegationChain:
    """Tests for validate_delegation_chain()."""

    def test_empty_chain_is_valid(self):
        passport = _make_passport(delegation_chain=[])
        result = validate_delegation_chain(passport)
        assert result["valid"] is True
        assert result["issues"] == []

    def test_valid_chain(self):
        now = datetime.utcnow()
        hops = [
            _make_delegation_hop(
                "alice", "bot-1",
                granted_at=now,
                expires_at=now + timedelta(days=30),
            ),
            _make_delegation_hop(
                "bot-1", "bot-2",
                granted_at=now + timedelta(seconds=1),
                expires_at=now + timedelta(days=30),
            ),
        ]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        result = validate_delegation_chain(passport)
        assert result["valid"] is True
        assert len(result["issues"]) == 0

    def test_broken_chain(self):
        now = datetime.utcnow()
        hops = [
            _make_delegation_hop("alice", "bot-1", granted_at=now),
            _make_delegation_hop(
                "WRONG-ENTITY", "bot-2",
                granted_at=now + timedelta(seconds=1),
            ),
        ]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        result = validate_delegation_chain(passport)
        assert result["valid"] is False
        assert any("broken chain" in issue.lower() for issue in result["issues"])

    def test_circular_delegation(self):
        now = datetime.utcnow()
        hops = [
            _make_delegation_hop("alice", "bot-1", granted_at=now),
            _make_delegation_hop(
                "bot-1", "alice",
                granted_at=now + timedelta(seconds=1),
            ),
        ]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        result = validate_delegation_chain(passport)
        assert result["valid"] is False
        assert any("circular" in issue.lower() for issue in result["issues"])

    def test_expired_delegation_in_chain(self):
        past = datetime.utcnow() - timedelta(days=1)
        hops = [
            _make_delegation_hop(
                "alice", "bot-1",
                granted_at=past - timedelta(days=10),
                expires_at=past,
            ),
        ]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        result = validate_delegation_chain(passport)
        assert result["valid"] is False
        assert any("expired" in issue.lower() for issue in result["issues"])

    def test_out_of_order_timestamps(self):
        now = datetime.utcnow()
        hops = [
            _make_delegation_hop(
                "alice", "bot-1",
                granted_at=now + timedelta(hours=1),
                expires_at=now + timedelta(days=30),
            ),
            _make_delegation_hop(
                "bot-1", "bot-2",
                granted_at=now,  # earlier than first hop
                expires_at=now + timedelta(days=30),
            ),
        ]
        passport = _make_passport(
            human_owner="alice",
            delegation_chain=hops,
        )
        result = validate_delegation_chain(passport)
        assert result["valid"] is False
        assert any("timestamp" in issue.lower() or "order" in issue.lower()
                    for issue in result["issues"])


# ############################################################################
#
#  3. SHARING MODULE TESTS
#
# ############################################################################

from id_wispera.sharing import (
    create_share_link,
    resolve_share_link,
    get_share_info,
    is_share_valid,
    create_share_url,
    parse_share_url,
    create_one_time_share,
    create_metadata_share,
)


class TestCreateAndResolveShareLink:
    """Tests for create_share_link() and resolve_share_link()."""

    def test_create_share_link_returns_tuple(self):
        passport = _make_passport()
        options = ShareOptions(scope=ShareScope.FULL)
        payload, key = create_share_link(passport, options)
        assert isinstance(payload, str)
        assert isinstance(key, str)
        assert len(payload) > 0
        assert len(key) > 0

    def test_resolve_decrypts_correctly(self):
        passport = _make_passport(
            name="Roundtrip Test Key",
            credential_value="sk-roundtrip-test",
        )
        options = ShareOptions(scope=ShareScope.FULL)
        payload, key = create_share_link(passport, options)
        resolved = resolve_share_link(payload, key)
        assert isinstance(resolved, Passport)
        assert resolved.name == "Roundtrip Test Key"
        assert resolved.credential_value == "sk-roundtrip-test"

    def test_roundtrip_preserves_all_fields(self):
        now = datetime.utcnow()
        passport = _make_passport(
            name="Full Passport",
            credential_value="secret-123",
            visa_type=VisaType.DATA,
            scope=["read", "write"],
            tags=["tag1", "tag2"],
            human_owner="owner@example.com",
            valid_from=now - timedelta(days=1),
            valid_until=now + timedelta(days=30),
        )
        options = ShareOptions(scope=ShareScope.FULL)
        payload, key = create_share_link(passport, options)
        resolved = resolve_share_link(payload, key)

        assert resolved.name == passport.name
        assert resolved.credential_value == passport.credential_value
        assert resolved.human_owner == passport.human_owner
        assert set(resolved.scope) == set(passport.scope)
        assert set(resolved.tags) == set(passport.tags)

    def test_wrong_key_fails(self):
        passport = _make_passport()
        options = ShareOptions(scope=ShareScope.FULL)
        payload, _ = create_share_link(passport, options)

        # Generate a different key
        from id_wispera.sharing import _generate_share_key
        wrong_key = _generate_share_key()

        with pytest.raises(ValueError):
            resolve_share_link(payload, wrong_key)

    def test_invalid_payload_format_fails(self):
        with pytest.raises(ValueError, match="Invalid share payload"):
            resolve_share_link("not-valid-base64!!!", "also-invalid")


class TestGetShareInfo:
    """Tests for get_share_info()."""

    def test_returns_metadata_without_decrypting(self):
        passport = _make_passport()
        options = ShareOptions(scope=ShareScope.FULL, max_views=10)
        payload, _ = create_share_link(passport, options)

        info = get_share_info(payload)
        assert "version" in info
        assert "options" in info
        assert "createdAt" in info
        assert "passportId" in info
        assert "viewCount" in info
        assert "hasCredential" in info

    def test_full_scope_has_credential(self):
        passport = _make_passport()
        options = ShareOptions(scope=ShareScope.FULL)
        payload, _ = create_share_link(passport, options)

        info = get_share_info(payload)
        assert info["hasCredential"] is True

    def test_metadata_only_scope_no_credential(self):
        passport = _make_passport()
        options = ShareOptions(scope=ShareScope.METADATA_ONLY)
        payload, _ = create_share_link(passport, options)

        info = get_share_info(payload)
        assert info["hasCredential"] is False

    def test_passport_id_present(self):
        passport = _make_passport()
        options = ShareOptions(scope=ShareScope.FULL)
        payload, _ = create_share_link(passport, options)

        info = get_share_info(payload)
        assert info["passportId"] == passport.id

    def test_invalid_payload_raises(self):
        with pytest.raises(ValueError, match="Invalid share payload"):
            get_share_info("garbage-data")


class TestIsShareValid:
    """Tests for is_share_valid()."""

    def test_valid_share(self):
        passport = _make_passport()
        options = ShareOptions(scope=ShareScope.FULL)
        payload, _ = create_share_link(passport, options)
        valid, reason = is_share_valid(payload)
        assert valid is True
        assert reason is None

    def test_invalid_payload_format(self):
        valid, reason = is_share_valid("invalid-payload")
        assert valid is False
        assert reason is not None

    def test_max_views_zero_is_invalid(self):
        """A share with max_views=0 and view_count=0 should be reported as maxed."""
        passport = _make_passport()
        options = ShareOptions(scope=ShareScope.FULL, max_views=0)
        payload, _ = create_share_link(passport, options)
        valid, reason = is_share_valid(payload)
        assert valid is False
        assert "maximum views" in reason.lower()


class TestShareUrlHelpers:
    """Tests for create_share_url() and parse_share_url()."""

    def test_create_share_url(self):
        url = create_share_url("https://example.com", "payload123", "key456")
        assert url == "https://example.com/share/payload123#key456"

    def test_parse_share_url(self):
        url = "https://example.com/share/payload123#key456"
        result = parse_share_url(url)
        assert result is not None
        payload, key = result
        assert payload == "payload123"
        assert key == "key456"

    def test_roundtrip_url(self):
        base = "https://wispera.dev"
        original_payload = "my-encrypted-payload"
        original_key = "my-secret-key"
        url = create_share_url(base, original_payload, original_key)
        parsed = parse_share_url(url)
        assert parsed is not None
        assert parsed[0] == original_payload
        assert parsed[1] == original_key

    def test_parse_invalid_url_returns_none(self):
        assert parse_share_url("https://example.com/not-a-share") is None

    def test_parse_url_without_key_returns_none(self):
        assert parse_share_url("https://example.com/share/payload") is None

    def test_parse_empty_key_returns_none(self):
        assert parse_share_url("https://example.com/share/payload#") is None


class TestCreateOneTimeShare:
    """Tests for create_one_time_share()."""

    def test_returns_payload_and_key(self):
        passport = _make_passport()
        payload, key = create_one_time_share(passport)
        assert isinstance(payload, str)
        assert isinstance(key, str)

    def test_one_time_share_has_max_views_one(self):
        passport = _make_passport()
        payload, _ = create_one_time_share(passport)
        info = get_share_info(payload)
        assert info["options"]["maxViews"] == 1

    def test_one_time_share_has_expiration(self):
        passport = _make_passport()
        payload, _ = create_one_time_share(passport, expires_in_minutes=30)
        info = get_share_info(payload)
        assert info["options"]["expiresAt"] is not None

    def test_one_time_share_is_resolvable(self):
        passport = _make_passport(
            name="One-Time Passport",
            credential_value="one-time-secret",
        )
        payload, key = create_one_time_share(passport)
        resolved = resolve_share_link(payload, key)
        assert resolved.name == "One-Time Passport"
        assert resolved.credential_value == "one-time-secret"


class TestCreateMetadataShare:
    """Tests for create_metadata_share()."""

    def test_metadata_share_strips_credential(self):
        passport = _make_passport(
            credential_value="SUPER-SECRET-VALUE",
        )
        payload, key = create_metadata_share(passport)

        info = get_share_info(payload)
        assert info["hasCredential"] is False

    def test_metadata_share_cannot_resolve_as_full_passport(self):
        """A metadata-only share strips the credentialValue. Attempting to
        resolve it as a full Passport raises a ValueError because Pydantic
        requires credentialValue."""
        passport = _make_passport(
            name="Metadata Passport",
            credential_value="secret-value",
        )
        payload, key = create_metadata_share(passport)
        # The decrypted data is missing credentialValue, so Passport
        # construction will fail inside resolve_share_link.
        with pytest.raises(ValueError):
            resolve_share_link(payload, key)

    def test_metadata_share_info_shows_no_credential(self):
        """get_share_info on a metadata share should report hasCredential=False."""
        passport = _make_passport(credential_value="should-be-hidden")
        payload, _ = create_metadata_share(passport)
        info = get_share_info(payload)
        assert info["hasCredential"] is False

    def test_metadata_share_with_options(self):
        passport = _make_passport()
        payload, _ = create_metadata_share(
            passport,
            max_views=5,
        )
        info = get_share_info(payload)
        assert info["options"]["maxViews"] == 5


# ############################################################################
#
#  4. LOCATIONS MODULE TESTS
#
# ############################################################################

from id_wispera.locations import (
    get_provider_ids,
    get_provider,
    expand_path,
    sort_by_risk,
    get_risk_label,
    DiscoveredCredential,
    CredentialClassification,
)


class TestGetProviderIds:
    """Tests for get_provider_ids()."""

    def test_returns_list_of_strings(self):
        ids = get_provider_ids()
        assert isinstance(ids, list)
        for pid in ids:
            assert isinstance(pid, str)

    def test_known_providers_present(self):
        ids = get_provider_ids()
        assert "openclaw" in ids
        assert "aws" in ids
        assert "ssh" in ids
        assert "docker" in ids
        assert "npm" in ids
        assert "kubernetes" in ids

    def test_at_least_six_providers(self):
        assert len(get_provider_ids()) >= 6


class TestGetProvider:
    """Tests for get_provider()."""

    def test_returns_provider_definition(self):
        provider = get_provider("aws")
        assert provider is not None
        assert provider.id == "aws"
        assert provider.name == "AWS"

    def test_returns_none_for_unknown(self):
        assert get_provider("nonexistent-provider") is None

    def test_provider_has_locations(self):
        provider = get_provider("openclaw")
        assert provider is not None
        assert len(provider.locations) > 0

    def test_each_provider_has_id_and_name(self):
        for pid in get_provider_ids():
            provider = get_provider(pid)
            assert provider is not None
            assert provider.id == pid
            assert len(provider.name) > 0
            assert len(provider.description) > 0


class TestExpandPath:
    """Tests for expand_path()."""

    def test_replaces_home_variable(self):
        from id_wispera.locations import get_home_dir
        result = expand_path("{home}/.aws/credentials")
        expected = f"{get_home_dir()}/.aws/credentials"
        assert result == expected

    def test_replaces_custom_variables(self):
        result = expand_path("{home}/{project}/config", vars={"project": "myapp"})
        from id_wispera.locations import get_home_dir
        assert f"{get_home_dir()}/myapp/config" == result

    def test_no_vars_just_home(self):
        result = expand_path("{home}")
        from id_wispera.locations import get_home_dir
        assert result == get_home_dir()

    def test_no_variables_in_path(self):
        result = expand_path("/usr/local/bin")
        assert result == "/usr/local/bin"

    def test_multiple_custom_vars(self):
        result = expand_path(
            "{a}/{b}/{c}",
            vars={"a": "x", "b": "y", "c": "z"},
        )
        # {home} is also replaced, but since it's not in the pattern, it
        # doesn't matter. Check the custom vars:
        assert "x/y/z" in result


class TestSortByRisk:
    """Tests for sort_by_risk()."""

    def _make_cred(self, risk_level: RiskLevel) -> DiscoveredCredential:
        """Helper to create a DiscoveredCredential with a given risk level."""
        return DiscoveredCredential(
            provider="test",
            file_path="/test/path",
            classification=CredentialClassification(
                name="Test Cred",
                credential_type=CredentialType.API_KEY,
                visa_type=VisaType.ACCESS,
                risk_level=risk_level,
                platforms=["test"],
                tags=["test"],
                issuing_authority="Test Authority",
            ),
            raw_value="test-value",
            metadata={},
        )

    def test_sorts_critical_first(self):
        creds = [
            self._make_cred(RiskLevel.LOW),
            self._make_cred(RiskLevel.CRITICAL),
            self._make_cred(RiskLevel.MEDIUM),
            self._make_cred(RiskLevel.HIGH),
        ]
        sorted_creds = sort_by_risk(creds)
        risk_levels = [c.classification.risk_level for c in sorted_creds]

        # If use_enum_values is True, these come back as strings
        risk_strings = [r.value if hasattr(r, "value") else r for r in risk_levels]
        assert risk_strings == ["critical", "high", "medium", "low"]

    def test_empty_list(self):
        assert sort_by_risk([]) == []

    def test_single_item(self):
        cred = self._make_cred(RiskLevel.HIGH)
        assert sort_by_risk([cred]) == [cred]

    def test_same_risk_level_stable(self):
        creds = [
            self._make_cred(RiskLevel.MEDIUM),
            self._make_cred(RiskLevel.MEDIUM),
        ]
        sorted_creds = sort_by_risk(creds)
        assert len(sorted_creds) == 2


class TestGetRiskLabel:
    """Tests for get_risk_label()."""

    def test_critical(self):
        assert get_risk_label("critical") == "CRITICAL"

    def test_high(self):
        assert get_risk_label("high") == "HIGH"

    def test_medium(self):
        assert get_risk_label("medium") == "MEDIUM"

    def test_low(self):
        assert get_risk_label("low") == "LOW"

    def test_with_enum(self):
        assert get_risk_label(RiskLevel.CRITICAL) == "CRITICAL"
        assert get_risk_label(RiskLevel.HIGH) == "HIGH"
        assert get_risk_label(RiskLevel.MEDIUM) == "MEDIUM"
        assert get_risk_label(RiskLevel.LOW) == "LOW"


# ############################################################################
#
#  5. PROVISIONING MODULE TESTS
#
# ############################################################################

from id_wispera.provisioning import (
    list_providers,
    get_provider_capabilities,
    OpenAIProvider,
    AWSProvider,
)
from id_wispera.provisioning.types import (
    ProvisioningProvider,
    ProviderCapabilities,
)


class TestListProviders:
    """Tests for list_providers()."""

    def test_returns_list(self):
        result = list_providers()
        assert isinstance(result, list)

    def test_at_least_two_providers(self):
        result = list_providers()
        assert len(result) >= 2

    def test_each_entry_has_provider_and_capabilities(self):
        for entry in list_providers():
            assert "provider" in entry
            assert "capabilities" in entry
            assert isinstance(entry["provider"], str)
            assert isinstance(entry["capabilities"], dict)

    def test_openai_in_providers(self):
        names = [p["provider"] for p in list_providers()]
        assert "openai" in names

    def test_aws_in_providers(self):
        names = [p["provider"] for p in list_providers()]
        assert "aws" in names

    def test_capabilities_have_expected_keys(self):
        for entry in list_providers():
            caps = entry["capabilities"]
            assert "canCreate" in caps
            assert "canRotate" in caps
            assert "canRevoke" in caps
            assert "canList" in caps
            assert "supportsExpiry" in caps
            assert "supportsScopedKeys" in caps


class TestGetProviderCapabilities:
    """Tests for get_provider_capabilities()."""

    def test_openai_capabilities(self):
        caps = get_provider_capabilities("openai")
        assert caps is not None
        assert isinstance(caps, ProviderCapabilities)
        assert caps.can_create is True
        assert caps.can_list is True
        assert caps.supports_scoped_keys is True

    def test_aws_capabilities(self):
        caps = get_provider_capabilities("aws")
        assert caps is not None
        assert isinstance(caps, ProviderCapabilities)
        assert caps.can_create is True
        assert caps.can_rotate is True
        assert caps.supports_expiry is True

    def test_unknown_provider_returns_none(self):
        caps = get_provider_capabilities("nonexistent")
        assert caps is None

    def test_openai_does_not_support_expiry(self):
        caps = get_provider_capabilities("openai")
        assert caps is not None
        assert caps.supports_expiry is False


class TestProvisioningProviderEnum:
    """Tests for ProvisioningProvider enum."""

    def test_openai_value(self):
        assert ProvisioningProvider.OPENAI.value == "openai"

    def test_aws_value(self):
        assert ProvisioningProvider.AWS.value == "aws"

    def test_google_cloud_value(self):
        assert ProvisioningProvider.GOOGLE_CLOUD.value == "google-cloud"

    def test_azure_entra_value(self):
        assert ProvisioningProvider.AZURE_ENTRA.value == "azure-entra"

    def test_github_value(self):
        assert ProvisioningProvider.GITHUB.value == "github"

    def test_twilio_value(self):
        assert ProvisioningProvider.TWILIO.value == "twilio"

    def test_sendgrid_value(self):
        assert ProvisioningProvider.SENDGRID.value == "sendgrid"

    def test_anthropic_value(self):
        assert ProvisioningProvider.ANTHROPIC.value == "anthropic"

    def test_enum_is_str_enum(self):
        assert isinstance(ProvisioningProvider.OPENAI, str)


class TestOpenAIProviderInstantiation:
    """Tests for OpenAIProvider basic instantiation (no network calls)."""

    def test_can_instantiate(self):
        provider = OpenAIProvider()
        assert provider is not None

    def test_provider_name(self):
        provider = OpenAIProvider()
        assert provider.provider == "openai"

    def test_capabilities_type(self):
        provider = OpenAIProvider()
        assert isinstance(provider.capabilities, ProviderCapabilities)

    def test_can_create_is_true(self):
        provider = OpenAIProvider()
        assert provider.capabilities.can_create is True

    def test_can_configure_permissions(self):
        provider = OpenAIProvider()
        assert provider.capabilities.can_configure_permissions is True

    def test_not_initialized_by_default(self):
        provider = OpenAIProvider()
        assert provider._initialized is False

    def test_initialize_sets_auth(self):
        provider = OpenAIProvider()
        provider.initialize({"type": "api-key", "key": "sk-test"})
        assert provider._initialized is True

    def test_ensure_initialized_raises_before_init(self):
        provider = OpenAIProvider()
        with pytest.raises(RuntimeError, match="not initialized"):
            provider._ensure_initialized()


class TestAWSProviderInstantiation:
    """Tests for AWSProvider basic instantiation (no network calls)."""

    def test_can_instantiate(self):
        provider = AWSProvider()
        assert provider is not None

    def test_provider_name(self):
        provider = AWSProvider()
        assert provider.provider == "aws"

    def test_capabilities_type(self):
        provider = AWSProvider()
        assert isinstance(provider.capabilities, ProviderCapabilities)

    def test_supports_expiry(self):
        provider = AWSProvider()
        assert provider.capabilities.supports_expiry is True

    def test_supports_scoped_keys(self):
        provider = AWSProvider()
        assert provider.capabilities.supports_scoped_keys is True

    def test_can_revoke(self):
        provider = AWSProvider()
        assert provider.capabilities.can_revoke is True

    def test_initialize_sets_auth(self):
        provider = AWSProvider()
        provider.initialize({
            "type": "aws-sigv4",
            "accessKeyId": "AKIA...",
            "secretAccessKey": "test-secret",
            "region": "us-east-1",
        })
        assert provider._initialized is True

    def test_ensure_initialized_raises_before_init(self):
        provider = AWSProvider()
        with pytest.raises(RuntimeError, match="not initialized"):
            provider._ensure_initialized()


class TestProvisioningProviderBaseStaticMethods:
    """Tests for static helper methods on ProvisioningProviderBase."""

    def test_map_to_visa_type_openai(self):
        from id_wispera.provisioning.provider import ProvisioningProviderBase
        assert ProvisioningProviderBase.map_to_visa_type("openai", False) == "privilege"
        assert ProvisioningProviderBase.map_to_visa_type("openai", True) == "privilege"

    def test_map_to_visa_type_anthropic(self):
        from id_wispera.provisioning.provider import ProvisioningProviderBase
        assert ProvisioningProviderBase.map_to_visa_type("anthropic", False) == "privilege"

    def test_map_to_visa_type_aws(self):
        from id_wispera.provisioning.provider import ProvisioningProviderBase
        assert ProvisioningProviderBase.map_to_visa_type("aws", True) == "privilege"
        assert ProvisioningProviderBase.map_to_visa_type("aws", False) == "access"

    def test_map_to_visa_type_unknown(self):
        from id_wispera.provisioning.provider import ProvisioningProviderBase
        assert ProvisioningProviderBase.map_to_visa_type("unknown-provider", False) == "access"

    def test_map_to_platform_openai(self):
        from id_wispera.provisioning.provider import ProvisioningProviderBase
        assert ProvisioningProviderBase.map_to_platform("openai") == "openai"

    def test_map_to_platform_aws(self):
        from id_wispera.provisioning.provider import ProvisioningProviderBase
        assert ProvisioningProviderBase.map_to_platform("aws") == "aws"

    def test_map_to_platform_github(self):
        from id_wispera.provisioning.provider import ProvisioningProviderBase
        assert ProvisioningProviderBase.map_to_platform("github") == "github"

    def test_map_to_platform_unknown(self):
        from id_wispera.provisioning.provider import ProvisioningProviderBase
        assert ProvisioningProviderBase.map_to_platform("unknown") == "custom"
