"""
Unit Tests for RBAC/ABAC Policy System

Tests cover:
- Role Enum: All roles (Korrektur + Zeugnis)
- Action Enum: All actions
- ResourceType Enum: All resource types (Klausur + Zeugnis)
- VerfahrenType: is_exam_type(), is_certificate_type()
- PolicySet: Creation, serialization, type checks
- RoleAssignment: Creation, validity, revocation
- KeyShare: Creation, acceptance, revocation
- PolicyEngine:
  - register_policy_set()
  - get_policy_for_context()
  - assign_role()
  - revoke_role()
  - get_user_roles()
  - create_key_share()
  - accept_key_share()
  - revoke_key_share()
  - check_permission()
  - get_allowed_actions()
- DEFAULT_PERMISSIONS: Matrix completeness
- Zeugnis-Workflow: Role chain permissions
"""

import pytest
import uuid
from datetime import datetime, timezone, timedelta

import sys
sys.path.insert(0, '..')
from compliance.rbac import (
    Role,
    Action,
    ResourceType,
    ZKVisibilityMode,
    EHVisibilityMode,
    VerfahrenType,
    PolicySet,
    RoleAssignment,
    KeyShare,
    Tenant,
    Namespace,
    ExamPackage,
    PolicyEngine,
    DEFAULT_PERMISSIONS,
    create_default_policy_sets,
    get_policy_engine,
)


# =============================================
# FIXTURES
# =============================================

@pytest.fixture
def policy_engine():
    """Fresh PolicyEngine instance for each test."""
    return PolicyEngine()


@pytest.fixture
def sample_policy():
    """Sample PolicySet for testing."""
    return PolicySet(
        id="TEST-2025-ABITUR",
        bundesland="niedersachsen",
        jahr=2025,
        fach=None,
        verfahren="abitur",
        zk_visibility_mode=ZKVisibilityMode.FULL,
        eh_visibility_mode=EHVisibilityMode.SHARED,
        allow_teacher_uploaded_eh=True,
        allow_land_uploaded_eh=True,
        third_correction_threshold=4,
        final_signoff_role="fachvorsitz"
    )


@pytest.fixture
def sample_zeugnis_policy():
    """Sample PolicySet for Zeugnis testing."""
    return PolicySet(
        id="TEST-2025-ZEUGNIS",
        bundesland="niedersachsen",
        jahr=2025,
        fach=None,
        verfahren="halbjahreszeugnis",
        require_klassenlehrer_approval=True,
        require_schulleitung_signoff=True,
        konferenz_protokoll_required=True,
        kopfnoten_enabled=True,
        versetzung_auto_calculate=True
    )


@pytest.fixture
def sample_tenant():
    """Sample Tenant for testing."""
    return Tenant(
        id="school-001",
        name="Test Gymnasium",
        bundesland="niedersachsen",
        tenant_type="school",
        encryption_enabled=True
    )


@pytest.fixture
def sample_namespace(sample_tenant):
    """Sample Namespace for testing."""
    return Namespace(
        id="namespace-001",
        tenant_id=sample_tenant.id,
        name="Abitur 2025 - Deutsch LK",
        jahr=2025,
        fach="deutsch",
        kurs="12a",
        pruefungsart="abitur"
    )


@pytest.fixture
def sample_exam_package(sample_namespace, sample_tenant):
    """Sample ExamPackage for testing."""
    return ExamPackage(
        id="package-001",
        namespace_id=sample_namespace.id,
        tenant_id=sample_tenant.id,
        name="Deutsch LK Q1 Klausur",
        status="draft",
        owner_id="teacher-001"
    )


# =============================================
# ENUM TESTS
# =============================================

class TestRoleEnum:
    """Tests for Role Enum."""

    def test_all_korrektur_roles_exist(self):
        """All correction chain roles are defined."""
        assert Role.ERSTKORREKTOR.value == "erstkorrektor"
        assert Role.ZWEITKORREKTOR.value == "zweitkorrektor"
        assert Role.DRITTKORREKTOR.value == "drittkorrektor"

    def test_all_zeugnis_roles_exist(self):
        """All certificate workflow roles are defined."""
        assert Role.KLASSENLEHRER.value == "klassenlehrer"
        assert Role.FACHLEHRER.value == "fachlehrer"
        assert Role.ZEUGNISBEAUFTRAGTER.value == "zeugnisbeauftragter"
        assert Role.SEKRETARIAT.value == "sekretariat"

    def test_all_leadership_roles_exist(self):
        """All leadership roles are defined."""
        assert Role.FACHVORSITZ.value == "fachvorsitz"
        assert Role.PRUEFUNGSVORSITZ.value == "pruefungsvorsitz"
        assert Role.SCHULLEITUNG.value == "schulleitung"
        assert Role.STUFENLEITUNG.value == "stufenleitung"

    def test_all_admin_roles_exist(self):
        """All admin roles are defined."""
        assert Role.SCHUL_ADMIN.value == "schul_admin"
        assert Role.LAND_ADMIN.value == "land_admin"

    def test_all_special_roles_exist(self):
        """All special roles are defined."""
        assert Role.AUDITOR.value == "auditor"
        assert Role.OPERATOR.value == "operator"
        assert Role.TEACHER_ASSISTANT.value == "teacher_assistant"
        assert Role.EXAM_AUTHOR.value == "exam_author"

    def test_role_string_behavior(self):
        """Role enum behaves as string."""
        role = Role.ERSTKORREKTOR
        assert str(role) == "Role.ERSTKORREKTOR"
        assert role.value == "erstkorrektor"
        assert f"{role.value}" == "erstkorrektor"


