"""
Tests for multi-user document isolation and feature flag.

Covers:
- user_scope utilities (get_current_owner_id, apply_owner_filter)
- FileRecord.owner_id model field
- API file list/detail/delete scoping in multi-user mode
- Upload endpoint owner_id propagation
- Feature flag toggling (single-user vs multi-user mode)
"""

from unittest.mock import MagicMock, patch

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

from app.config import settings
from app.database import Base
from app.models import ApiToken, FileRecord

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


@pytest.fixture()
def mu_engine():
    """In-memory SQLite engine for multi-user tests."""
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(bind=engine)
    yield engine
    Base.metadata.drop_all(bind=engine)


@pytest.fixture()
def mu_session(mu_engine):
    """Session scoped to a single test function."""
    Session = sessionmaker(bind=mu_engine)
    session = Session()
    yield session
    session.close()


def _create_file_record(session, owner_id=None, filename="test.pdf"):
    """Helper to insert a minimal FileRecord."""
    rec = FileRecord(
        filehash="abc123",
        original_filename=filename,
        local_filename="/tmp/test.pdf",
        file_size=1024,
        mime_type="application/pdf",
        is_duplicate=False,
        owner_id=owner_id,
    )
    session.add(rec)
    session.commit()
    session.refresh(rec)
    return rec


def _mock_request(user=None):
    """Create a mock request with the given session user."""
    request = MagicMock()
    request.session = {"user": user} if user else {}
    return request


def _patch_multi_user(enabled):
    """Return a patch context manager for multi_user_enabled."""
    return patch.object(settings, "multi_user_enabled", enabled)


# ---------------------------------------------------------------------------
# Model tests
# ---------------------------------------------------------------------------


class TestFileRecordOwnerField:
    """Verify the owner_id column on FileRecord."""

    @pytest.mark.unit
    def test_owner_id_defaults_to_none(self, mu_session):
        """FileRecord created without owner_id should have None."""
        rec = _create_file_record(mu_session)
        assert rec.owner_id is None

    @pytest.mark.unit
    def test_owner_id_stores_value(self, mu_session):
        """FileRecord created with owner_id should persist it."""
        rec = _create_file_record(mu_session, owner_id="user@example.com")
        assert rec.owner_id == "user@example.com"

    @pytest.mark.unit
    def test_owner_id_filterable(self, mu_session):
        """Can query FileRecord by owner_id."""
        _create_file_record(mu_session, owner_id="alice")
        _create_file_record(mu_session, owner_id="bob")
        _create_file_record(mu_session, owner_id=None)

        alice_files = mu_session.query(FileRecord).filter(FileRecord.owner_id == "alice").all()
        assert len(alice_files) == 1
        assert alice_files[0].owner_id == "alice"

        global_files = mu_session.query(FileRecord).filter(FileRecord.owner_id.is_(None)).all()
        assert len(global_files) == 1


# ---------------------------------------------------------------------------
# user_scope utility tests
# ---------------------------------------------------------------------------


