"""
Tests for Phase 7: Space Composability.

Tests space references, variable/config inheritance, and access control.
"""

import pytest
import tempfile
from pathlib import Path
from datetime import datetime, timedelta

from markitect.spaces import (
    # Models
    InformationSpace,
    SpaceConfig,
    SpaceMetadata,
    SpaceVariable,
    SpaceStatus,
    # Composability Models
    SpaceReference,
    SpaceReferenceType,
    SpacePermission,
    SpaceRole,
    AccessLevel,
    SpaceAccess,
    InheritedVariable,
    InheritedConfig,
    # Services
    ComposableSpaceService,
    InheritanceResolver,
    AccessControlService,
    CircularReferenceError,
    # Repositories
    SqliteSpaceRepository,
    SqliteVariableRepository,
    SqliteSpaceReferenceRepository,
    SqliteAccessControlRepository,
)


# ===========================================================================
# Fixtures
# ===========================================================================


@pytest.fixture
def db_path():
    """Create a temporary database path."""
    with tempfile.TemporaryDirectory() as tmpdir:
        yield Path(tmpdir) / "test.db"


@pytest.fixture
def space_repo(db_path):
    """Create a space repository."""
    return SqliteSpaceRepository(db_path)


@pytest.fixture
def variable_repo(db_path):
    """Create a variable repository."""
    return SqliteVariableRepository(db_path)


@pytest.fixture
def reference_repo(db_path):
    """Create a reference repository."""
    return SqliteSpaceReferenceRepository(db_path)


@pytest.fixture
def permission_repo(db_path):
    """Create a permission repository."""
    return SqliteAccessControlRepository(db_path)


@pytest.fixture
def inheritance_resolver(space_repo, variable_repo, reference_repo):
    """Create an inheritance resolver."""
    return InheritanceResolver(
        space_repo=space_repo,
        variable_repo=variable_repo,
        reference_repo=reference_repo,
    )


@pytest.fixture
def access_service(permission_repo, space_repo, inheritance_resolver):
    """Create an access control service."""
    return AccessControlService(
        permission_repo=permission_repo,
        space_repo=space_repo,
        inheritance_resolver=inheritance_resolver,
    )


@pytest.fixture
def composable_service(space_repo, variable_repo, reference_repo, permission_repo):
    """Create a composable space service."""
    return ComposableSpaceService(
        space_repo=space_repo,
        variable_repo=variable_repo,
        reference_repo=reference_repo,
        permission_repo=permission_repo,
    )


# ===========================================================================
# SpaceReference Model Tests
# ===========================================================================


class TestSpaceReferenceModel:
    """Tests for SpaceReference model."""

    def test_create_reference(self):
        """Test creating a space reference."""
        ref = SpaceReference(
            source_space_id="space-1",
            target_space_id="space-2",
            reference_type=SpaceReferenceType.INCLUDES,
        )
        assert ref.source_space_id == "space-1"
        assert ref.target_space_id == "space-2"
        assert ref.reference_type == SpaceReferenceType.INCLUDES
        assert ref.id is not None

    def test_reference_with_alias(self):
        """Test reference with alias."""
        ref = SpaceReference(
            source_space_id="space-1",
            target_space_id="space-2",
            reference_type=SpaceReferenceType.LINKS_TO,
            alias="common-components",
        )
        assert ref.alias == "common-components"

    def test_reference_to_dict(self):
        """Test serialization to dict."""
        ref = SpaceReference(
            source_space_id="space-1",
            target_space_id="space-2",
            reference_type=SpaceReferenceType.EXTENDS,
            alias="base",
            metadata={"priority": 1},
        )
        data = ref.to_dict()
        assert data["source_space_id"] == "space-1"
        assert data["target_space_id"] == "space-2"
        assert data["reference_type"] == "extends"
        assert data["alias"] == "base"
        assert data["metadata"]["priority"] == 1

    def test_reference_from_dict(self):
        """Test deserialization from dict."""
        data = {
            "id": "ref-1",
            "source_space_id": "space-1",
            "target_space_id": "space-2",
            "reference_type": "composed_of",
            "alias": "part-a",
            "metadata": {"order": 1},
            "created_at": "2024-01-01T00:00:00",
        }
        ref = SpaceReference.from_dict(data)
        assert ref.id == "ref-1"
        assert ref.reference_type == SpaceReferenceType.COMPOSED_OF
        assert ref.alias == "part-a"


