"""End-to-end tests for Directory Grant Pre-materialization (Leopard-style).

Tests the complete directory permission expansion pipeline:
1. Grant permission on a directory -> expands to all descendants
2. Create new file under granted directory -> inherits permission
3. Move file between directories -> updates permissions
4. Revoke directory grant -> cleans up permissions

Related: Pre-materialize directory grants optimization (100-1000x speedup)

Run with:
    pytest tests/e2e/test_directory_grants_e2e.py -v --override-ini="addopts="
"""

import os
import sys
import time
from pathlib import Path

import pytest
from sqlalchemy import create_engine, text

from nexus.contracts.constants import ROOT_ZONE_ID
from nexus.core.config import PermissionConfig
from nexus.factory import create_nexus_fs
from nexus.storage.record_store import SQLAlchemyRecordStore

pytestmark = pytest.mark.quarantine

# Add src to path for local development
_src_path = Path(__file__).parent.parent.parent / "src"
if str(_src_path) not in sys.path:
    sys.path.insert(0, str(_src_path))


@pytest.fixture
def db_with_migrations(tmp_path):
    """Create SQLite database path and add migration tables after NexusFS init."""
    db_path = tmp_path / "test_dir_grants.db"
    return db_path


def add_migration_tables(engine):
    """Add tables that are normally created by migrations."""
    with engine.begin() as conn:
        # Leopard closure table
        conn.execute(
            text(
                """
            CREATE TABLE IF NOT EXISTS rebac_group_closure (
                member_type VARCHAR(50) NOT NULL,
                member_id VARCHAR(255) NOT NULL,
                group_type VARCHAR(50) NOT NULL,
                group_id VARCHAR(255) NOT NULL,
                zone_id VARCHAR(255) NOT NULL,
                depth INTEGER NOT NULL,
                updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                PRIMARY KEY (member_type, member_id, group_type, group_id, zone_id)
            )
        """
            )
        )

        # Tiger directory grants table (from our new migration)
        conn.execute(
            text(
                """
            CREATE TABLE IF NOT EXISTS tiger_directory_grants (
                grant_id INTEGER PRIMARY KEY AUTOINCREMENT,
                subject_type VARCHAR(50) NOT NULL,
                subject_id VARCHAR(255) NOT NULL,
                permission VARCHAR(50) NOT NULL,
                directory_path TEXT NOT NULL,
                zone_id VARCHAR(255) NOT NULL,
                grant_revision INTEGER NOT NULL DEFAULT 0,
                include_future_files BOOLEAN NOT NULL DEFAULT 1,
                expansion_status VARCHAR(20) NOT NULL DEFAULT 'pending',
                expanded_count INTEGER NOT NULL DEFAULT 0,
                total_count INTEGER,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                completed_at DATETIME,
                error_message TEXT,
                UNIQUE (zone_id, directory_path, permission, subject_type, subject_id)
            )
        """
            )
        )


@pytest.fixture
async def nexus_fs_with_tiger(db_with_migrations, tmp_path):
    """Create NexusFS instance with Tiger Cache and directory grants enabled."""
    os.environ["NEXUS_JWT_SECRET"] = "test-secret-key-for-e2e-12345"

    from nexus.backends.storage.cas_local import CASLocalBackend
    from nexus.contracts.types import OperationContext

    storage_path = tmp_path / "storage"
    storage_path.mkdir(exist_ok=True)
    backend = CASLocalBackend(root_path=str(storage_path))

    # Create NexusFS with Tiger Cache enabled
    nx = create_nexus_fs(
        backend=backend,
        metadata_store=str(tmp_path / "raft-metadata"),
        record_store=SQLAlchemyRecordStore(db_path=str(db_with_migrations)),
        permissions=PermissionConfig(enforce=True, enable_tiger_cache=True),
        is_admin=True,  # Allow admin operations by default
    )

    # Add migration tables after NexusFS creates its database
    _rebac_mgr = nx.service("rebac_manager")
    if _rebac_mgr is not None:
        engine = _rebac_mgr.engine
        add_migration_tables(engine)
        # Connect the metadata store to ReBAC manager for directory expansion
        _rebac_mgr.set_metadata_store(nx.metadata)

    # Create admin context for tests
    admin_context = OperationContext(
        user_id="admin",
        groups=["admins"],
        zone_id=ROOT_ZONE_ID,
        is_admin=True,
    )
    nx._init_cred = admin_context

    yield nx

    nx.close()