class TestGetCurrentOwnerId:
    """Tests for get_current_owner_id()."""

    @pytest.mark.unit
    def test_returns_none_when_no_user(self):
        from app.utils.user_scope import get_current_owner_id

        request = _mock_request(user=None)
        assert get_current_owner_id(request) is None

    @pytest.mark.unit
    def test_prefers_sub_claim(self):
        from app.utils.user_scope import get_current_owner_id

        request = _mock_request(user={"sub": "sub-123", "preferred_username": "alice", "email": "a@b.com"})
        assert get_current_owner_id(request) == "sub-123"

    @pytest.mark.unit
    def test_falls_back_to_preferred_username(self):
        from app.utils.user_scope import get_current_owner_id

        request = _mock_request(user={"preferred_username": "alice", "email": "a@b.com"})
        assert get_current_owner_id(request) == "alice"

    @pytest.mark.unit
    def test_falls_back_to_email(self):
        from app.utils.user_scope import get_current_owner_id

        request = _mock_request(user={"email": "a@b.com"})
        assert get_current_owner_id(request) == "a@b.com"

    @pytest.mark.unit
    def test_falls_back_to_id(self):
        from app.utils.user_scope import get_current_owner_id

        request = _mock_request(user={"id": "admin"})
        assert get_current_owner_id(request) == "admin"

    @pytest.mark.unit
    def test_returns_none_for_empty_session(self):
        from app.utils.user_scope import get_current_owner_id

        request = MagicMock()
        request.session = {}
        assert get_current_owner_id(request) is None

    @pytest.mark.unit
    def test_resolves_from_api_token_user_state(self):
        """get_current_owner_id should resolve from request.state.api_token_user."""
        from app.utils.user_scope import get_current_owner_id

        request = MagicMock()
        request.session = {}
        request.state.api_token_user = {
            "id": "tok-owner",
            "preferred_username": "tok-owner",
            "email": "tok-owner",
        }
        assert get_current_owner_id(request) == "tok-owner"

    @pytest.mark.unit
    def test_session_takes_precedence_over_api_token_user(self):
        """Session auth should take precedence over api_token_user in state."""
        from app.utils.user_scope import get_current_owner_id

        request = MagicMock()
        request.session = {"user": {"sub": "session-sub", "email": "session@example.com"}}
        request.state.api_token_user = {"id": "tok-owner"}
        assert get_current_owner_id(request) == "session-sub"

    @pytest.mark.unit
    def test_resolves_bearer_token_directly(self, mu_engine, mu_session):
        """get_current_owner_id should resolve a Bearer token when no session exists."""
        from types import SimpleNamespace

        from app.api.api_tokens import generate_api_token, hash_token
        from app.utils.user_scope import get_current_owner_id

        # Create a token in the DB
        plaintext = generate_api_token()
        token_hash = hash_token(plaintext)
        db_token = ApiToken(
            owner_id="bearer-owner",
            name="Test Bearer",
            token_hash=token_hash,
            token_prefix=plaintext[:12],
            is_active=True,
        )
        mu_session.add(db_token)
        mu_session.commit()

        # Build a mock request with Bearer header but no session.
        # SimpleNamespace starts with no attributes so getattr(..., None) works.
        request = MagicMock()
        request.session = {}
        request.state = SimpleNamespace()
        request.headers = {"authorization": f"Bearer {plaintext}"}
        request.client.host = "127.0.0.1"

        # Provide the test session and make close() a no-op so the shared
        # session is not torn down prematurely.
        noop_close = MagicMock()
        with patch("app.database.SessionLocal", return_value=mu_session), patch.object(mu_session, "close", noop_close):
            result = get_current_owner_id(request)

        assert result == "bearer-owner"
        # Verify the resolved user was cached in request.state
        assert request.state.api_token_user["id"] == "bearer-owner"

    @pytest.mark.unit
    def test_returns_none_for_invalid_bearer_token(self, mu_engine, mu_session):
        """get_current_owner_id should return None for an invalid Bearer token."""
        from types import SimpleNamespace

        from app.utils.user_scope import get_current_owner_id

        request = MagicMock()
        request.session = {}
        request.state = SimpleNamespace()
        request.headers = {"authorization": "Bearer de_invalid_token_value"}
        request.client.host = "127.0.0.1"

        noop_close = MagicMock()
        with patch("app.database.SessionLocal", return_value=mu_session), patch.object(mu_session, "close", noop_close):
            result = get_current_owner_id(request)

        assert result is None


class TestOwnerIdFromUser:
    """Tests for the _owner_id_from_user helper."""

    @pytest.mark.unit
    def test_prefers_sub(self):
        from app.utils.user_scope import _owner_id_from_user

        assert _owner_id_from_user({"sub": "s", "preferred_username": "u", "email": "e"}) == "s"

    @pytest.mark.unit
    def test_falls_back_to_preferred_username(self):
        from app.utils.user_scope import _owner_id_from_user

        assert _owner_id_from_user({"preferred_username": "u", "email": "e"}) == "u"

    @pytest.mark.unit
    def test_falls_back_to_email(self):
        from app.utils.user_scope import _owner_id_from_user

        assert _owner_id_from_user({"email": "e"}) == "e"

    @pytest.mark.unit
    def test_falls_back_to_id(self):
        from app.utils.user_scope import _owner_id_from_user

        assert _owner_id_from_user({"id": "i"}) == "i"

    @pytest.mark.unit
    def test_returns_none_for_empty_dict(self):
        from app.utils.user_scope import _owner_id_from_user

        assert _owner_id_from_user({}) is None