# ===========================================================================
# SpacePermission Model Tests
# ===========================================================================


class TestSpacePermissionModel:
    """Tests for SpacePermission model."""

    def test_create_permission(self):
        """Test creating a permission."""
        perm = SpacePermission(
            space_id="space-1",
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.WRITE,
        )
        assert perm.space_id == "space-1"
        assert perm.principal_id == "user-1"
        assert perm.access_level == AccessLevel.WRITE

    def test_permission_with_role(self):
        """Test permission with role."""
        perm = SpacePermission(
            space_id="space-1",
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.ADMIN,
            role=SpaceRole.OWNER,
        )
        assert perm.role == SpaceRole.OWNER

    def test_permission_expiration(self):
        """Test permission expiration check."""
        # Not expired
        perm = SpacePermission(
            space_id="space-1",
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.READ,
            expires_at=datetime.now() + timedelta(days=1),
        )
        assert not perm.is_expired()

        # Expired
        perm_expired = SpacePermission(
            space_id="space-1",
            principal_type="user",
            principal_id="user-2",
            access_level=AccessLevel.READ,
            expires_at=datetime.now() - timedelta(days=1),
        )
        assert perm_expired.is_expired()

    def test_has_access(self):
        """Test has_access check."""
        perm = SpacePermission(
            space_id="space-1",
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.WRITE,
        )
        assert perm.has_access(AccessLevel.READ)
        assert perm.has_access(AccessLevel.WRITE)
        assert not perm.has_access(AccessLevel.ADMIN)

    def test_permission_to_dict(self):
        """Test serialization."""
        perm = SpacePermission(
            space_id="space-1",
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.ADMIN,
            role=SpaceRole.OWNER,
            granted_by="admin",
        )
        data = perm.to_dict()
        assert data["access_level"] == "admin"
        assert data["role"] == "owner"
        assert data["granted_by"] == "admin"


# ===========================================================================
# SpaceAccess Model Tests
# ===========================================================================


class TestSpaceAccessModel:
    """Tests for SpaceAccess model."""

    def test_access_checks(self):
        """Test access level checks."""
        access = SpaceAccess(
            space_id="space-1",
            principal_id="user-1",
            effective_level=AccessLevel.WRITE,
        )
        assert access.can_read()
        assert access.can_write()
        assert not access.is_admin()

    def test_admin_access(self):
        """Test admin access."""
        access = SpaceAccess(
            space_id="space-1",
            principal_id="user-1",
            effective_level=AccessLevel.ADMIN,
            roles=[SpaceRole.OWNER],
        )
        assert access.can_read()
        assert access.can_write()
        assert access.is_admin()

    def test_no_access(self):
        """Test no access."""
        access = SpaceAccess(
            space_id="space-1",
            principal_id="user-1",
            effective_level=AccessLevel.NONE,
        )
        assert not access.can_read()
        assert not access.can_write()
        assert not access.is_admin()


# ===========================================================================
# InheritedVariable Tests
# ===========================================================================


class TestInheritedVariable:
    """Tests for InheritedVariable."""

    def test_local_variable(self):
        """Test local variable detection."""
        var = InheritedVariable(
            name="version",
            value="1.0",
            source_space_id="space-1",
            inheritance_depth=0,
        )
        assert var.is_local()

    def test_inherited_variable(self):
        """Test inherited variable."""
        var = InheritedVariable(
            name="company",
            value="Acme Corp",
            source_space_id="parent-space",
            inheritance_depth=2,
        )
        assert not var.is_local()
        assert var.inheritance_depth == 2