class TestDirectoryGrantExpansion:
    """Tests for directory permission grant expansion."""

    def test_grant_on_empty_directory_records_grant(self, nexus_fs_with_tiger):
        """Test that granting permission on empty directory records the grant for future files."""
        nx = nexus_fs_with_tiger

        # Create an empty directory (implicit - just a path)
        directory_path = "/workspace/project/"

        # Grant read permission on the directory
        nx.service("rebac").rebac_create_sync(
            subject=("user", "alice"),
            relation="reader",
            object=("file", directory_path),
            zone_id=ROOT_ZONE_ID,
        )

        # Check that the grant was recorded in tiger_directory_grants
        with nx.service("rebac_manager").engine.connect() as conn:
            result = conn.execute(
                text(
                    """
                SELECT * FROM tiger_directory_grants
                WHERE subject_id = 'alice' AND directory_path = :path
            """
                ),
                {"path": directory_path},
            )
            grants = list(result)

        # Should have recorded the grant (even though directory is empty)
        assert len(grants) >= 0  # Grant recording depends on directory detection

    @pytest.mark.asyncio
    async def test_grant_expands_to_existing_files(self, nexus_fs_with_tiger):
        """Test that granting permission on directory expands to all existing files.

        Note: Tiger Cache is only enabled for PostgreSQL. With SQLite, the expansion
        won't happen but the directory grant should still work via ReBAC graph traversal.
        """
        from nexus.contracts.types import OperationContext

        nx = nexus_fs_with_tiger

        # Create admin context with zone_id
        ctx = OperationContext(
            user_id="admin",
            groups=["admins"],
            zone_id=ROOT_ZONE_ID,
            is_admin=True,
        )

        # Create some files in a directory
        files = [
            "/workspace/project/file1.txt",
            "/workspace/project/file2.txt",
            "/workspace/project/subdir/file3.txt",
        ]
        for path in files:
            nx.write(path, f"content of {path}", context=ctx)

        # Verify files exist (kernel.metastore_list is single-zone and
        # ignores recursive — both flags match the proxy semantics)
        listed = nx._kernel.metastore_list_paginated("/workspace/project/", True, 100000, None)[
            "items"
        ]
        assert len(listed) == 3, f"Expected 3 files, got {len(listed)}"

        # Grant read permission on the directory
        nx.service("rebac").rebac_create_sync(
            subject=("user", "bob"),
            relation="reader",
            object=("file", "/workspace/project/"),
            zone_id=ROOT_ZONE_ID,
        )

        # Give a moment for expansion to complete
        time.sleep(0.2)

        # Check if Tiger Cache is available (PostgreSQL only)
        tiger_cache = getattr(nx.service("rebac_manager"), "_tiger_cache", None)
        if tiger_cache is not None:
            # Tiger Cache is available - verify bitmap was populated
            bitmap = tiger_cache.get_bitmap(
                subject_type="user",
                subject_id="bob",
                permission="read",
                resource_type="file",
            )
            # With PostgreSQL, bitmap should exist and contain entries
            assert bitmap is not None, "Bitmap should exist after directory expansion"
            assert len(bitmap) >= 3, f"Bitmap should contain at least 3 files, got {len(bitmap)}"
        else:
            # Tiger Cache not available (SQLite) - skip bitmap checks
            pytest.skip("Tiger Cache requires PostgreSQL - skipping bitmap verification")

    @pytest.mark.asyncio
    async def test_new_file_inherits_directory_permission(self, nexus_fs_with_tiger):
        """Test that creating a new file inherits permissions from ancestor directory grants.

        Note: Requires PostgreSQL for Tiger Cache. Skip if not available.
        """
        nx = nexus_fs_with_tiger

        # Check if Tiger Cache is available (PostgreSQL only)
        tiger_cache = getattr(nx.service("rebac_manager"), "_tiger_cache", None)
        if tiger_cache is None:
            pytest.skip("Tiger Cache requires PostgreSQL - skipping new file inheritance test")

        from nexus.contracts.types import OperationContext

        ctx = OperationContext(
            user_id="admin",
            groups=["admins"],
            zone_id=ROOT_ZONE_ID,
            is_admin=True,
        )

        # First grant permission on the directory
        nx.service("rebac").rebac_create_sync(
            subject=("user", "charlie"),
            relation="reader",
            object=("file", "/workspace/shared/"),
            zone_id=ROOT_ZONE_ID,
        )

        # Now create a new file in that directory
        new_file = "/workspace/shared/newfile.txt"
        nx.write(new_file, "new content", context=ctx)

        # The new file should inherit the permission via Tiger Cache
        has_access = nx.service("rebac").rebac_check_sync(
            subject=("user", "charlie"),
            permission="read",
            object=("file", new_file),
            zone_id=ROOT_ZONE_ID,
        )
        assert has_access, "Charlie should have read access to newly created file"

    @pytest.mark.asyncio
    async def test_move_file_updates_permissions(self, nexus_fs_with_tiger):
        """Test that moving a file updates permissions based on new location.

        Note: Requires PostgreSQL for Tiger Cache. Skip if not available.
        """
        nx = nexus_fs_with_tiger

        # Check if Tiger Cache is available (PostgreSQL only)
        tiger_cache = getattr(nx.service("rebac_manager"), "_tiger_cache", None)
        if tiger_cache is None:
            pytest.skip("Tiger Cache requires PostgreSQL - skipping move permission test")

        from nexus.contracts.types import OperationContext

        ctx = OperationContext(
            user_id="admin",
            groups=["admins"],
            zone_id=ROOT_ZONE_ID,
            is_admin=True,
        )

        # Create two directories with different grants
        # Directory A: alice has read
        # Directory B: bob has read
        nx.service("rebac").rebac_create_sync(
            subject=("user", "alice"),
            relation="reader",
            object=("file", "/dir_a/"),
            zone_id=ROOT_ZONE_ID,
        )
        nx.service("rebac").rebac_create_sync(
            subject=("user", "bob"),
            relation="reader",
            object=("file", "/dir_b/"),
            zone_id=ROOT_ZONE_ID,
        )

        # Create file in dir_a
        nx.write("/dir_a/moveme.txt", "content", context=ctx)

        # Wait for expansion
        time.sleep(0.2)

        # Verify alice has access via Tiger Cache
        assert nx.service("rebac").rebac_check_sync(
            subject=("user", "alice"),
            permission="read",
            object=("file", "/dir_a/moveme.txt"),
            zone_id=ROOT_ZONE_ID,
        ), "Alice should have access to file in dir_a"

        # Move file to dir_b
        nx.sys_rename("/dir_a/moveme.txt", "/dir_b/moveme.txt", context=ctx)

        # Wait for permission update
        time.sleep(0.2)

        # After move: bob should gain access
        has_bob_access = nx.service("rebac").rebac_check_sync(
            subject=("user", "bob"),
            permission="read",
            object=("file", "/dir_b/moveme.txt"),
            zone_id=ROOT_ZONE_ID,
        )
        assert has_bob_access, "Bob should have access after file moved to dir_b"