class TestApplyOwnerFilter:
    """Tests for apply_owner_filter()."""

    @pytest.mark.unit
    def test_no_filter_when_disabled(self, mu_session):
        """When multi_user_enabled=False, all files are returned."""
        from app.utils.user_scope import apply_owner_filter

        _create_file_record(mu_session, owner_id="alice")
        _create_file_record(mu_session, owner_id="bob")
        _create_file_record(mu_session, owner_id=None)

        request = _mock_request(user={"preferred_username": "alice"})
        query = mu_session.query(FileRecord)

        with _patch_multi_user(False):
            filtered = apply_owner_filter(query, request)

        assert filtered.count() == 3

    @pytest.mark.unit
    def test_filters_by_owner_when_enabled(self, mu_session):
        """When multi_user_enabled=True with unowned_docs_visible, user sees own + unowned files."""
        from app.utils.user_scope import apply_owner_filter

        _create_file_record(mu_session, owner_id="alice")
        _create_file_record(mu_session, owner_id="bob")
        _create_file_record(mu_session, owner_id=None)

        request = _mock_request(user={"preferred_username": "alice"})
        query = mu_session.query(FileRecord)

        with _patch_multi_user(True), patch.object(settings, "unowned_docs_visible_to_all", True):
            filtered = apply_owner_filter(query, request)

        results = filtered.all()
        # Alice sees her own file + the unowned file (not Bob's)
        assert len(results) == 2
        owner_ids = {r.owner_id for r in results}
        assert owner_ids == {"alice", None}

    @pytest.mark.unit
    def test_filters_strictly_when_unowned_not_visible(self, mu_session):
        """When unowned_docs_visible_to_all=False, user sees only own files."""
        from app.utils.user_scope import apply_owner_filter

        _create_file_record(mu_session, owner_id="alice")
        _create_file_record(mu_session, owner_id="bob")
        _create_file_record(mu_session, owner_id=None)

        request = _mock_request(user={"preferred_username": "alice"})
        query = mu_session.query(FileRecord)

        with _patch_multi_user(True), patch.object(settings, "unowned_docs_visible_to_all", False):
            filtered = apply_owner_filter(query, request)

        results = filtered.all()
        assert len(results) == 1
        assert results[0].owner_id == "alice"

    @pytest.mark.unit
    def test_admin_sees_all_when_enabled(self, mu_session):
        """Admin users bypass the owner filter in multi-user mode."""
        from app.utils.user_scope import apply_owner_filter

        _create_file_record(mu_session, owner_id="alice")
        _create_file_record(mu_session, owner_id="bob")
        _create_file_record(mu_session, owner_id=None)

        request = _mock_request(user={"preferred_username": "admin", "is_admin": True})
        query = mu_session.query(FileRecord)

        with _patch_multi_user(True):
            filtered = apply_owner_filter(query, request)

        assert filtered.count() == 3

    @pytest.mark.unit
    def test_unauthenticated_sees_nothing_when_enabled(self, mu_session):
        """When no user is logged in and multi-user is enabled, return empty."""
        from app.utils.user_scope import apply_owner_filter

        _create_file_record(mu_session, owner_id="alice")

        request = _mock_request(user=None)
        query = mu_session.query(FileRecord)

        with _patch_multi_user(True):
            filtered = apply_owner_filter(query, request)

        assert filtered.count() == 0


# ---------------------------------------------------------------------------
# Config / feature flag tests
# ---------------------------------------------------------------------------


class TestMultiUserConfig:
    """Verify the multi-user configuration settings."""

    @pytest.mark.unit
    def test_multi_user_default_disabled(self):
        """multi_user_enabled should default to False."""
        from app.config import settings

        # Default is False (overridable via env)
        assert hasattr(settings, "multi_user_enabled")

    @pytest.mark.unit
    def test_default_daily_upload_limit_exists(self):
        """default_daily_upload_limit should exist on settings."""
        from app.config import settings

        assert hasattr(settings, "default_daily_upload_limit")

    @pytest.mark.unit
    def test_multi_user_setting_has_metadata(self):
        """multi_user_enabled must be in SETTING_METADATA."""
        from app.utils.settings_service import SETTING_METADATA

        assert "multi_user_enabled" in SETTING_METADATA
        meta = SETTING_METADATA["multi_user_enabled"]
        assert meta["type"] == "boolean"
        assert meta["category"] == "Authentication"

    @pytest.mark.unit
    def test_daily_upload_limit_setting_has_metadata(self):
        """default_daily_upload_limit must be in SETTING_METADATA."""
        from app.utils.settings_service import SETTING_METADATA

        assert "default_daily_upload_limit" in SETTING_METADATA
        meta = SETTING_METADATA["default_daily_upload_limit"]
        assert meta["type"] == "integer"