# ===========================================================================
# InheritedConfig Tests
# ===========================================================================


class TestInheritedConfig:
    """Tests for InheritedConfig."""

    def test_default_config(self):
        """Test default config values."""
        config = InheritedConfig()
        assert config.default_variant == "hierarchical"
        assert config.enable_caching is True
        assert config.theme is None

    def test_source_tracking(self):
        """Test config source tracking."""
        config = InheritedConfig(
            theme="dark",
            source_spaces={"theme": "parent-space"},
        )
        assert config.get_source("theme") == "parent-space"
        assert config.is_inherited("theme", "child-space")
        assert not config.is_inherited("theme", "parent-space")


# ===========================================================================
# SpaceReferenceRepository Tests
# ===========================================================================


class TestSqliteSpaceReferenceRepository:
    """Tests for SQLite space reference repository."""

    def test_add_reference(self, reference_repo):
        """Test adding a reference."""
        ref = SpaceReference(
            source_space_id="space-1",
            target_space_id="space-2",
            reference_type=SpaceReferenceType.INCLUDES,
        )
        saved = reference_repo.add_reference(ref)
        assert saved.id == ref.id

    def test_get_reference(self, reference_repo):
        """Test getting a reference by ID."""
        ref = SpaceReference(
            source_space_id="space-1",
            target_space_id="space-2",
            reference_type=SpaceReferenceType.LINKS_TO,
        )
        reference_repo.add_reference(ref)

        found = reference_repo.get_reference(ref.id)
        assert found is not None
        assert found.source_space_id == "space-1"

    def test_get_references_from(self, reference_repo):
        """Test getting references from a source."""
        ref1 = SpaceReference(
            source_space_id="space-1",
            target_space_id="space-2",
            reference_type=SpaceReferenceType.INCLUDES,
        )
        ref2 = SpaceReference(
            source_space_id="space-1",
            target_space_id="space-3",
            reference_type=SpaceReferenceType.LINKS_TO,
        )
        reference_repo.add_reference(ref1)
        reference_repo.add_reference(ref2)

        refs = reference_repo.get_references_from("space-1")
        assert len(refs) == 2

        # Filter by type
        refs_includes = reference_repo.get_references_from(
            "space-1", SpaceReferenceType.INCLUDES
        )
        assert len(refs_includes) == 1
        assert refs_includes[0].target_space_id == "space-2"

    def test_get_references_to(self, reference_repo):
        """Test getting references to a target."""
        ref1 = SpaceReference(
            source_space_id="space-1",
            target_space_id="space-3",
            reference_type=SpaceReferenceType.INCLUDES,
        )
        ref2 = SpaceReference(
            source_space_id="space-2",
            target_space_id="space-3",
            reference_type=SpaceReferenceType.LINKS_TO,
        )
        reference_repo.add_reference(ref1)
        reference_repo.add_reference(ref2)

        refs = reference_repo.get_references_to("space-3")
        assert len(refs) == 2

    def test_remove_reference(self, reference_repo):
        """Test removing a reference."""
        ref = SpaceReference(
            source_space_id="space-1",
            target_space_id="space-2",
            reference_type=SpaceReferenceType.INCLUDES,
        )
        reference_repo.add_reference(ref)
        assert reference_repo.remove_reference(ref.id)
        assert reference_repo.get_reference(ref.id) is None

    def test_reference_exists(self, reference_repo):
        """Test checking if reference exists."""
        ref = SpaceReference(
            source_space_id="space-1",
            target_space_id="space-2",
            reference_type=SpaceReferenceType.EXTENDS,
        )
        reference_repo.add_reference(ref)

        assert reference_repo.reference_exists(
            "space-1", "space-2", SpaceReferenceType.EXTENDS
        )
        assert not reference_repo.reference_exists(
            "space-1", "space-2", SpaceReferenceType.INCLUDES
        )