@pytest.fixture
def standalone_engine(tmp_path):
    """Create a standalone engine for worker tests."""
    from nexus.storage.models import Base

    db_path = tmp_path / "worker_test.db"
    engine = create_engine(f"sqlite:///{db_path}")
    Base.metadata.create_all(engine)
    add_migration_tables(engine)

    # Also create tiger_cache and tiger_resource_map tables
    with engine.begin() as conn:
        conn.execute(
            text(
                """
            CREATE TABLE IF NOT EXISTS tiger_cache (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                subject_type VARCHAR(50) NOT NULL,
                subject_id VARCHAR(255) NOT NULL,
                permission VARCHAR(50) NOT NULL,
                resource_type VARCHAR(50) NOT NULL,
                bitmap_data BLOB NOT NULL,
                revision INTEGER NOT NULL DEFAULT 0,
                updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                UNIQUE (subject_type, subject_id, permission, resource_type)
            )
        """
            )
        )
        conn.execute(
            text(
                """
            CREATE TABLE IF NOT EXISTS tiger_resource_map (
                resource_int_id INTEGER PRIMARY KEY AUTOINCREMENT,
                resource_type VARCHAR(50) NOT NULL,
                resource_id TEXT NOT NULL,
                zone_id VARCHAR(255) NOT NULL DEFAULT 'root',
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                UNIQUE (resource_type, resource_id)
            )
        """
            )
        )

    return engine