class TestActionEnum:
    """Tests for Action Enum."""

    def test_crud_actions_exist(self):
        """Basic CRUD actions are defined."""
        assert Action.CREATE.value == "create"
        assert Action.READ.value == "read"
        assert Action.UPDATE.value == "update"
        assert Action.DELETE.value == "delete"

    def test_role_actions_exist(self):
        """Role management actions are defined."""
        assert Action.ASSIGN_ROLE.value == "assign_role"
        assert Action.INVITE_USER.value == "invite_user"
        assert Action.REMOVE_USER.value == "remove_user"

    def test_file_actions_exist(self):
        """File operations are defined."""
        assert Action.UPLOAD.value == "upload"
        assert Action.DOWNLOAD.value == "download"

    def test_workflow_actions_exist(self):
        """Workflow actions are defined."""
        assert Action.LOCK.value == "lock"
        assert Action.UNLOCK.value == "unlock"
        assert Action.SIGN_OFF.value == "sign_off"

    def test_special_actions_exist(self):
        """Special actions are defined."""
        assert Action.SHARE_KEY.value == "share_key"
        assert Action.VIEW_PII.value == "view_pii"
        assert Action.BREAK_GLASS.value == "break_glass"
        assert Action.PUBLISH_OFFICIAL.value == "publish_official"


class TestResourceTypeEnum:
    """Tests for ResourceType Enum."""

    def test_tenant_resources_exist(self):
        """Tenant-level resources are defined."""
        assert ResourceType.TENANT.value == "tenant"
        assert ResourceType.NAMESPACE.value == "namespace"

    def test_klausur_resources_exist(self):
        """Klausur-related resources are defined."""
        assert ResourceType.EXAM_PACKAGE.value == "exam_package"
        assert ResourceType.STUDENT_WORK.value == "student_work"
        assert ResourceType.EH_DOCUMENT.value == "eh_document"
        assert ResourceType.RUBRIC.value == "rubric"
        assert ResourceType.ANNOTATION.value == "annotation"
        assert ResourceType.EVALUATION.value == "evaluation"
        assert ResourceType.REPORT.value == "report"
        assert ResourceType.GRADE_DECISION.value == "grade_decision"

    def test_zeugnis_resources_exist(self):
        """Zeugnis-related resources are defined."""
        assert ResourceType.ZEUGNIS.value == "zeugnis"
        assert ResourceType.ZEUGNIS_VORLAGE.value == "zeugnis_vorlage"
        assert ResourceType.ZEUGNIS_ENTWURF.value == "zeugnis_entwurf"
        assert ResourceType.SCHUELER_DATEN.value == "schueler_daten"
        assert ResourceType.FACHNOTE.value == "fachnote"
        assert ResourceType.KOPFNOTE.value == "kopfnote"
        assert ResourceType.FEHLZEITEN.value == "fehlzeiten"
        assert ResourceType.BEMERKUNG.value == "bemerkung"
        assert ResourceType.KONFERENZ_BESCHLUSS.value == "konferenz_beschluss"
        assert ResourceType.VERSETZUNG.value == "versetzung"

    def test_general_resources_exist(self):
        """General resources are defined."""
        assert ResourceType.DOCUMENT.value == "document"
        assert ResourceType.TEMPLATE.value == "template"
        assert ResourceType.EXPORT.value == "export"
        assert ResourceType.AUDIT_LOG.value == "audit_log"
        assert ResourceType.KEY_MATERIAL.value == "key_material"