# ===========================================================================
# AccessControlRepository Tests
# ===========================================================================


class TestSqliteAccessControlRepository:
    """Tests for SQLite access control repository."""

    def test_grant_permission(self, permission_repo):
        """Test granting a permission."""
        perm = SpacePermission(
            space_id="space-1",
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.WRITE,
        )
        saved = permission_repo.grant_permission(perm)
        assert saved.space_id == "space-1"

    def test_get_permission(self, permission_repo):
        """Test getting a specific permission."""
        perm = SpacePermission(
            space_id="space-1",
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.READ,
        )
        permission_repo.grant_permission(perm)

        found = permission_repo.get_permission("space-1", "user", "user-1")
        assert found is not None
        assert found.access_level == AccessLevel.READ

    def test_revoke_permission(self, permission_repo):
        """Test revoking a permission."""
        perm = SpacePermission(
            space_id="space-1",
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.ADMIN,
        )
        permission_repo.grant_permission(perm)
        assert permission_repo.revoke_permission("space-1", "user", "user-1")
        assert permission_repo.get_permission("space-1", "user", "user-1") is None

    def test_get_permissions_for_space(self, permission_repo):
        """Test getting all permissions for a space."""
        perm1 = SpacePermission(
            space_id="space-1",
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.READ,
        )
        perm2 = SpacePermission(
            space_id="space-1",
            principal_type="user",
            principal_id="user-2",
            access_level=AccessLevel.WRITE,
        )
        permission_repo.grant_permission(perm1)
        permission_repo.grant_permission(perm2)

        perms = permission_repo.get_permissions_for_space("space-1")
        assert len(perms) == 2

    def test_get_permissions_for_principal(self, permission_repo):
        """Test getting all permissions for a principal."""
        perm1 = SpacePermission(
            space_id="space-1",
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.READ,
        )
        perm2 = SpacePermission(
            space_id="space-2",
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.ADMIN,
        )
        permission_repo.grant_permission(perm1)
        permission_repo.grant_permission(perm2)

        perms = permission_repo.get_permissions_for_principal("user", "user-1")
        assert len(perms) == 2


# ===========================================================================
# InheritanceResolver Tests
# ===========================================================================