# ---------------------------------------------------------------------------
# Migration tests
# ---------------------------------------------------------------------------


class TestMigration012:
    """Verify the multi-user migration file exists and is well-formed."""

    @pytest.mark.unit
    def test_migration_file_exists(self):
        """Migration 012 should exist."""
        from pathlib import Path

        migration = Path("migrations/versions/012_add_multi_user_support.py")
        assert migration.exists()

    @pytest.mark.unit
    def test_migration_chain(self):
        """Migration 012 should chain from 011."""
        import importlib.util

        spec = importlib.util.spec_from_file_location(
            "migration_012", "migrations/versions/012_add_multi_user_support.py"
        )
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)
        assert mod.down_revision == "011_add_pdfa_paths"
        assert mod.revision == "012_add_multi_user_support"


# ---------------------------------------------------------------------------
# API integration tests (list files with owner scoping)
# ---------------------------------------------------------------------------


class TestFilesAPIMultiUser:
    """Verify that the files API respects multi-user scoping."""

    @pytest.mark.integration
    def test_list_files_unscoped_single_user(self, client, db_session):
        """In single-user mode all files are visible."""
        _create_file_record(db_session, owner_id="alice", filename="a.pdf")
        _create_file_record(db_session, owner_id="bob", filename="b.pdf")

        with _patch_multi_user(False):
            response = client.get("/api/files")

        assert response.status_code == 200
        data = response.json()
        assert data["pagination"]["total"] == 2

    @pytest.mark.integration
    def test_list_files_scoped_multi_user(self, client, db_session):
        """In multi-user mode only the user's files should be returned."""
        _create_file_record(db_session, owner_id="alice", filename="a.pdf")
        _create_file_record(db_session, owner_id="bob", filename="b.pdf")

        with _patch_multi_user(True):
            # Without a real session, the filter will return no results
            # (unauthenticated user sees nothing in multi-user mode)
            response = client.get("/api/files")
            assert response.status_code == 200
            data = response.json()
            # No session user means empty results
            assert data["pagination"]["total"] == 0

    @pytest.mark.integration
    def test_get_file_detail_respects_scope(self, client, db_session):
        """File detail endpoint should return 404 for files owned by other users."""
        rec = _create_file_record(db_session, owner_id="alice", filename="a.pdf")

        with _patch_multi_user(True):
            response = client.get(f"/api/files/{rec.id}")
            # Without session, user is unauthenticated → 404
            assert response.status_code == 404

    @pytest.mark.integration
    def test_get_file_detail_single_user_mode(self, client, db_session):
        """File detail endpoint should work normally in single-user mode."""
        rec = _create_file_record(db_session, owner_id="alice", filename="a.pdf")

        with _patch_multi_user(False):
            response = client.get(f"/api/files/{rec.id}")
            assert response.status_code == 200
            data = response.json()
            assert data["file"]["id"] == rec.id


# ---------------------------------------------------------------------------
# process_document owner_id parameter tests
# ---------------------------------------------------------------------------


class TestProcessDocumentOwnerId:
    """Verify process_document task accepts owner_id."""

    @pytest.mark.unit
    def test_process_document_signature_accepts_owner_id(self):
        """process_document should accept owner_id as a keyword argument."""
        import inspect

        from app.tasks.process_document import process_document

        sig = inspect.signature(process_document)
        assert "owner_id" in sig.parameters
        assert sig.parameters["owner_id"].default is None

    @pytest.mark.unit
    def test_convert_to_pdf_signature_accepts_owner_id(self):
        """convert_to_pdf should accept owner_id as a keyword argument."""
        import inspect

        from app.tasks.convert_to_pdf import convert_to_pdf

        sig = inspect.signature(convert_to_pdf)
        assert "owner_id" in sig.parameters
        assert sig.parameters["owner_id"].default is None

    @pytest.mark.unit
    def test_default_owner_id_fallback_in_process_document(self):
        """When owner_id is None and default_owner_id is set, fallback is used."""
        # Verify the fallback logic exists in the source code
        import ast
        from pathlib import Path

        src = Path("app/tasks/process_document.py").read_text()
        tree = ast.parse(src)
        # Check that 'default_owner_id' string appears in the source
        assert "default_owner_id" in src, "default_owner_id fallback not found in process_document.py"