class TestVerfahrenType:
    """Tests for VerfahrenType Enum."""

    def test_exam_types_exist(self):
        """All exam types are defined."""
        assert VerfahrenType.ABITUR.value == "abitur"
        assert VerfahrenType.VORABITUR.value == "vorabitur"
        assert VerfahrenType.KLAUSUR.value == "klausur"
        assert VerfahrenType.NACHPRUEFUNG.value == "nachpruefung"

    def test_certificate_types_exist(self):
        """All certificate types are defined."""
        assert VerfahrenType.HALBJAHRESZEUGNIS.value == "halbjahreszeugnis"
        assert VerfahrenType.JAHRESZEUGNIS.value == "jahreszeugnis"
        assert VerfahrenType.ABSCHLUSSZEUGNIS.value == "abschlusszeugnis"
        assert VerfahrenType.ABGANGSZEUGNIS.value == "abgangszeugnis"

    def test_is_exam_type_returns_true_for_exams(self):
        """is_exam_type() returns True for exam types."""
        assert VerfahrenType.is_exam_type("abitur") is True
        assert VerfahrenType.is_exam_type("vorabitur") is True
        assert VerfahrenType.is_exam_type("klausur") is True
        assert VerfahrenType.is_exam_type("nachpruefung") is True

    def test_is_exam_type_returns_false_for_certificates(self):
        """is_exam_type() returns False for certificate types."""
        assert VerfahrenType.is_exam_type("halbjahreszeugnis") is False
        assert VerfahrenType.is_exam_type("jahreszeugnis") is False
        assert VerfahrenType.is_exam_type("abschlusszeugnis") is False
        assert VerfahrenType.is_exam_type("abgangszeugnis") is False

    def test_is_exam_type_returns_false_for_invalid(self):
        """is_exam_type() returns False for invalid types."""
        assert VerfahrenType.is_exam_type("invalid") is False
        assert VerfahrenType.is_exam_type("") is False

    def test_is_certificate_type_returns_true_for_certificates(self):
        """is_certificate_type() returns True for certificate types."""
        assert VerfahrenType.is_certificate_type("halbjahreszeugnis") is True
        assert VerfahrenType.is_certificate_type("jahreszeugnis") is True
        assert VerfahrenType.is_certificate_type("abschlusszeugnis") is True
        assert VerfahrenType.is_certificate_type("abgangszeugnis") is True

    def test_is_certificate_type_returns_false_for_exams(self):
        """is_certificate_type() returns False for exam types."""
        assert VerfahrenType.is_certificate_type("abitur") is False
        assert VerfahrenType.is_certificate_type("vorabitur") is False
        assert VerfahrenType.is_certificate_type("klausur") is False
        assert VerfahrenType.is_certificate_type("nachpruefung") is False

    def test_is_certificate_type_returns_false_for_invalid(self):
        """is_certificate_type() returns False for invalid types."""
        assert VerfahrenType.is_certificate_type("invalid") is False
        assert VerfahrenType.is_certificate_type("") is False


class TestZKVisibilityMode:
    """Tests for ZK Visibility Mode Enum."""

    def test_all_modes_exist(self):
        """All visibility modes are defined."""
        assert ZKVisibilityMode.BLIND.value == "blind"
        assert ZKVisibilityMode.SEMI.value == "semi"
        assert ZKVisibilityMode.FULL.value == "full"


class TestEHVisibilityMode:
    """Tests for EH Visibility Mode Enum."""

    def test_all_modes_exist(self):
        """All visibility modes are defined."""
        assert EHVisibilityMode.BLIND.value == "blind"
        assert EHVisibilityMode.SHARED.value == "shared"


# =============================================
# DATA STRUCTURE TESTS
# =============================================

class TestPolicySet:
    """Tests for PolicySet dataclass."""

    def test_create_exam_policy(self, sample_policy):
        """PolicySet for exam can be created."""
        assert sample_policy.id == "TEST-2025-ABITUR"
        assert sample_policy.bundesland == "niedersachsen"
        assert sample_policy.jahr == 2025
        assert sample_policy.verfahren == "abitur"

    def test_create_zeugnis_policy(self, sample_zeugnis_policy):
        """PolicySet for certificate can be created."""
        assert sample_zeugnis_policy.verfahren == "halbjahreszeugnis"
        assert sample_zeugnis_policy.require_klassenlehrer_approval is True
        assert sample_zeugnis_policy.kopfnoten_enabled is True

    def test_is_exam_policy(self, sample_policy):
        """is_exam_policy() returns True for exam policies."""
        assert sample_policy.is_exam_policy() is True
        assert sample_policy.is_certificate_policy() is False

    def test_is_certificate_policy(self, sample_zeugnis_policy):
        """is_certificate_policy() returns True for certificate policies."""
        assert sample_zeugnis_policy.is_certificate_policy() is True
        assert sample_zeugnis_policy.is_exam_policy() is False

    def test_policy_to_dict(self, sample_policy):
        """PolicySet can be serialized to dict."""
        d = sample_policy.to_dict()
        assert d["id"] == "TEST-2025-ABITUR"
        assert d["bundesland"] == "niedersachsen"
        assert d["zk_visibility_mode"] == "full"
        assert d["eh_visibility_mode"] == "shared"
        assert "created_at" in d

    def test_default_values(self):
        """PolicySet has sensible defaults."""
        policy = PolicySet(
            id="test",
            bundesland="test",
            jahr=2025,
            fach=None,
            verfahren="abitur"
        )
        assert policy.zk_visibility_mode == ZKVisibilityMode.FULL
        assert policy.eh_visibility_mode == EHVisibilityMode.SHARED
        assert policy.allow_teacher_uploaded_eh is True
        assert policy.third_correction_threshold == 4