class TestInheritanceResolver:
    """Tests for inheritance resolution."""

    def test_get_parent_chain_no_parent(self, inheritance_resolver, space_repo):
        """Test getting parent chain for space without parent."""
        space = InformationSpace(name="root-space")
        space_repo.create(space)

        chain = inheritance_resolver.get_parent_chain(space.id)
        assert chain == []

    def test_get_parent_chain_single_parent(self, inheritance_resolver, space_repo):
        """Test getting parent chain with one parent."""
        parent = InformationSpace(name="parent-space")
        space_repo.create(parent)

        child = InformationSpace(name="child-space", parent_space_id=parent.id)
        space_repo.create(child)

        chain = inheritance_resolver.get_parent_chain(child.id)
        assert len(chain) == 1
        assert chain[0].id == parent.id

    def test_get_parent_chain_multiple_levels(self, inheritance_resolver, space_repo):
        """Test getting parent chain with multiple levels."""
        grandparent = InformationSpace(name="grandparent")
        space_repo.create(grandparent)

        parent = InformationSpace(name="parent", parent_space_id=grandparent.id)
        space_repo.create(parent)

        child = InformationSpace(name="child", parent_space_id=parent.id)
        space_repo.create(child)

        chain = inheritance_resolver.get_parent_chain(child.id)
        assert len(chain) == 2
        assert chain[0].id == parent.id
        assert chain[1].id == grandparent.id

    def test_resolve_variable_local(self, inheritance_resolver, space_repo, variable_repo):
        """Test resolving a local variable."""
        space = InformationSpace(name="test-space")
        space_repo.create(space)

        var = SpaceVariable(space_id=space.id, name="version", value="1.0")
        variable_repo.set_variable(var)

        resolved = inheritance_resolver.resolve_variable(space.id, "version")
        assert resolved is not None
        assert resolved.value == "1.0"
        assert resolved.is_local()

    def test_resolve_variable_inherited(
        self, inheritance_resolver, space_repo, variable_repo
    ):
        """Test resolving an inherited variable."""
        parent = InformationSpace(name="parent-space")
        space_repo.create(parent)

        child = InformationSpace(name="child-space", parent_space_id=parent.id)
        space_repo.create(child)

        # Set variable in parent only
        var = SpaceVariable(space_id=parent.id, name="company", value="Acme")
        variable_repo.set_variable(var)

        resolved = inheritance_resolver.resolve_variable(child.id, "company")
        assert resolved is not None
        assert resolved.value == "Acme"
        assert not resolved.is_local()
        assert resolved.source_space_id == parent.id

    def test_resolve_variable_override(
        self, inheritance_resolver, space_repo, variable_repo
    ):
        """Test that local variable overrides inherited."""
        parent = InformationSpace(name="parent-space")
        space_repo.create(parent)

        child = InformationSpace(name="child-space", parent_space_id=parent.id)
        space_repo.create(child)

        # Set in both
        var_parent = SpaceVariable(space_id=parent.id, name="version", value="1.0")
        variable_repo.set_variable(var_parent)

        var_child = SpaceVariable(space_id=child.id, name="version", value="2.0")
        variable_repo.set_variable(var_child)

        resolved = inheritance_resolver.resolve_variable(child.id, "version")
        assert resolved is not None
        assert resolved.value == "2.0"
        assert resolved.is_local()

    def test_resolve_all_variables(
        self, inheritance_resolver, space_repo, variable_repo
    ):
        """Test resolving all variables including inherited."""
        parent = InformationSpace(name="parent")
        space_repo.create(parent)

        child = InformationSpace(name="child", parent_space_id=parent.id)
        space_repo.create(child)

        # Parent variables
        variable_repo.set_variable(
            SpaceVariable(space_id=parent.id, name="company", value="Acme")
        )
        variable_repo.set_variable(
            SpaceVariable(space_id=parent.id, name="version", value="1.0")
        )

        # Child variables (one override, one new)
        variable_repo.set_variable(
            SpaceVariable(space_id=child.id, name="version", value="2.0")
        )
        variable_repo.set_variable(
            SpaceVariable(space_id=child.id, name="author", value="John")
        )

        all_vars = inheritance_resolver.resolve_all_variables(child.id)
        assert len(all_vars) == 3
        assert all_vars["company"].value == "Acme"
        assert not all_vars["company"].is_local()
        assert all_vars["version"].value == "2.0"
        assert all_vars["version"].is_local()
        assert all_vars["author"].value == "John"

    def test_resolve_config_inheritance(self, inheritance_resolver, space_repo):
        """Test resolving config with inheritance."""
        parent = InformationSpace(
            name="parent",
            config=SpaceConfig(theme="dark", enable_caching=True),
        )
        space_repo.create(parent)

        child = InformationSpace(
            name="child",
            parent_space_id=parent.id,
            config=SpaceConfig(default_variant="flat"),
        )
        space_repo.create(child)

        config = inheritance_resolver.resolve_config(child.id)
        assert config.theme == "dark"  # inherited
        assert config.default_variant == "flat"  # local
        assert config.source_spaces["theme"] == parent.id


# ===========================================================================
# AccessControlService Tests
# ===========================================================================