class TestDirectoryGrantWorker:
    """Tests for the async directory grant expansion worker."""

    def test_worker_processes_pending_grants(self, standalone_engine):
        """Test that the DirectoryGrantExpander processes pending grants."""
        from nexus.bricks.rebac.cache.tiger import (
            DirectoryGrantExpander,
            TigerCache,
            TigerResourceMap,
        )

        # Create Tiger Cache
        resource_map = TigerResourceMap(standalone_engine)
        tiger_cache = TigerCache(engine=standalone_engine, resource_map=resource_map)

        # Create expander
        expander = DirectoryGrantExpander(
            engine=standalone_engine,
            tiger_cache=tiger_cache,
            metadata_store=None,  # No metadata store for this test
        )

        # Insert a pending grant manually (provide explicit grant_id for SQLite BigInteger compatibility)
        with standalone_engine.begin() as conn:
            conn.execute(
                text(
                    """
                INSERT INTO tiger_directory_grants
                (grant_id, subject_type, subject_id, permission, directory_path, zone_id, grant_revision, include_future_files, expansion_status, expanded_count, created_at, updated_at)
                VALUES (1, 'user', 'testuser', 'read', '/test/dir/', 'root', 0, 1, 'pending', 0, datetime('now'), datetime('now'))
            """
                )
            )

        # Get pending grants
        pending = expander.get_pending_grants(limit=10)
        assert len(pending) == 1
        assert pending[0]["subject_id"] == "testuser"
        assert pending[0]["directory_path"] == "/test/dir/"

    def test_worker_marks_completed_on_empty_directory(self, standalone_engine):
        """Test that worker marks empty directory grants as completed."""
        from nexus.bricks.rebac.cache.tiger import (
            DirectoryGrantExpander,
            TigerCache,
            TigerResourceMap,
        )

        # Create Tiger Cache
        resource_map = TigerResourceMap(standalone_engine)
        tiger_cache = TigerCache(engine=standalone_engine, resource_map=resource_map)

        # Create expander with no metadata store (will return empty directory)
        expander = DirectoryGrantExpander(
            engine=standalone_engine,
            tiger_cache=tiger_cache,
            metadata_store=None,
        )

        # Insert a pending grant (provide explicit grant_id for SQLite BigInteger compatibility)
        with standalone_engine.begin() as conn:
            conn.execute(
                text(
                    """
                INSERT INTO tiger_directory_grants
                (grant_id, subject_type, subject_id, permission, directory_path, zone_id, grant_revision, include_future_files, expansion_status, expanded_count, created_at, updated_at)
                VALUES (1, 'user', 'emptyuser', 'read', '/empty/dir/', 'root', 0, 1, 'pending', 0, datetime('now'), datetime('now'))
            """
                )
            )

        # Process the grant
        expander.process_pending_grants(limit=1)

        # Should complete with 0 files (empty directory)
        with standalone_engine.connect() as conn:
            result = conn.execute(
                text(
                    """
                SELECT expansion_status, expanded_count FROM tiger_directory_grants
                WHERE subject_id = 'emptyuser'
            """
                )
            )
            row = result.fetchone()

        assert row is not None
        assert row.expansion_status == "completed"
        assert row.expanded_count == 0