class TestRoleAssignment:
    """Tests for RoleAssignment dataclass."""

    def test_create_role_assignment(self):
        """RoleAssignment can be created."""
        assignment = RoleAssignment(
            id=str(uuid.uuid4()),
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001"
        )
        assert assignment.user_id == "user-001"
        assert assignment.role == Role.ERSTKORREKTOR
        assert assignment.is_active() is True

    def test_is_active_returns_true_when_valid(self):
        """is_active() returns True for valid assignment."""
        assignment = RoleAssignment(
            id=str(uuid.uuid4()),
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001"
        )
        assert assignment.is_active() is True

    def test_is_active_returns_false_when_revoked(self):
        """is_active() returns False when revoked."""
        assignment = RoleAssignment(
            id=str(uuid.uuid4()),
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001",
            revoked_at=datetime.now(timezone.utc)
        )
        assert assignment.is_active() is False

    def test_is_active_returns_false_when_expired(self):
        """is_active() returns False when expired."""
        assignment = RoleAssignment(
            id=str(uuid.uuid4()),
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001",
            valid_to=datetime.now(timezone.utc) - timedelta(days=1)
        )
        assert assignment.is_active() is False

    def test_is_active_returns_false_when_not_yet_valid(self):
        """is_active() returns False when not yet valid."""
        assignment = RoleAssignment(
            id=str(uuid.uuid4()),
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001",
            valid_from=datetime.now(timezone.utc) + timedelta(days=1)
        )
        assert assignment.is_active() is False

    def test_role_assignment_to_dict(self):
        """RoleAssignment can be serialized to dict."""
        assignment = RoleAssignment(
            id="assign-001",
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001"
        )
        d = assignment.to_dict()
        assert d["id"] == "assign-001"
        assert d["role"] == "erstkorrektor"
        assert d["resource_type"] == "exam_package"
        assert d["is_active"] is True


class TestKeyShare:
    """Tests for KeyShare dataclass."""

    def test_create_key_share(self):
        """KeyShare can be created."""
        share = KeyShare(
            id=str(uuid.uuid4()),
            user_id="user-002",
            package_id="package-001",
            permissions={"read_original", "read_eh"},
            scope="full",
            granted_by="user-001"
        )
        assert share.user_id == "user-002"
        assert share.is_active() is True
        assert "read_original" in share.permissions

    def test_is_active_returns_true_when_accepted(self):
        """is_active() returns True when accepted."""
        share = KeyShare(
            id=str(uuid.uuid4()),
            user_id="user-002",
            package_id="package-001",
            permissions={"read_original"},
            granted_by="user-001",
            invite_token="token-123",
            accepted_at=datetime.now(timezone.utc)
        )
        assert share.is_active() is True

    def test_is_active_returns_false_when_pending(self):
        """is_active() returns False when invite pending."""
        share = KeyShare(
            id=str(uuid.uuid4()),
            user_id="user-002",
            package_id="package-001",
            permissions={"read_original"},
            granted_by="user-001",
            invite_token="token-123"  # Not accepted
        )
        assert share.is_active() is False

    def test_is_active_returns_false_when_revoked(self):
        """is_active() returns False when revoked."""
        share = KeyShare(
            id=str(uuid.uuid4()),
            user_id="user-002",
            package_id="package-001",
            permissions={"read_original"},
            granted_by="user-001",
            revoked_at=datetime.now(timezone.utc)
        )
        assert share.is_active() is False

    def test_key_share_to_dict(self):
        """KeyShare can be serialized to dict."""
        share = KeyShare(
            id="share-001",
            user_id="user-002",
            package_id="package-001",
            permissions={"read_original", "read_eh"},
            scope="full",
            granted_by="user-001"
        )
        d = share.to_dict()
        assert d["id"] == "share-001"
        assert "read_original" in d["permissions"]
        assert d["is_active"] is True


class TestTenant:
    """Tests for Tenant dataclass."""

    def test_create_tenant(self, sample_tenant):
        """Tenant can be created."""
        assert sample_tenant.id == "school-001"
        assert sample_tenant.bundesland == "niedersachsen"
        assert sample_tenant.encryption_enabled is True

    def test_tenant_to_dict(self, sample_tenant):
        """Tenant can be serialized to dict."""
        d = sample_tenant.to_dict()
        assert d["id"] == "school-001"
        assert d["tenant_type"] == "school"
        assert "created_at" in d


class TestNamespace:
    """Tests for Namespace dataclass."""

    def test_create_namespace(self, sample_namespace):
        """Namespace can be created."""
        assert sample_namespace.id == "namespace-001"
        assert sample_namespace.fach == "deutsch"
        assert sample_namespace.pruefungsart == "abitur"

    def test_namespace_to_dict(self, sample_namespace):
        """Namespace can be serialized to dict."""
        d = sample_namespace.to_dict()
        assert d["id"] == "namespace-001"
        assert d["jahr"] == 2025
        assert d["fach"] == "deutsch"


class TestExamPackage:
    """Tests for ExamPackage dataclass."""

    def test_create_exam_package(self, sample_exam_package):
        """ExamPackage can be created."""
        assert sample_exam_package.id == "package-001"
        assert sample_exam_package.status == "draft"
        assert sample_exam_package.owner_id == "teacher-001"

    def test_exam_package_to_dict(self, sample_exam_package):
        """ExamPackage can be serialized to dict."""
        d = sample_exam_package.to_dict()
        assert d["id"] == "package-001"
        assert d["status"] == "draft"
        assert "created_at" in d