class TestAccessControlService:
    """Tests for access control service."""

    def test_grant_permission(self, access_service, space_repo):
        """Test granting a permission."""
        space = InformationSpace(name="test-space")
        space_repo.create(space)

        perm = access_service.grant_permission(
            space_id=space.id,
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.WRITE,
        )
        assert perm.access_level == AccessLevel.WRITE

    def test_check_access(self, access_service, space_repo):
        """Test checking access."""
        space = InformationSpace(name="test-space")
        space_repo.create(space)

        access_service.grant_permission(
            space_id=space.id,
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.WRITE,
        )

        assert access_service.check_access(
            space.id, "user", "user-1", AccessLevel.READ
        )
        assert access_service.check_access(
            space.id, "user", "user-1", AccessLevel.WRITE
        )
        assert not access_service.check_access(
            space.id, "user", "user-1", AccessLevel.ADMIN
        )

    def test_revoke_permission(self, access_service, space_repo):
        """Test revoking access."""
        space = InformationSpace(name="test-space")
        space_repo.create(space)

        access_service.grant_permission(
            space_id=space.id,
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.READ,
        )

        assert access_service.revoke_permission(space.id, "user", "user-1")
        assert not access_service.check_access(
            space.id, "user", "user-1", AccessLevel.READ
        )

    def test_effective_access_direct(self, access_service, space_repo):
        """Test getting effective access with direct permission."""
        space = InformationSpace(name="test-space")
        space_repo.create(space)

        access_service.grant_permission(
            space_id=space.id,
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.ADMIN,
            role=SpaceRole.OWNER,
        )

        access = access_service.get_effective_access(space.id, "user", "user-1")
        assert access.effective_level == AccessLevel.ADMIN
        assert SpaceRole.OWNER in access.roles

    def test_effective_access_inherited(self, access_service, space_repo):
        """Test getting effective access with inherited permission."""
        parent = InformationSpace(name="parent")
        space_repo.create(parent)

        child = InformationSpace(name="child", parent_space_id=parent.id)
        space_repo.create(child)

        # Grant on parent only
        access_service.grant_permission(
            space_id=parent.id,
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.READ,
        )

        access = access_service.get_effective_access(child.id, "user", "user-1")
        assert access.effective_level == AccessLevel.READ
        assert parent.id in access.inherited_from


# ===========================================================================
# ComposableSpaceService Tests
# ===========================================================================


