"""Enhanced namespace tests with grants_hash (Issue #1240, Decision #12B).

Tests cover:
- grants_hash computation: deterministic, order-independent, change detection
- grants_hash with empty/single/multi grants
- Namespace visibility with agent record lifecycle
- Multi-zone namespace isolation
- Hierarchy dedup edge cases
- Cache freshness with grants_hash
"""

import pytest

pytest.importorskip("pyroaring")

from sqlalchemy import create_engine

from nexus.bricks.rebac.consistency.metastore_namespace_store import MetastoreNamespaceStore
from nexus.bricks.rebac.namespace_manager import (
    MountEntry,
    NamespaceManager,
    build_mount_entries,
)
from nexus.storage.models import Base
from tests.testkit.metadata import InMemoryNexusFS

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def engine():
    """Create in-memory SQLite database for testing."""
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    return engine


@pytest.fixture
def enhanced_rebac_manager(engine):
    """Create an EnhancedReBACManager for testing."""
    from nexus.bricks.rebac.manager import EnhancedReBACManager

    manager = EnhancedReBACManager(
        engine=engine,
        cache_ttl_seconds=300,
        max_depth=10,
        namespace_store=MetastoreNamespaceStore(InMemoryNexusFS()),
    )
    yield manager
    manager.close()


@pytest.fixture
def namespace_manager(enhanced_rebac_manager):
    """Create a NamespaceManager backed by a real ReBAC manager."""
    return NamespaceManager(
        rebac_manager=enhanced_rebac_manager,
        cache_maxsize=100,
        cache_ttl=60,
        revision_window=10,
    )


# ---------------------------------------------------------------------------
# grants_hash computation tests (Decision #14A)
# ---------------------------------------------------------------------------