# =============================================
# POLICY ENGINE TESTS
# =============================================

class TestPolicyEngineRegistration:
    """Tests for PolicyEngine policy registration."""

    def test_register_policy_set(self, policy_engine, sample_policy):
        """PolicySet can be registered."""
        policy_engine.register_policy_set(sample_policy)
        assert sample_policy.id in policy_engine.policy_sets

    def test_get_policy_for_context_exact_match(self, policy_engine, sample_policy):
        """get_policy_for_context() returns exact match."""
        policy_engine.register_policy_set(sample_policy)

        found = policy_engine.get_policy_for_context(
            bundesland="niedersachsen",
            jahr=2025,
            fach="deutsch",
            verfahren="abitur"
        )
        assert found is not None
        assert found.id == sample_policy.id

    def test_get_policy_for_context_fallback_to_default(self, policy_engine):
        """get_policy_for_context() falls back to DEFAULT."""
        default_policy = PolicySet(
            id="DEFAULT-2025",
            bundesland="DEFAULT",
            jahr=2025,
            fach=None,
            verfahren="abitur"
        )
        policy_engine.register_policy_set(default_policy)

        found = policy_engine.get_policy_for_context(
            bundesland="unknown-land",
            jahr=2025,
            verfahren="abitur"
        )
        assert found is not None
        assert found.bundesland == "DEFAULT"

    def test_get_policy_for_context_returns_none_when_no_match(self, policy_engine):
        """get_policy_for_context() returns None when no match."""
        found = policy_engine.get_policy_for_context(
            bundesland="unknown",
            jahr=2025,
            verfahren="abitur"
        )
        assert found is None


class TestPolicyEngineRoleAssignment:
    """Tests for PolicyEngine role assignment."""

    def test_assign_role_creates_assignment(self, policy_engine):
        """assign_role() creates a RoleAssignment."""
        assignment = policy_engine.assign_role(
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001"
        )

        assert assignment is not None
        assert assignment.role == Role.ERSTKORREKTOR
        assert "user-001" in policy_engine.role_assignments

    def test_get_user_roles_returns_active_roles(self, policy_engine):
        """get_user_roles() returns active roles."""
        policy_engine.assign_role(
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001"
        )
        policy_engine.assign_role(
            user_id="user-001",
            role=Role.FACHVORSITZ,
            resource_type=ResourceType.NAMESPACE,
            resource_id="namespace-001",
            granted_by="admin-001"
        )

        roles = policy_engine.get_user_roles("user-001")
        assert Role.ERSTKORREKTOR in roles
        assert Role.FACHVORSITZ in roles

    def test_get_user_roles_filters_by_resource_type(self, policy_engine):
        """get_user_roles() filters by resource type."""
        policy_engine.assign_role(
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001"
        )

        roles = policy_engine.get_user_roles(
            "user-001",
            resource_type=ResourceType.EXAM_PACKAGE
        )
        assert Role.ERSTKORREKTOR in roles

        roles = policy_engine.get_user_roles(
            "user-001",
            resource_type=ResourceType.NAMESPACE
        )
        assert Role.ERSTKORREKTOR not in roles

    def test_get_user_roles_filters_by_resource_id(self, policy_engine):
        """get_user_roles() filters by resource ID."""
        policy_engine.assign_role(
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001"
        )

        roles = policy_engine.get_user_roles(
            "user-001",
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001"
        )
        assert Role.ERSTKORREKTOR in roles

        roles = policy_engine.get_user_roles(
            "user-001",
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-other"
        )
        assert Role.ERSTKORREKTOR not in roles

    def test_revoke_role_deactivates_assignment(self, policy_engine):
        """revoke_role() deactivates the assignment."""
        assignment = policy_engine.assign_role(
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001"
        )

        result = policy_engine.revoke_role(assignment.id, "admin-001")
        assert result is True

        roles = policy_engine.get_user_roles("user-001")
        assert Role.ERSTKORREKTOR not in roles

    def test_revoke_role_returns_false_for_invalid_id(self, policy_engine):
        """revoke_role() returns False for invalid assignment ID."""
        result = policy_engine.revoke_role("invalid-id", "admin-001")
        assert result is False


