"""Follow-up regression tests for the NEW collection classification semantics.

A collection (``collection_<uuid>`` or the aggregate ``library``) is ALWAYS a
local knowledge base, and its ``is_public`` flag is ADDITIVE — marking a
collection public ALSO makes it eligible under PUBLIC_ONLY/cloud inference, but
NEVER removes its private (local) eligibility. Concretely, ``_engine_bucket``
returns ``(is_public, is_local=True)`` for any collection.

These tests pin that bucket shape directly, the DB-resolution fallback when no
metadata is supplied, and the ADAPTIVE-scope resolution when the PRIMARY engine
is a collection (public -> BOTH, private -> PRIVATE_ONLY forcing local
inference). They complement (do not duplicate) the metadata-driven evaluate_engine
cases already covered in tests/security/test_egress_policy.py by attacking the
``_engine_bucket`` / ``_resolve_collection_is_public`` / ``_resolve_adaptive_scope``
functions directly and the DB-lookup path.
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

from local_deep_research.security.egress.policy import (
    EgressContext,
    EgressScope,
    _engine_bucket,
    _resolve_adaptive_scope,
    _resolve_collection_is_public,
    context_from_snapshot,
    evaluate_engine,
)


# ---------------------------------------------------------------------------
# Helpers (mirrors make_ctx in tests/security/test_egress_policy.py)
# ---------------------------------------------------------------------------


def make_ctx(
    scope: EgressScope = EgressScope.BOTH,
    primary: str = "arxiv",
    require_local_llm: bool = False,
    require_local_embeddings: bool = False,
    local_hostnames=(),
    username=None,
) -> EgressContext:
    return EgressContext(
        scope=scope,
        primary_engine=primary,
        require_local_llm=require_local_llm,
        require_local_embeddings=require_local_embeddings,
        local_hostnames=tuple(local_hostnames),
        username=username,
    )


_POLICY = "local_deep_research.security.egress.policy"


# ---------------------------------------------------------------------------
# _engine_bucket — the (is_public, is_local=True) contract
# ---------------------------------------------------------------------------


def test_engine_bucket_public_collection_is_public_AND_local():
    """A public collection buckets as (True, True): the public flag is
    ADDITIVE, it never strips the local-KB eligibility."""
    ctx = make_ctx(primary="collection_pub")
    bucket = _engine_bucket(
        "collection_pub", ctx, {}, metadata={"is_public": True}
    )
    assert bucket == (True, True)


def test_engine_bucket_private_collection_is_local_only():
    """A private collection buckets as (False, True) — local, not public."""
    ctx = make_ctx(primary="collection_priv")
    bucket = _engine_bucket(
        "collection_priv", ctx, {}, metadata={"is_public": False}
    )
    assert bucket == (False, True)


def test_engine_bucket_library_is_local_only_via_db_default():
    """``library`` is the always-private aggregate; with no metadata it
    resolves via the (always-False) collection resolver -> (False, True)."""
    ctx = make_ctx(primary="library")
    bucket = _engine_bucket("library", ctx, {})
    assert bucket == (False, True)


def test_engine_bucket_metadata_short_circuits_db_lookup():
    """When the caller supplies engine metadata, _engine_bucket must NOT
    fall back to the DB resolver — the metadata flag wins outright."""
    ctx = make_ctx(primary="collection_x")
    with patch(f"{_POLICY}._resolve_collection_is_public") as mock_resolve:
        mock_resolve.return_value = False  # would mis-classify if consulted
        bucket = _engine_bucket(
            "collection_x", ctx, {}, metadata={"is_public": True}
        )
    mock_resolve.assert_not_called()
    assert bucket == (True, True)


def test_engine_bucket_collection_no_metadata_consults_db_resolver():
    """Without metadata, _engine_bucket delegates classification to
    _resolve_collection_is_public and threads is_local=True regardless."""
    ctx = make_ctx(primary="collection_y", username="alice")
    with patch(
        f"{_POLICY}._resolve_collection_is_public", return_value=True
    ) as mock_resolve:
        bucket = _engine_bucket("collection_y", ctx, {})
    mock_resolve.assert_called_once_with("collection_y", "alice")
    assert bucket == (True, True)


# ---------------------------------------------------------------------------
# evaluate_engine — library across ALL scopes
# ---------------------------------------------------------------------------


def test_library_engine_evaluate_across_all_scopes():
    """``library`` (always private/local): allowed under PRIVATE_ONLY and
    BOTH, DENIED under PUBLIC_ONLY, allowed under STRICT only when primary."""
    # PRIVATE_ONLY / BOTH -> allowed
    for scope in (EgressScope.PRIVATE_ONLY, EgressScope.BOTH):
        ctx = make_ctx(scope=scope, primary="library")
        assert evaluate_engine("library", ctx, settings_snapshot={}).allowed, (
            f"library should be allowed under {scope}"
        )
    # PUBLIC_ONLY -> denied (it is local-only)
    ctx_pub = make_ctx(scope=EgressScope.PUBLIC_ONLY, primary="library")
    denied = evaluate_engine("library", ctx_pub, settings_snapshot={})
    assert denied.allowed is False
    assert denied.reason == "scope_mismatch_public_only"
    # STRICT + library is primary -> allowed
    ctx_strict_ok = make_ctx(scope=EgressScope.STRICT, primary="library")
    assert evaluate_engine(
        "library", ctx_strict_ok, settings_snapshot={}
    ).allowed
    # STRICT + library is NOT primary -> denied
    ctx_strict_no = make_ctx(scope=EgressScope.STRICT, primary="arxiv")
    d = evaluate_engine("library", ctx_strict_no, settings_snapshot={})
    assert d.allowed is False
    assert d.reason == "strict_not_primary"


# ---------------------------------------------------------------------------
# evaluate_engine — DB-resolution path (no metadata supplied)
# ---------------------------------------------------------------------------


def test_collection_db_default_private_denied_under_public_only():
    """A collection resolved to private via the DB path is excluded from
    PUBLIC_ONLY — the additive flag must default closed (private)."""
    ctx = make_ctx(scope=EgressScope.PUBLIC_ONLY, primary="collection_z")
    with patch(f"{_POLICY}._resolve_collection_is_public", return_value=False):
        decision = evaluate_engine("collection_z", ctx, settings_snapshot={})
    assert decision.allowed is False
    assert decision.reason == "scope_mismatch_public_only"


def test_collection_db_public_allowed_under_public_only_without_metadata():
    """A collection the DB marks public is allowed under PUBLIC_ONLY even
    when the caller passes NO metadata (the resolver is consulted)."""
    ctx = make_ctx(scope=EgressScope.PUBLIC_ONLY, primary="collection_z")
    with patch(f"{_POLICY}._resolve_collection_is_public", return_value=True):
        decision = evaluate_engine("collection_z", ctx, settings_snapshot={})
    assert decision.allowed is True


def test_collection_db_public_still_allowed_under_private_only():
    """Additive semantics: a PUBLIC collection (no metadata, DB path) is
    STILL local, so it remains allowed under PRIVATE_ONLY."""
    ctx = make_ctx(scope=EgressScope.PRIVATE_ONLY, primary="collection_z")
    with patch(f"{_POLICY}._resolve_collection_is_public", return_value=True):
        decision = evaluate_engine("collection_z", ctx, settings_snapshot={})
    assert decision.allowed is True


# ---------------------------------------------------------------------------
# _resolve_collection_is_public — direct unit coverage of the DB path
# ---------------------------------------------------------------------------


def test_resolve_collection_is_public_library_always_private():
    """``library`` short-circuits to False without any DB access."""
    assert _resolve_collection_is_public("library", "alice") is False


def test_resolve_collection_is_public_non_collection_name_false():
    """A non-collection engine name short-circuits to False."""
    assert _resolve_collection_is_public("arxiv", "alice") is False


def test_resolve_collection_is_public_reads_db_row_true():
    """The DB path returns the row's is_public flag (True case)."""
    mock_row = MagicMock()
    mock_row.is_public = True
    mock_session = MagicMock()
    mock_session.query.return_value.filter.return_value.first.return_value = (
        mock_row
    )
    cm = MagicMock()
    cm.__enter__.return_value = mock_session
    cm.__exit__.return_value = False
    with patch(
        "local_deep_research.database.session_context.get_user_db_session",
        return_value=cm,
    ):
        result = _resolve_collection_is_public("collection_pub123", "alice")
    assert result is True