# ---------------------------------------------------------------------------
# New config settings tests
# ---------------------------------------------------------------------------


class TestUnownedDocsConfig:
    """Verify the new multi-user configuration settings."""

    @pytest.mark.unit
    def test_unowned_docs_visible_default_true(self):
        """unowned_docs_visible_to_all should default to True."""
        assert hasattr(settings, "unowned_docs_visible_to_all")

    @pytest.mark.unit
    def test_default_owner_id_default_none(self):
        """default_owner_id should default to None."""
        assert hasattr(settings, "default_owner_id")

    @pytest.mark.unit
    def test_unowned_docs_has_metadata(self):
        """unowned_docs_visible_to_all must be in SETTING_METADATA."""
        from app.utils.settings_service import SETTING_METADATA

        assert "unowned_docs_visible_to_all" in SETTING_METADATA
        meta = SETTING_METADATA["unowned_docs_visible_to_all"]
        assert meta["type"] == "boolean"
        assert meta["category"] == "Authentication"

    @pytest.mark.unit
    def test_default_owner_id_has_metadata(self):
        """default_owner_id must be in SETTING_METADATA."""
        from app.utils.settings_service import SETTING_METADATA

        assert "default_owner_id" in SETTING_METADATA
        meta = SETTING_METADATA["default_owner_id"]
        assert meta["type"] == "user_autocomplete"


# ---------------------------------------------------------------------------
# Claim endpoint tests
# ---------------------------------------------------------------------------


class TestClaimEndpoint:
    """Tests for POST /api/files/{file_id}/claim."""

    @pytest.mark.integration
    def test_claim_disabled_without_multi_user(self, client, db_session):
        """Claiming is rejected when multi-user mode is off."""
        rec = _create_file_record(db_session, owner_id=None, filename="unclaimed.pdf")
        with _patch_multi_user(False):
            response = client.post(f"/api/files/{rec.id}/claim")
        assert response.status_code == 400
        assert "not enabled" in response.json()["detail"]

    @pytest.mark.integration
    def test_claim_unauthenticated_returns_401(self, client, db_session):
        """Claiming without a session returns 401."""
        rec = _create_file_record(db_session, owner_id=None, filename="unclaimed.pdf")
        with _patch_multi_user(True):
            response = client.post(f"/api/files/{rec.id}/claim")
        assert response.status_code == 401


class TestClaimUnit:
    """Unit tests for claim logic directly on the model."""

    @pytest.mark.unit
    def test_claim_sets_owner_id(self, mu_session):
        """Setting owner_id on a NULL-owner file persists correctly."""
        rec = _create_file_record(mu_session, owner_id=None)
        assert rec.owner_id is None

        rec.owner_id = "alice"
        mu_session.commit()
        mu_session.refresh(rec)
        assert rec.owner_id == "alice"

    @pytest.mark.unit
    def test_claim_already_owned_same_user(self, mu_session):
        """Claiming a file you already own returns already_owned status."""
        rec = _create_file_record(mu_session, owner_id="alice")
        # The API would return already_owned; test the model constraint
        assert rec.owner_id == "alice"

    @pytest.mark.unit
    def test_claim_unowned_then_verify(self, mu_session):
        """Claim unowned, then verify the file is filterable by new owner."""
        rec = _create_file_record(mu_session, owner_id=None, filename="orphan.pdf")
        # Before claim: found via NULL filter
        unowned = mu_session.query(FileRecord).filter(FileRecord.owner_id.is_(None)).all()
        assert rec in unowned

        # After claim
        rec.owner_id = "claimer"
        mu_session.commit()
        mu_session.refresh(rec)

        owned = mu_session.query(FileRecord).filter(FileRecord.owner_id == "claimer").all()
        assert rec in owned
        unowned = mu_session.query(FileRecord).filter(FileRecord.owner_id.is_(None)).all()
        assert rec not in unowned


# ---------------------------------------------------------------------------
# Bulk claim endpoint tests
# ---------------------------------------------------------------------------