class TestPolicyEngineKeyShare:
    """Tests for PolicyEngine key sharing."""

    def test_create_key_share(self, policy_engine):
        """create_key_share() creates a KeyShare."""
        share = policy_engine.create_key_share(
            user_id="user-002",
            package_id="package-001",
            permissions={"read_original", "read_eh"},
            granted_by="user-001"
        )

        assert share is not None
        assert share.user_id == "user-002"
        assert "user-002" in policy_engine.key_shares

    def test_create_key_share_with_invite_token(self, policy_engine):
        """create_key_share() with invite token creates pending share."""
        share = policy_engine.create_key_share(
            user_id="user-002",
            package_id="package-001",
            permissions={"read_original"},
            granted_by="user-001",
            invite_token="token-123"
        )

        assert share.invite_token == "token-123"
        assert share.is_active() is False

    def test_accept_key_share(self, policy_engine):
        """accept_key_share() activates the share."""
        share = policy_engine.create_key_share(
            user_id="user-002",
            package_id="package-001",
            permissions={"read_original"},
            granted_by="user-001",
            invite_token="token-123"
        )

        result = policy_engine.accept_key_share(share.id, "token-123")
        assert result is True
        assert share.is_active() is True

    def test_accept_key_share_wrong_token(self, policy_engine):
        """accept_key_share() fails with wrong token."""
        share = policy_engine.create_key_share(
            user_id="user-002",
            package_id="package-001",
            permissions={"read_original"},
            granted_by="user-001",
            invite_token="token-123"
        )

        result = policy_engine.accept_key_share(share.id, "wrong-token")
        assert result is False

    def test_revoke_key_share(self, policy_engine):
        """revoke_key_share() deactivates the share."""
        share = policy_engine.create_key_share(
            user_id="user-002",
            package_id="package-001",
            permissions={"read_original"},
            granted_by="user-001"
        )

        result = policy_engine.revoke_key_share(share.id, "user-001")
        assert result is True
        assert share.is_active() is False


class TestPolicyEnginePermissionCheck:
    """Tests for PolicyEngine permission checking."""

    def test_check_permission_returns_true_for_valid(self, policy_engine):
        """check_permission() returns True for valid permission."""
        policy_engine.assign_role(
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001"
        )

        result = policy_engine.check_permission(
            user_id="user-001",
            action=Action.READ,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001"
        )
        assert result is True

    def test_check_permission_returns_false_without_role(self, policy_engine):
        """check_permission() returns False without role."""
        result = policy_engine.check_permission(
            user_id="user-001",
            action=Action.READ,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001"
        )
        assert result is False

    def test_check_permission_returns_false_for_disallowed_action(self, policy_engine):
        """check_permission() returns False for disallowed action."""
        policy_engine.assign_role(
            user_id="user-001",
            role=Role.ZWEITKORREKTOR,  # ZK cannot SHARE_KEY
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001"
        )

        result = policy_engine.check_permission(
            user_id="user-001",
            action=Action.SHARE_KEY,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001"
        )
        assert result is False

    def test_check_permission_with_key_share_required(self, policy_engine):
        """check_permission() checks key share when package_id specified."""
        policy_engine.assign_role(
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001"
        )

        # Without key share
        result = policy_engine.check_permission(
            user_id="user-001",
            action=Action.READ,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            package_id="package-001"
        )
        assert result is False

        # With key share
        policy_engine.create_key_share(
            user_id="user-001",
            package_id="package-001",
            permissions={"read_original"},
            granted_by="admin-001"
        )

        result = policy_engine.check_permission(
            user_id="user-001",
            action=Action.READ,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            package_id="package-001"
        )
        assert result is True

    def test_check_permission_with_semi_visibility_policy(self, policy_engine):
        """check_permission() applies SEMI visibility for ZK."""
        semi_policy = PolicySet(
            id="TEST-SEMI",
            bundesland="bayern",
            jahr=2025,
            fach=None,
            verfahren="abitur",
            zk_visibility_mode=ZKVisibilityMode.SEMI
        )

        policy_engine.assign_role(
            user_id="user-002",
            role=Role.ZWEITKORREKTOR,
            resource_type=ResourceType.GRADE_DECISION,
            resource_id="decision-001",
            granted_by="admin-001"
        )

        result = policy_engine.check_permission(
            user_id="user-002",
            action=Action.READ,
            resource_type=ResourceType.GRADE_DECISION,
            resource_id="decision-001",
            policy=semi_policy
        )
        assert result is False  # SEMI mode blocks grade decision reading

    def test_get_allowed_actions(self, policy_engine):
        """get_allowed_actions() returns all allowed actions."""
        policy_engine.assign_role(
            user_id="user-001",
            role=Role.ERSTKORREKTOR,
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001",
            granted_by="admin-001"
        )

        allowed = policy_engine.get_allowed_actions(
            user_id="user-001",
            resource_type=ResourceType.EXAM_PACKAGE,
            resource_id="package-001"
        )

        assert Action.READ in allowed
        assert Action.UPDATE in allowed
        assert Action.SHARE_KEY in allowed
        assert Action.LOCK in allowed


# =============================================
# DEFAULT PERMISSIONS TESTS
# =============================================