def test_resolve_collection_is_public_missing_row_fails_closed_private():
    """No matching collection row -> False (private), not an error."""
    mock_session = MagicMock()
    mock_session.query.return_value.filter.return_value.first.return_value = (
        None
    )
    cm = MagicMock()
    cm.__enter__.return_value = mock_session
    cm.__exit__.return_value = False
    with patch(
        "local_deep_research.database.session_context.get_user_db_session",
        return_value=cm,
    ):
        result = _resolve_collection_is_public("collection_missing", "alice")
    assert result is False


def test_resolve_collection_is_public_db_error_fails_closed_private():
    """Any DB error fails closed to private (False) — never public."""
    with patch(
        "local_deep_research.database.session_context.get_user_db_session",
        side_effect=RuntimeError("db down"),
    ):
        result = _resolve_collection_is_public("collection_err", "alice")
    assert result is False


# ---------------------------------------------------------------------------
# _resolve_adaptive_scope — collection primary classification
# ---------------------------------------------------------------------------


def test_adaptive_public_collection_primary_resolves_to_both():
    """A PUBLIC collection primary buckets (True, True); neither exclusive
    branch fires, so ADAPTIVE resolves to BOTH (cloud inference allowed)."""
    with patch(f"{_POLICY}._resolve_collection_is_public", return_value=True):
        scope = _resolve_adaptive_scope(
            "collection_pub",
            {},
            username="alice",
            local_hostnames=(),
        )
    assert scope == EgressScope.BOTH