class TestBulkClaimEndpoint:
    """Tests for POST /api/files/bulk-claim."""

    @pytest.mark.integration
    def test_bulk_claim_disabled_without_multi_user(self, client, db_session):
        """Bulk claiming is rejected when multi-user mode is off."""
        _create_file_record(db_session, owner_id=None, filename="a.pdf")
        with _patch_multi_user(False):
            response = client.post("/api/files/bulk-claim", json=[1])
        assert response.status_code == 400

    @pytest.mark.integration
    def test_bulk_claim_unauthenticated_returns_401(self, client, db_session):
        """Bulk claiming without a session returns 401."""
        rec = _create_file_record(db_session, owner_id=None, filename="a.pdf")
        with _patch_multi_user(True):
            response = client.post("/api/files/bulk-claim", json=[rec.id])
        assert response.status_code == 401


class TestBulkClaimUnit:
    """Unit tests for bulk claim DB logic."""

    @pytest.mark.unit
    def test_bulk_claim_multiple_unowned(self, mu_session):
        """Bulk claim sets owner on multiple NULL-owner files."""
        r1 = _create_file_record(mu_session, owner_id=None, filename="a.pdf")
        r2 = _create_file_record(mu_session, owner_id=None, filename="b.pdf")
        r3 = _create_file_record(mu_session, owner_id="bob", filename="c.pdf")

        # Simulate bulk claim logic
        records = mu_session.query(FileRecord).filter(FileRecord.id.in_([r1.id, r2.id, r3.id])).all()
        claimed, skipped = [], []
        for rec in records:
            if rec.owner_id is None:
                rec.owner_id = "claimer"
                claimed.append(rec.id)
            else:
                skipped.append(rec.id)
        mu_session.commit()

        assert set(claimed) == {r1.id, r2.id}
        assert skipped == [r3.id]

    @pytest.mark.unit
    def test_bulk_claim_all_already_owned(self, mu_session):
        """Bulk claim with all already-owned files skips everything."""
        r1 = _create_file_record(mu_session, owner_id="alice", filename="a.pdf")
        r2 = _create_file_record(mu_session, owner_id="bob", filename="b.pdf")

        records = mu_session.query(FileRecord).filter(FileRecord.id.in_([r1.id, r2.id])).all()
        claimed = [rec for rec in records if rec.owner_id is None]
        assert len(claimed) == 0


# ---------------------------------------------------------------------------
# Assign-owner endpoint tests
# ---------------------------------------------------------------------------


class TestAssignOwnerEndpoint:
    """Tests for POST /api/files/assign-owner."""

    @pytest.mark.integration
    def test_assign_owner_disabled_without_multi_user(self, client, db_session):
        """Assigning owner is rejected when multi-user mode is off."""
        with _patch_multi_user(False):
            response = client.post("/api/files/assign-owner?owner_id=alice")
        assert response.status_code == 400

    @pytest.mark.integration
    def test_assign_owner_requires_admin(self, client, db_session):
        """Non-admin users cannot assign owners."""
        with _patch_multi_user(True):
            response = client.post("/api/files/assign-owner?owner_id=alice")
        # 403 (non-admin) or 401 (no auth)
        assert response.status_code in [401, 403]


class TestAssignOwnerUnit:
    """Unit tests for bulk owner assignment."""

    @pytest.mark.unit
    def test_assign_owner_to_unowned_files(self, mu_session):
        """Bulk update sets owner_id on all NULL-owner files."""
        _create_file_record(mu_session, owner_id=None, filename="a.pdf")
        _create_file_record(mu_session, owner_id=None, filename="b.pdf")
        _create_file_record(mu_session, owner_id="bob", filename="c.pdf")

        updated = (
            mu_session.query(FileRecord)
            .filter(FileRecord.owner_id.is_(None))
            .update({FileRecord.owner_id: "alice"}, synchronize_session="fetch")
        )
        mu_session.commit()

        assert updated == 2
        all_files = mu_session.query(FileRecord).all()
        owners = {f.original_filename: f.owner_id for f in all_files}
        assert owners["a.pdf"] == "alice"
        assert owners["b.pdf"] == "alice"
        assert owners["c.pdf"] == "bob"

    @pytest.mark.unit
    def test_assign_owner_to_specific_files(self, mu_session):
        """Update specific file IDs sets owner_id."""
        rec1 = _create_file_record(mu_session, owner_id=None, filename="a.pdf")
        rec2 = _create_file_record(mu_session, owner_id="bob", filename="b.pdf")

        updated = (
            mu_session.query(FileRecord)
            .filter(FileRecord.id.in_([rec1.id, rec2.id]))
            .update({FileRecord.owner_id: "charlie"}, synchronize_session="fetch")
        )
        mu_session.commit()

        assert updated == 2
        mu_session.refresh(rec1)
        mu_session.refresh(rec2)
        assert rec1.owner_id == "charlie"
        assert rec2.owner_id == "charlie"