class TestDefaultPermissions:
    """Tests for DEFAULT_PERMISSIONS matrix."""

    def test_erstkorrektor_has_exam_permissions(self):
        """ERSTKORREKTOR has exam package permissions."""
        perms = DEFAULT_PERMISSIONS[Role.ERSTKORREKTOR]

        assert Action.READ in perms[ResourceType.EXAM_PACKAGE]
        assert Action.UPDATE in perms[ResourceType.EXAM_PACKAGE]
        assert Action.SHARE_KEY in perms[ResourceType.EXAM_PACKAGE]
        assert Action.LOCK in perms[ResourceType.EXAM_PACKAGE]

    def test_zweitkorrektor_has_limited_permissions(self):
        """ZWEITKORREKTOR has limited permissions."""
        perms = DEFAULT_PERMISSIONS[Role.ZWEITKORREKTOR]

        assert Action.READ in perms[ResourceType.EXAM_PACKAGE]
        assert Action.SHARE_KEY not in perms[ResourceType.EXAM_PACKAGE]

    def test_klassenlehrer_has_zeugnis_permissions(self):
        """KLASSENLEHRER has certificate permissions."""
        perms = DEFAULT_PERMISSIONS[Role.KLASSENLEHRER]

        assert Action.CREATE in perms[ResourceType.ZEUGNIS]
        assert Action.READ in perms[ResourceType.ZEUGNIS]
        assert Action.UPDATE in perms[ResourceType.ZEUGNIS]
        assert Action.CREATE in perms[ResourceType.KOPFNOTE]
        assert Action.CREATE in perms[ResourceType.BEMERKUNG]

    def test_fachlehrer_has_fachnote_permissions(self):
        """FACHLEHRER has grade entry permissions."""
        perms = DEFAULT_PERMISSIONS[Role.FACHLEHRER]

        assert Action.CREATE in perms[ResourceType.FACHNOTE]
        assert Action.READ in perms[ResourceType.FACHNOTE]
        assert Action.UPDATE in perms[ResourceType.FACHNOTE]
        # FACHLEHRER cannot modify Kopfnoten
        assert ResourceType.KOPFNOTE not in perms

    def test_zeugnisbeauftragter_has_review_permissions(self):
        """ZEUGNISBEAUFTRAGTER has review permissions."""
        perms = DEFAULT_PERMISSIONS[Role.ZEUGNISBEAUFTRAGTER]

        assert Action.READ in perms[ResourceType.ZEUGNIS]
        assert Action.UPDATE in perms[ResourceType.ZEUGNIS]
        assert Action.UPLOAD in perms[ResourceType.ZEUGNIS_VORLAGE]

    def test_sekretariat_has_print_permissions(self):
        """SEKRETARIAT has print/export permissions."""
        perms = DEFAULT_PERMISSIONS[Role.SEKRETARIAT]

        assert Action.READ in perms[ResourceType.ZEUGNIS]
        assert Action.DOWNLOAD in perms[ResourceType.ZEUGNIS]
        assert Action.CREATE in perms[ResourceType.EXPORT]

    def test_schulleitung_has_signoff_permissions(self):
        """SCHULLEITUNG has sign-off permissions."""
        perms = DEFAULT_PERMISSIONS[Role.SCHULLEITUNG]

        assert Action.SIGN_OFF in perms[ResourceType.ZEUGNIS]
        assert Action.LOCK in perms[ResourceType.ZEUGNIS]
        assert Action.SIGN_OFF in perms[ResourceType.VERSETZUNG]
        assert Action.SIGN_OFF in perms[ResourceType.KONFERENZ_BESCHLUSS]

    def test_stufenleitung_has_coordination_permissions(self):
        """STUFENLEITUNG has coordination permissions."""
        perms = DEFAULT_PERMISSIONS[Role.STUFENLEITUNG]

        assert Action.READ in perms[ResourceType.ZEUGNIS]
        assert Action.UPDATE in perms[ResourceType.ZEUGNIS]
        assert Action.UPDATE in perms[ResourceType.VERSETZUNG]

    def test_land_admin_has_publish_permissions(self):
        """LAND_ADMIN has official EH publish permissions."""
        perms = DEFAULT_PERMISSIONS[Role.LAND_ADMIN]

        assert Action.PUBLISH_OFFICIAL in perms[ResourceType.EH_DOCUMENT]

    def test_auditor_has_limited_read_access(self):
        """AUDITOR has limited read-only access."""
        perms = DEFAULT_PERMISSIONS[Role.AUDITOR]

        assert Action.READ in perms[ResourceType.AUDIT_LOG]
        assert Action.READ in perms[ResourceType.EXAM_PACKAGE]
        # No write permissions
        assert ResourceType.STUDENT_WORK not in perms


# =============================================
# ZEUGNIS WORKFLOW TESTS
# =============================================