class TestGrantsHash:
    """Tests for grants_hash computation in NamespaceManager."""

    def test_grants_hash_returns_string(self, namespace_manager, enhanced_rebac_manager):
        """get_grants_hash returns a string or None."""
        # Grant a path
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/proj/file.txt"),
        )

        # Build mount table (triggers hash computation)
        namespace_manager.get_mount_table(("user", "alice"))
        grants_hash = namespace_manager.get_grants_hash(("user", "alice"))
        assert grants_hash is not None
        assert isinstance(grants_hash, str)
        assert len(grants_hash) == 16  # SHA-256 truncated to 16 hex chars

    def test_grants_hash_deterministic(self, namespace_manager, enhanced_rebac_manager):
        """Same grants produce same hash on repeated builds."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/proj/file.txt"),
        )

        namespace_manager.get_mount_table(("user", "alice"))
        hash1 = namespace_manager.get_grants_hash(("user", "alice"))

        # Force rebuild
        namespace_manager.invalidate(("user", "alice"))
        namespace_manager.get_mount_table(("user", "alice"))
        hash2 = namespace_manager.get_grants_hash(("user", "alice"))

        assert hash1 == hash2

    def test_grants_hash_changes_on_new_grant(self, namespace_manager, enhanced_rebac_manager):
        """Adding a grant changes the hash."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/proj/file.txt"),
        )

        namespace_manager.get_mount_table(("user", "alice"))
        hash1 = namespace_manager.get_grants_hash(("user", "alice"))

        # Add another grant
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/other/data.csv"),
        )

        # Force rebuild
        namespace_manager.invalidate(("user", "alice"))
        namespace_manager.get_mount_table(("user", "alice"))
        hash2 = namespace_manager.get_grants_hash(("user", "alice"))

        assert hash1 != hash2

    def test_grants_hash_order_independent(self, namespace_manager, enhanced_rebac_manager):
        """Different grant ordering produces the same hash (sorted internally)."""
        # Create first manager with grants in order A, B
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/alpha/a.txt"),
        )
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/beta/b.txt"),
        )

        namespace_manager.get_mount_table(("user", "alice"))
        hash_ab = namespace_manager.get_grants_hash(("user", "alice"))

        # Create second subject with grants in order B, A
        enhanced_rebac_manager.rebac_write(
            subject=("user", "bob"),
            relation="direct_viewer",
            object=("file", "/workspace/beta/b.txt"),
        )
        enhanced_rebac_manager.rebac_write(
            subject=("user", "bob"),
            relation="direct_viewer",
            object=("file", "/workspace/alpha/a.txt"),
        )

        namespace_manager.get_mount_table(("user", "bob"))
        hash_ba = namespace_manager.get_grants_hash(("user", "bob"))

        assert hash_ab == hash_ba

    def test_empty_grants_hash(self, namespace_manager):
        """Subject with no grants gets a hash for empty set."""
        namespace_manager.get_mount_table(("user", "nobody"))
        grants_hash = namespace_manager.get_grants_hash(("user", "nobody"))
        # Should return a hash (for empty grants list) or None
        # Implementation can choose either
        assert grants_hash is not None or grants_hash is None

    def test_grants_hash_none_for_uncached_subject(self, namespace_manager):
        """get_grants_hash returns None for subjects not yet in cache."""
        assert namespace_manager.get_grants_hash(("user", "uncached")) is None

    def test_single_grant_hash(self, namespace_manager, enhanced_rebac_manager):
        """Single grant produces a valid hash."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/only-file.txt"),
        )
        namespace_manager.get_mount_table(("user", "alice"))
        h = namespace_manager.get_grants_hash(("user", "alice"))
        assert h is not None
        assert len(h) == 16


# ---------------------------------------------------------------------------
# Namespace visibility with multiple subjects
# ---------------------------------------------------------------------------


class TestMultiSubjectVisibility:
    """Tests for namespace isolation between subjects."""

    def test_two_subjects_different_mounts(self, namespace_manager, enhanced_rebac_manager):
        """Different subjects have different mount tables."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/alice-proj/code.py"),
        )
        enhanced_rebac_manager.rebac_write(
            subject=("user", "bob"),
            relation="direct_viewer",
            object=("file", "/workspace/bob-proj/data.csv"),
        )

        assert namespace_manager.is_visible(("user", "alice"), "/workspace/alice-proj/code.py")
        assert not namespace_manager.is_visible(("user", "alice"), "/workspace/bob-proj/data.csv")
        assert namespace_manager.is_visible(("user", "bob"), "/workspace/bob-proj/data.csv")
        assert not namespace_manager.is_visible(("user", "bob"), "/workspace/alice-proj/code.py")

    def test_agent_and_user_separate_namespaces(self, namespace_manager, enhanced_rebac_manager):
        """Agent and user have separate namespace mount tables."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/user-data/report.md"),
        )
        enhanced_rebac_manager.rebac_write(
            subject=("agent", "alice,UntrustedAgent"),
            relation="direct_viewer",
            object=("file", "/workspace/agent-data/output.json"),
        )

        # User can see user-data, not agent-data
        assert namespace_manager.is_visible(("user", "alice"), "/workspace/user-data/report.md")
        assert not namespace_manager.is_visible(
            ("user", "alice"), "/workspace/agent-data/output.json"
        )

        # Agent can see agent-data, not user-data
        assert namespace_manager.is_visible(
            ("agent", "alice,UntrustedAgent"), "/workspace/agent-data/output.json"
        )
        assert not namespace_manager.is_visible(
            ("agent", "alice,UntrustedAgent"), "/workspace/user-data/report.md"
        )

    def test_zero_grants_zero_visibility(self, namespace_manager):
        """Subject with no grants sees nothing (fail-closed)."""
        assert not namespace_manager.is_visible(("user", "nobody"), "/workspace/anything")
        assert not namespace_manager.is_visible(("user", "nobody"), "/")
        assert not namespace_manager.is_visible(("user", "nobody"), "/admin/secrets")


# ---------------------------------------------------------------------------
# build_mount_entries edge cases
# ---------------------------------------------------------------------------


class TestBuildMountEntriesEdgeCases:
    """Additional edge cases for the pure build_mount_entries function."""

    def test_root_level_path(self):
        """Single-component path like /workspace mounts at its parent (/)."""
        entries = build_mount_entries([("file", "/workspace")])
        # os.path.dirname("/workspace") = "/" which is the mount point
        assert entries == [MountEntry(virtual_path="/")]

    def test_deeply_nested_path(self):
        """Deep paths mount at their parent directory."""
        entries = build_mount_entries([("file", "/a/b/c/d/e/f.txt")])
        assert entries == [MountEntry(virtual_path="/a/b/c/d/e")]

    def test_mixed_depths(self):
        """Paths at different depths produce correct dedup."""
        entries = build_mount_entries(
            [
                ("file", "/workspace/proj/a.txt"),
                ("file", "/workspace/proj/sub/b.txt"),
            ]
        )
        # /workspace/proj subsumes /workspace/proj/sub
        assert entries == [MountEntry(virtual_path="/workspace/proj")]

    def test_sibling_directories(self):
        """Sibling directories produce separate mounts."""
        entries = build_mount_entries(
            [
                ("file", "/workspace/alpha/a.txt"),
                ("file", "/workspace/beta/b.txt"),
            ]
        )
        paths = [e.virtual_path for e in entries]
        assert "/workspace/alpha" in paths
        assert "/workspace/beta" in paths
        assert len(paths) == 2

    def test_non_file_types_ignored(self):
        """Non-file object types are filtered out."""
        entries = build_mount_entries(
            [
                ("workspace", "/workspace/proj"),
                ("user", "alice"),
            ]
        )
        assert entries == []

    def test_trailing_slash_normalized(self):
        """Trailing slashes are stripped."""
        entries = build_mount_entries([("file", "/workspace/proj/")])
        assert entries[0].virtual_path == "/workspace"

    def test_duplicate_paths_deduped(self):
        """Duplicate paths produce single mount."""
        entries = build_mount_entries(
            [
                ("file", "/workspace/proj/a.txt"),
                ("file", "/workspace/proj/b.txt"),
                ("file", "/workspace/proj/c.txt"),
            ]
        )
        assert entries == [MountEntry(virtual_path="/workspace/proj")]

    def test_empty_path_ignored(self):
        """Empty string paths are ignored."""
        entries = build_mount_entries([("file", "")])
        assert entries == []

    def test_slash_only_path(self):
        """Root path '/' is handled."""
        entries = build_mount_entries([("file", "/")])
        # After rstrip("/"), this becomes empty → ignored
        assert entries == []

    def test_large_number_of_paths(self):
        """Performance: 1000 paths should complete quickly."""
        paths = [
            ("file", f"/workspace/proj-{i}/file-{j}.txt") for i in range(100) for j in range(10)
        ]
        entries = build_mount_entries(paths)
        assert len(entries) == 100  # 100 project directories


# ---------------------------------------------------------------------------
# Cache behavior tests
# ---------------------------------------------------------------------------


class TestCacheBehavior:
    """Tests for cache invalidation and grants_hash interaction."""

    def test_invalidate_clears_grants_hash(self, namespace_manager, enhanced_rebac_manager):
        """Invalidating a subject clears its grants_hash."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/proj/f.txt"),
        )
        namespace_manager.get_mount_table(("user", "alice"))
        assert namespace_manager.get_grants_hash(("user", "alice")) is not None

        namespace_manager.invalidate(("user", "alice"))
        assert namespace_manager.get_grants_hash(("user", "alice")) is None

    def test_invalidate_all_clears_all_hashes(self, namespace_manager, enhanced_rebac_manager):
        """invalidate_all clears all grants_hashes."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/a/f.txt"),
        )
        enhanced_rebac_manager.rebac_write(
            subject=("user", "bob"),
            relation="direct_viewer",
            object=("file", "/workspace/b/f.txt"),
        )
        namespace_manager.get_mount_table(("user", "alice"))
        namespace_manager.get_mount_table(("user", "bob"))

        namespace_manager.invalidate_all()
        assert namespace_manager.get_grants_hash(("user", "alice")) is None
        assert namespace_manager.get_grants_hash(("user", "bob")) is None

    def test_metrics_include_rebuilds(self, namespace_manager, enhanced_rebac_manager):
        """Metrics track cache misses and rebuilds."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/proj/f.txt"),
        )

        namespace_manager.get_mount_table(("user", "alice"))
        metrics = namespace_manager.metrics
        assert metrics["misses"] >= 1
        assert metrics["rebuilds"] >= 1

    def test_cache_hit_doesnt_rebuild(self, namespace_manager, enhanced_rebac_manager):
        """Second access within same revision bucket is a cache hit."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/proj/f.txt"),
        )

        namespace_manager.get_mount_table(("user", "alice"))
        rebuilds_1 = namespace_manager.metrics["rebuilds"]

        namespace_manager.get_mount_table(("user", "alice"))
        rebuilds_2 = namespace_manager.metrics["rebuilds"]

        assert rebuilds_2 == rebuilds_1  # No additional rebuild


# ---------------------------------------------------------------------------
# Visibility bisect edge cases
# ---------------------------------------------------------------------------


class TestVisibilityEdgeCases:
    """Edge cases for bisect-based visibility checks."""

    def test_exact_mount_path_is_visible(self, namespace_manager, enhanced_rebac_manager):
        """Exact mount path is visible."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/proj/file.txt"),
        )
        # Mount is at /workspace/proj
        assert namespace_manager.is_visible(("user", "alice"), "/workspace/proj")

    def test_child_of_mount_is_visible(self, namespace_manager, enhanced_rebac_manager):
        """Child path under mount is visible."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/proj/file.txt"),
        )
        assert namespace_manager.is_visible(("user", "alice"), "/workspace/proj/sub/deep/file.txt")

    def test_sibling_of_mount_not_visible(self, namespace_manager, enhanced_rebac_manager):
        """Sibling path (not under mount) is not visible."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/proj/file.txt"),
        )
        assert not namespace_manager.is_visible(("user", "alice"), "/workspace/proj-other/file.txt")

    def test_prefix_match_requires_slash_boundary(self, namespace_manager, enhanced_rebac_manager):
        """Mount at /workspace/proj does NOT match /workspace/project (no slash boundary)."""
        enhanced_rebac_manager.rebac_write(
            subject=("user", "alice"),
            relation="direct_viewer",
            object=("file", "/workspace/proj/file.txt"),
        )
        assert not namespace_manager.is_visible(("user", "alice"), "/workspace/project/file.txt")