# ---------------------------------------------------------------------------
# User search endpoint tests
# ---------------------------------------------------------------------------


class TestUserSearchEndpoint:
    """Tests for GET /api/users/search."""

    @pytest.mark.integration
    def test_search_returns_known_users(self, client, db_session):
        """Search should return distinct owner_ids from file records."""
        _create_file_record(db_session, owner_id="alice", filename="a.pdf")
        _create_file_record(db_session, owner_id="bob", filename="b.pdf")
        _create_file_record(db_session, owner_id="alice", filename="a2.pdf")  # duplicate owner
        _create_file_record(db_session, owner_id=None, filename="c.pdf")  # unowned

        response = client.get("/api/users/search?q=")
        assert response.status_code == 200
        data = response.json()
        assert "users" in data
        # Should contain alice and bob (not None)
        assert set(data["users"]) == {"alice", "bob"}

    @pytest.mark.integration
    def test_search_filters_by_substring(self, client, db_session):
        """Search should filter by case-insensitive substring."""
        _create_file_record(db_session, owner_id="christianlouis", filename="a.pdf")
        _create_file_record(db_session, owner_id="bob", filename="b.pdf")
        _create_file_record(db_session, owner_id="alice", filename="c.pdf")

        response = client.get("/api/users/search?q=risti")
        assert response.status_code == 200
        data = response.json()
        assert data["users"] == ["christianlouis"]

    @pytest.mark.integration
    def test_search_respects_limit(self, client, db_session):
        """Search should respect the limit parameter."""
        for i in range(10):
            _create_file_record(db_session, owner_id=f"user_{i:02d}", filename=f"file_{i}.pdf")

        response = client.get("/api/users/search?q=user&limit=3")
        assert response.status_code == 200
        data = response.json()
        assert len(data["users"]) == 3

    @pytest.mark.integration
    def test_search_empty_when_no_matches(self, client, db_session):
        """Search with no matches should return empty list."""
        _create_file_record(db_session, owner_id="alice", filename="a.pdf")

        response = client.get("/api/users/search?q=zzzzz")
        assert response.status_code == 200
        data = response.json()
        assert data["users"] == []

    @pytest.mark.integration
    def test_search_case_insensitive(self, client, db_session):
        """Search should be case-insensitive."""
        _create_file_record(db_session, owner_id="ChristianLouis", filename="a.pdf")

        response = client.get("/api/users/search?q=CHRISTIAN")
        assert response.status_code == 200
        data = response.json()
        assert data["users"] == ["ChristianLouis"]

    @pytest.mark.integration
    def test_search_no_query_param(self, client, db_session):
        """Search without q parameter defaults to empty string (returns all)."""
        _create_file_record(db_session, owner_id="alice", filename="a.pdf")
        _create_file_record(db_session, owner_id="bob", filename="b.pdf")

        response = client.get("/api/users/search")
        assert response.status_code == 200
        data = response.json()
        assert set(data["users"]) == {"alice", "bob"}

    @pytest.mark.integration
    def test_search_empty_database(self, client, db_session):
        """Search on an empty database returns empty list."""
        response = client.get("/api/users/search?q=anything")
        assert response.status_code == 200
        data = response.json()
        assert data["users"] == []

    @pytest.mark.integration
    def test_search_results_sorted_alphabetically(self, client, db_session):
        """Search results should be sorted alphabetically."""
        _create_file_record(db_session, owner_id="zebra", filename="z.pdf")
        _create_file_record(db_session, owner_id="apple", filename="a.pdf")
        _create_file_record(db_session, owner_id="mango", filename="m.pdf")

        response = client.get("/api/users/search?q=")
        assert response.status_code == 200
        data = response.json()
        assert data["users"] == ["apple", "mango", "zebra"]

    @pytest.mark.integration
    def test_search_default_limit_is_5(self, client, db_session):
        """Default limit should be 5."""
        for i in range(10):
            _create_file_record(db_session, owner_id=f"user_{i:02d}", filename=f"file_{i}.pdf")

        response = client.get("/api/users/search?q=user")
        assert response.status_code == 200
        data = response.json()
        assert len(data["users"]) == 5

    @pytest.mark.integration
    def test_search_whitespace_query_trimmed(self, client, db_session):
        """Leading/trailing whitespace in query should be trimmed."""
        _create_file_record(db_session, owner_id="alice", filename="a.pdf")

        response = client.get("/api/users/search?q=%20alice%20")
        assert response.status_code == 200
        data = response.json()
        assert data["users"] == ["alice"]