class TestZeugnisWorkflow:
    """Tests for certificate workflow role chain."""

    def test_fachlehrer_can_enter_grades(self, policy_engine):
        """FACHLEHRER can enter grades for their subject."""
        policy_engine.assign_role(
            user_id="lehrer-mathe",
            role=Role.FACHLEHRER,
            resource_type=ResourceType.FACHNOTE,
            resource_id="klasse-7a-mathe",
            granted_by="admin-001"
        )

        assert policy_engine.check_permission(
            user_id="lehrer-mathe",
            action=Action.CREATE,
            resource_type=ResourceType.FACHNOTE,
            resource_id="klasse-7a-mathe"
        ) is True

    def test_klassenlehrer_can_create_zeugnis(self, policy_engine):
        """KLASSENLEHRER can create certificates."""
        policy_engine.assign_role(
            user_id="klassenlehrer-7a",
            role=Role.KLASSENLEHRER,
            resource_type=ResourceType.ZEUGNIS,
            resource_id="klasse-7a",
            granted_by="admin-001"
        )

        assert policy_engine.check_permission(
            user_id="klassenlehrer-7a",
            action=Action.CREATE,
            resource_type=ResourceType.ZEUGNIS,
            resource_id="klasse-7a"
        ) is True

    def test_klassenlehrer_can_add_kopfnoten(self, policy_engine):
        """KLASSENLEHRER can add Kopfnoten."""
        policy_engine.assign_role(
            user_id="klassenlehrer-7a",
            role=Role.KLASSENLEHRER,
            resource_type=ResourceType.KOPFNOTE,
            resource_id="klasse-7a",
            granted_by="admin-001"
        )

        assert policy_engine.check_permission(
            user_id="klassenlehrer-7a",
            action=Action.CREATE,
            resource_type=ResourceType.KOPFNOTE,
            resource_id="klasse-7a"
        ) is True

    def test_zeugnisbeauftragter_can_review(self, policy_engine):
        """ZEUGNISBEAUFTRAGTER can review certificates."""
        policy_engine.assign_role(
            user_id="zb-001",
            role=Role.ZEUGNISBEAUFTRAGTER,
            resource_type=ResourceType.ZEUGNIS,
            resource_id="klasse-7a",
            granted_by="admin-001"
        )

        assert policy_engine.check_permission(
            user_id="zb-001",
            action=Action.UPDATE,
            resource_type=ResourceType.ZEUGNIS,
            resource_id="klasse-7a"
        ) is True

    def test_schulleitung_can_sign_off(self, policy_engine):
        """SCHULLEITUNG can sign off certificates."""
        policy_engine.assign_role(
            user_id="schulleiter",
            role=Role.SCHULLEITUNG,
            resource_type=ResourceType.ZEUGNIS,
            resource_id="klasse-7a",
            granted_by="admin-001"
        )

        assert policy_engine.check_permission(
            user_id="schulleiter",
            action=Action.SIGN_OFF,
            resource_type=ResourceType.ZEUGNIS,
            resource_id="klasse-7a"
        ) is True

    def test_sekretariat_can_export(self, policy_engine):
        """SEKRETARIAT can export for printing."""
        policy_engine.assign_role(
            user_id="sekretariat-001",
            role=Role.SEKRETARIAT,
            resource_type=ResourceType.EXPORT,
            resource_id="klasse-7a",
            granted_by="admin-001"
        )

        assert policy_engine.check_permission(
            user_id="sekretariat-001",
            action=Action.CREATE,
            resource_type=ResourceType.EXPORT,
            resource_id="klasse-7a"
        ) is True


# =============================================
# CREATE DEFAULT POLICY SETS TESTS
# =============================================

class TestCreateDefaultPolicySets:
    """Tests for create_default_policy_sets()."""

    def test_creates_default_policy(self):
        """Creates a DEFAULT fallback policy."""
        policies = create_default_policy_sets()
        default = next((p for p in policies if p.bundesland == "DEFAULT"), None)

        assert default is not None
        assert default.jahr == 2025

    def test_creates_niedersachsen_policy(self):
        """Creates Niedersachsen-specific policy."""
        policies = create_default_policy_sets()
        ni = next((p for p in policies if p.bundesland == "niedersachsen"), None)

        assert ni is not None
        assert ni.zk_visibility_mode == ZKVisibilityMode.FULL

    def test_creates_bayern_policy_with_semi_visibility(self):
        """Creates Bayern policy with SEMI visibility."""
        policies = create_default_policy_sets()
        by = next((p for p in policies if p.bundesland == "bayern"), None)

        assert by is not None
        assert by.zk_visibility_mode == ZKVisibilityMode.SEMI

    def test_creates_policies_for_all_bundeslaender(self):
        """Creates policies for all 16 Bundeslaender."""
        policies = create_default_policy_sets()
        bundeslaender = {p.bundesland for p in policies}

        expected = {
            "DEFAULT", "baden-wuerttemberg", "bayern", "berlin", "brandenburg",
            "bremen", "hamburg", "hessen", "mecklenburg-vorpommern",
            "niedersachsen", "nordrhein-westfalen", "rheinland-pfalz",
            "saarland", "sachsen", "sachsen-anhalt", "schleswig-holstein",
            "thueringen"
        }

        assert bundeslaender == expected


# =============================================
# GET POLICY ENGINE SINGLETON TESTS
# =============================================

class TestGetPolicyEngine:
    """Tests for get_policy_engine() singleton."""

    def test_returns_engine_instance(self):
        """get_policy_engine() returns PolicyEngine."""
        engine = get_policy_engine()
        assert isinstance(engine, PolicyEngine)

    def test_returns_same_instance(self):
        """get_policy_engine() returns same instance."""
        engine1 = get_policy_engine()
        engine2 = get_policy_engine()
        assert engine1 is engine2

    def test_has_default_policies_registered(self):
        """Singleton has default policies registered."""
        engine = get_policy_engine()
        assert len(engine.policy_sets) > 0
        assert "DEFAULT-2025" in engine.policy_sets


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