class TestComposableSpaceService:
    """Tests for composable space service."""

    def test_add_reference(self, composable_service, space_repo):
        """Test adding a space reference."""
        space1 = InformationSpace(name="space-1")
        space2 = InformationSpace(name="space-2")
        space_repo.create(space1)
        space_repo.create(space2)

        ref = composable_service.add_reference(
            source_space_id=space1.id,
            target_space_id=space2.id,
            reference_type=SpaceReferenceType.INCLUDES,
            alias="shared",
        )
        assert ref.alias == "shared"

    def test_add_reference_invalid_source(self, composable_service, space_repo):
        """Test adding reference with invalid source."""
        space = InformationSpace(name="valid-space")
        space_repo.create(space)

        with pytest.raises(ValueError, match="Source space.*not found"):
            composable_service.add_reference(
                source_space_id="invalid-id",
                target_space_id=space.id,
                reference_type=SpaceReferenceType.LINKS_TO,
            )

    def test_add_reference_self_reference(self, composable_service, space_repo):
        """Test that self-reference is rejected."""
        space = InformationSpace(name="test-space")
        space_repo.create(space)

        with pytest.raises(ValueError, match="Cannot reference self"):
            composable_service.add_reference(
                source_space_id=space.id,
                target_space_id=space.id,
                reference_type=SpaceReferenceType.INCLUDES,
            )

    def test_add_reference_circular_detection(self, composable_service, space_repo):
        """Test circular reference detection for EXTENDS type."""
        space1 = InformationSpace(name="space-1")
        space2 = InformationSpace(name="space-2")
        space3 = InformationSpace(name="space-3")
        space_repo.create(space1)
        space_repo.create(space2)
        space_repo.create(space3)

        # space1 -> space2 -> space3
        composable_service.add_reference(
            space1.id, space2.id, SpaceReferenceType.EXTENDS
        )
        composable_service.add_reference(
            space2.id, space3.id, SpaceReferenceType.EXTENDS
        )

        # space3 -> space1 would create a cycle
        with pytest.raises(CircularReferenceError):
            composable_service.add_reference(
                space3.id, space1.id, SpaceReferenceType.EXTENDS
            )

    def test_remove_reference(self, composable_service, space_repo):
        """Test removing a reference."""
        space1 = InformationSpace(name="space-1")
        space2 = InformationSpace(name="space-2")
        space_repo.create(space1)
        space_repo.create(space2)

        ref = composable_service.add_reference(
            space1.id, space2.id, SpaceReferenceType.LINKS_TO
        )
        assert composable_service.remove_reference(ref.id)

    def test_get_references_from_and_to(self, composable_service, space_repo):
        """Test getting references."""
        space1 = InformationSpace(name="space-1")
        space2 = InformationSpace(name="space-2")
        space3 = InformationSpace(name="space-3")
        space_repo.create(space1)
        space_repo.create(space2)
        space_repo.create(space3)

        composable_service.add_reference(
            space1.id, space2.id, SpaceReferenceType.INCLUDES
        )
        composable_service.add_reference(
            space1.id, space3.id, SpaceReferenceType.LINKS_TO
        )

        refs_from = composable_service.get_references_from(space1.id)
        assert len(refs_from) == 2

        refs_to = composable_service.get_references_to(space2.id)
        assert len(refs_to) == 1

    def test_get_composed_spaces(self, composable_service, space_repo):
        """Test getting all composed spaces."""
        parent = InformationSpace(name="parent")
        space_repo.create(parent)

        main = InformationSpace(name="main", parent_space_id=parent.id)
        space_repo.create(main)

        included = InformationSpace(name="included")
        extended = InformationSpace(name="extended")
        space_repo.create(included)
        space_repo.create(extended)

        composable_service.add_reference(
            main.id, included.id, SpaceReferenceType.INCLUDES
        )
        composable_service.add_reference(
            main.id, extended.id, SpaceReferenceType.EXTENDS
        )

        composed = composable_service.get_composed_spaces(main.id)
        assert len(composed) == 3  # parent, included, extended
        ids = {s.id for s in composed}
        assert parent.id in ids
        assert included.id in ids
        assert extended.id in ids

    def test_resolve_variable(self, composable_service, space_repo, variable_repo):
        """Test variable resolution via service."""
        parent = InformationSpace(name="parent")
        space_repo.create(parent)

        child = InformationSpace(name="child", parent_space_id=parent.id)
        space_repo.create(child)

        variable_repo.set_variable(
            SpaceVariable(space_id=parent.id, name="inherited_var", value="from parent")
        )

        resolved = composable_service.resolve_variable(child.id, "inherited_var")
        assert resolved is not None
        assert resolved.value == "from parent"

    def test_resolve_config(self, composable_service, space_repo):
        """Test config resolution via service."""
        space = InformationSpace(
            name="test-space", config=SpaceConfig(theme="minimal")
        )
        space_repo.create(space)

        config = composable_service.resolve_config(space.id)
        assert config.theme == "minimal"

    def test_grant_and_check_access(self, composable_service, space_repo):
        """Test access control via service."""
        space = InformationSpace(name="test-space")
        space_repo.create(space)

        composable_service.grant_access(
            space_id=space.id,
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.WRITE,
            role=SpaceRole.EDITOR,
        )

        assert composable_service.check_access(
            space.id, "user", "user-1", AccessLevel.WRITE
        )
        assert not composable_service.check_access(
            space.id, "user", "user-1", AccessLevel.ADMIN
        )

    def test_revoke_access(self, composable_service, space_repo):
        """Test revoking access via service."""
        space = InformationSpace(name="test-space")
        space_repo.create(space)

        composable_service.grant_access(
            space_id=space.id,
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.READ,
        )

        assert composable_service.revoke_access(space.id, "user", "user-1")
        assert not composable_service.check_access(
            space.id, "user", "user-1", AccessLevel.READ
        )

    def test_get_parent_chain(self, composable_service, space_repo):
        """Test getting parent chain via service."""
        grandparent = InformationSpace(name="grandparent")
        space_repo.create(grandparent)

        parent = InformationSpace(name="parent", parent_space_id=grandparent.id)
        space_repo.create(parent)

        child = InformationSpace(name="child", parent_space_id=parent.id)
        space_repo.create(child)

        chain = composable_service.get_parent_chain(child.id)
        assert len(chain) == 2
        assert chain[0].name == "parent"
        assert chain[1].name == "grandparent"

    def test_get_space_permissions(self, composable_service, space_repo):
        """Test getting space permissions."""
        space = InformationSpace(name="test-space")
        space_repo.create(space)

        composable_service.grant_access(
            space.id, "user", "user-1", AccessLevel.READ
        )
        composable_service.grant_access(
            space.id, "user", "user-2", AccessLevel.WRITE
        )

        perms = composable_service.get_space_permissions(space.id)
        assert len(perms) == 2