# ---------------------------------------------------------------------------
# Additional user_scope filter edge-case tests
# ---------------------------------------------------------------------------


class TestApplyOwnerFilterEdgeCases:
    """Additional edge-case tests for apply_owner_filter."""

    @pytest.mark.unit
    def test_unowned_visible_includes_null_and_own(self, mu_session):
        """With unowned_docs_visible_to_all=True, user sees own + NULL files."""
        from app.utils.user_scope import apply_owner_filter

        own = _create_file_record(mu_session, owner_id="alice", filename="mine.pdf")
        other = _create_file_record(mu_session, owner_id="bob", filename="theirs.pdf")
        orphan = _create_file_record(mu_session, owner_id=None, filename="orphan.pdf")

        request = _mock_request(user={"preferred_username": "alice"})
        query = mu_session.query(FileRecord)

        with _patch_multi_user(True), patch.object(settings, "unowned_docs_visible_to_all", True):
            results = apply_owner_filter(query, request).all()

        result_ids = {r.id for r in results}
        assert own.id in result_ids
        assert orphan.id in result_ids
        assert other.id not in result_ids

    @pytest.mark.unit
    def test_unowned_not_visible_excludes_null(self, mu_session):
        """With unowned_docs_visible_to_all=False, user sees only own files."""
        from app.utils.user_scope import apply_owner_filter

        own = _create_file_record(mu_session, owner_id="alice", filename="mine.pdf")
        orphan = _create_file_record(mu_session, owner_id=None, filename="orphan.pdf")

        request = _mock_request(user={"preferred_username": "alice"})
        query = mu_session.query(FileRecord)

        with _patch_multi_user(True), patch.object(settings, "unowned_docs_visible_to_all", False):
            results = apply_owner_filter(query, request).all()

        result_ids = {r.id for r in results}
        assert own.id in result_ids
        assert orphan.id not in result_ids

    @pytest.mark.unit
    def test_admin_always_sees_all(self, mu_session):
        """Admin user always sees all files, regardless of unowned_docs_visible_to_all."""
        from app.utils.user_scope import apply_owner_filter

        _create_file_record(mu_session, owner_id="alice")
        _create_file_record(mu_session, owner_id=None)

        request = _mock_request(user={"preferred_username": "admin", "is_admin": True})
        query = mu_session.query(FileRecord)

        with _patch_multi_user(True), patch.object(settings, "unowned_docs_visible_to_all", False):
            results = apply_owner_filter(query, request).all()

        assert len(results) == 2

    @pytest.mark.unit
    def test_no_files_returns_empty(self, mu_session):
        """Empty database returns no results for any user."""
        from app.utils.user_scope import apply_owner_filter

        request = _mock_request(user={"preferred_username": "alice"})
        query = mu_session.query(FileRecord)

        with _patch_multi_user(True):
            results = apply_owner_filter(query, request).all()

        assert results == []

    @pytest.mark.unit
    def test_user_with_sub_claim(self, mu_session):
        """User with OAuth 'sub' claim uses sub as owner_id."""
        from app.utils.user_scope import apply_owner_filter

        rec = _create_file_record(mu_session, owner_id="oauth-sub-123")
        request = _mock_request(user={"sub": "oauth-sub-123", "preferred_username": "alice"})
        query = mu_session.query(FileRecord)

        with _patch_multi_user(True), patch.object(settings, "unowned_docs_visible_to_all", False):
            results = apply_owner_filter(query, request).all()

        assert len(results) == 1
        assert results[0].id == rec.id