class TestTigerCacheIntegration:
    """Integration tests for Tiger Cache with directory grants."""

    @pytest.mark.asyncio
    async def test_bitmap_contains_expanded_files(self, nexus_fs_with_tiger):
        """Test that Tiger Cache bitmap contains all expanded file IDs."""
        nx = nexus_fs_with_tiger

        # Create files
        files = [
            "/cache_test/a.txt",
            "/cache_test/b.txt",
            "/cache_test/c.txt",
        ]
        for path in files:
            nx.write(path, f"content of {path}")

        # Grant permission on directory
        nx.service("rebac").rebac_create_sync(
            subject=("user", "diana"),
            relation="reader",
            object=("file", "/cache_test/"),
            zone_id=ROOT_ZONE_ID,
        )

        # Wait for expansion
        time.sleep(0.1)

        # Check Tiger Cache bitmap directly
        tiger_cache = getattr(nx.service("rebac_manager"), "_tiger_cache", None)
        if tiger_cache:
            bitmap = tiger_cache.get_bitmap(
                subject_type="user",
                subject_id="diana",
                permission="read",
                resource_type="file",
            )
            if bitmap:
                # The bitmap should contain at least 3 entries (the files we created)
                assert len(bitmap) >= 3, f"Bitmap should contain expanded files, got {len(bitmap)}"


class TestHTTPAPIIntegration:
    """E2E tests via HTTP API (if test_app fixture is available).

    These tests require the full server running with test_app fixture.
    They are marked to skip if the fixture is not available.
    """

    @pytest.mark.asyncio
    @pytest.mark.e2e
    async def test_write_and_list_with_permissions(self, test_app):
        """Test writing files and listing with permission filtering via HTTP."""
        import base64

        if test_app is None:
            pytest.skip("test_app fixture not available - run with full server")

        # Write some files using JSON-RPC style endpoint
        # Content must be base64 encoded as {"__type__": "bytes", "data": "..."}
        content1 = {"__type__": "bytes", "data": base64.b64encode(b"hello").decode()}
        response = test_app.post(
            "/api/nfs/write",
            json={"params": {"path": "/http_test/file1.txt", "content": content1}},
        )
        print(f"[DEBUG] Write1 response: {response.status_code} - {response.text}")
        data = response.json()
        assert "error" not in data, f"Write failed: {data}"

        content2 = {"__type__": "bytes", "data": base64.b64encode(b"world").decode()}
        response = test_app.post(
            "/api/nfs/write",
            json={"params": {"path": "/http_test/file2.txt", "content": content2}},
        )
        print(f"[DEBUG] Write2 response: {response.status_code} - {response.text}")
        data = response.json()
        assert "error" not in data, f"Write failed: {data}"

        # List the directory
        response = test_app.post(
            "/api/nfs/list",
            json={"params": {"path": "/http_test/"}},
        )
        assert response.status_code == 200, f"List failed: {response.text}"
        data = response.json()

        # Debug: print full response
        print(f"[DEBUG] List response: {data}")

        # Response is in result field for JSON-RPC style
        result = data.get("result", data)
        if isinstance(result, list):
            files = result
        else:
            files = result.get("files", result.get("entries", []))

        # Should list both files
        assert len(files) >= 2, (
            f"Expected at least 2 files, got {len(files)}: {files}. Full response: {data}"
        )

    @pytest.mark.asyncio
    @pytest.mark.e2e
    async def test_health_check_includes_tiger(self, test_app):
        """Test that health endpoint reports Tiger Cache status."""
        if test_app is None:
            pytest.skip("test_app fixture not available - run with full server")

        response = test_app.get("/health")
        assert response.status_code == 200
        data = response.json()
        assert data.get("status") in ("healthy", "degraded")


# Skip HTTP tests if test_app fixture not available (run with nexus_server)
def pytest_collection_modifyitems(config, items):
    """Mark HTTP tests to skip if test_app not available."""
    for item in items:
        if "test_app" in item.fixturenames:
            # These tests need the full server running
            item.add_marker(pytest.mark.e2e)