# ===========================================================================
# Edge Cases and Error Handling
# ===========================================================================


class TestEdgeCases:
    """Tests for edge cases and error handling."""

    def test_max_inheritance_depth(self, space_repo, variable_repo):
        """Test max inheritance depth is enforced."""
        resolver = InheritanceResolver(
            space_repo=space_repo,
            variable_repo=variable_repo,
            max_depth=3,
        )

        # Create a chain of 5 spaces
        spaces = []
        for i in range(5):
            parent_id = spaces[-1].id if spaces else None
            space = InformationSpace(name=f"space-{i}", parent_space_id=parent_id)
            space_repo.create(space)
            spaces.append(space)

        # Getting parent chain from the deepest should raise error
        with pytest.raises(CircularReferenceError, match="Maximum inheritance depth"):
            resolver.get_parent_chain(spaces[-1].id)

    def test_permission_expiration_handling(self, access_service, space_repo):
        """Test that expired permissions are not counted."""
        space = InformationSpace(name="test-space")
        space_repo.create(space)

        # Grant expired permission directly in repo
        expired_perm = SpacePermission(
            space_id=space.id,
            principal_type="user",
            principal_id="user-1",
            access_level=AccessLevel.ADMIN,
            expires_at=datetime.now() - timedelta(days=1),
        )
        access_service._permission_repo.grant_permission(expired_perm)

        # Should not have access
        access = access_service.get_effective_access(space.id, "user", "user-1")
        assert access.effective_level == AccessLevel.NONE

    def test_nonexistent_variable(self, inheritance_resolver, space_repo):
        """Test resolving nonexistent variable returns None."""
        space = InformationSpace(name="test-space")
        space_repo.create(space)

        resolved = inheritance_resolver.resolve_variable(space.id, "nonexistent")
        assert resolved is None

    def test_reference_type_filtering(self, composable_service, space_repo):
        """Test filtering references by type."""
        space1 = InformationSpace(name="space-1")
        space2 = InformationSpace(name="space-2")
        space3 = InformationSpace(name="space-3")
        space_repo.create(space1)
        space_repo.create(space2)
        space_repo.create(space3)

        composable_service.add_reference(
            space1.id, space2.id, SpaceReferenceType.INCLUDES
        )
        composable_service.add_reference(
            space1.id, space3.id, SpaceReferenceType.EXTENDS
        )

        # Filter by type
        includes = composable_service.get_references_from(
            space1.id, SpaceReferenceType.INCLUDES
        )
        assert len(includes) == 1
        assert includes[0].target_space_id == space2.id

        extends = composable_service.get_references_from(
            space1.id, SpaceReferenceType.EXTENDS
        )
        assert len(extends) == 1
        assert extends[0].target_space_id == space3.id