def test_adaptive_private_collection_primary_resolves_to_private_only():
    """A PRIVATE collection primary buckets (False, True) -> PRIVATE_ONLY."""
    with patch(f"{_POLICY}._resolve_collection_is_public", return_value=False):
        scope = _resolve_adaptive_scope(
            "collection_priv",
            {},
            username="alice",
            local_hostnames=(),
        )
    assert scope == EgressScope.PRIVATE_ONLY


def test_adaptive_library_primary_resolves_to_private_only():
    """``library`` is always private -> ADAPTIVE resolves to PRIVATE_ONLY."""
    scope = _resolve_adaptive_scope(
        "library",
        {},
        username="alice",
        local_hostnames=(),
    )
    assert scope == EgressScope.PRIVATE_ONLY


# ---------------------------------------------------------------------------
# context_from_snapshot — ADAPTIVE + collection primary end-to-end
# ---------------------------------------------------------------------------


def test_context_adaptive_public_collection_both_no_force_local():
    """ADAPTIVE + public-collection primary -> resolved scope BOTH and the
    local-inference flags are NOT forced (cloud LLM/embeddings permitted)."""
    snapshot = {"policy.egress_scope": {"value": "adaptive"}}
    with patch(f"{_POLICY}._resolve_collection_is_public", return_value=True):
        ctx = context_from_snapshot(
            snapshot, primary_engine="collection_pub", username="alice"
        )
    assert ctx.scope == EgressScope.BOTH
    assert ctx.require_local_llm is False
    assert ctx.require_local_embeddings is False


def test_context_adaptive_private_collection_private_forces_local():
    """ADAPTIVE + private-collection primary -> PRIVATE_ONLY, which implies
    (forces) local LLM and local embeddings so the corpus can't leak."""
    snapshot = {"policy.egress_scope": {"value": "adaptive"}}
    with patch(f"{_POLICY}._resolve_collection_is_public", return_value=False):
        ctx = context_from_snapshot(
            snapshot, primary_engine="collection_priv", username="alice"
        )
    assert ctx.scope == EgressScope.PRIVATE_ONLY
    assert ctx.require_local_llm is True
    assert ctx.require_local_embeddings is True
