"""Coverage for the request-boundary egress precheck in research_routes
(flagged untested by the PR #4300 review).

Targets:
- _apply_policy_overrides: overlays per-research form overrides onto the
  snapshot (pure dict logic).
- _precheck_engine_policy: rejects a forbidden engine / corrupt scope
  at /api/start_research with a 400, or returns None to continue.
  Needs a Flask app context for jsonify.
"""

from unittest.mock import Mock

import pytest
from flask import Flask

from local_deep_research.web.routes.research_routes import (
    _apply_policy_overrides,
    _precheck_engine_policy,
)


# ---------------------------------------------------------------------------
# _apply_policy_overrides (pure)
# ---------------------------------------------------------------------------


class TestApplyPolicyOverrides:
    def test_scope_override_applied(self):
        snap = {"policy.egress_scope": "both"}
        _apply_policy_overrides(snap, {"policy_egress_scope": "private_only"})
        assert snap["policy.egress_scope"] == "private_only"

    def test_bool_overrides_coerced(self):
        snap = {}
        _apply_policy_overrides(
            snap,
            {
                "llm_require_local_endpoint": "1",
                "embeddings_require_local": "",
            },
        )
        assert snap["llm.require_local_endpoint"] is True
        # Empty string is falsy => coerced to False.
        assert snap["embeddings.require_local"] is False

    def test_absent_params_leave_snapshot_untouched(self):
        snap = {"policy.egress_scope": "both"}
        _apply_policy_overrides(snap, {})
        assert snap == {"policy.egress_scope": "both"}

    def test_non_dict_snapshot_is_noop(self):
        # Must not raise on a non-dict snapshot.
        assert (
            _apply_policy_overrides(None, {"policy_egress_scope": "x"}) is None
        )


# ---------------------------------------------------------------------------
# _precheck_engine_policy (needs app context for jsonify)
# ---------------------------------------------------------------------------


def _mgr(snapshot, primary="arxiv"):
    m = Mock()
    # A real settings snapshot always carries the primary under "search.tool".
    # The precheck now resolves the primary FROM the snapshot (matching the
    # worker via resolve_run_primary_engine), so reflect that here.
    if isinstance(snapshot, dict) and "search.tool" not in snapshot:
        snapshot = {**snapshot, "search.tool": primary}
    m.get_settings_snapshot.return_value = snapshot
    m.get_setting.side_effect = lambda key, default=None: (
        primary if key == "search.tool" else default
    )
    return m


@pytest.fixture
def app_ctx():
    app = Flask(__name__)
    with app.app_context():
        yield


class TestPrecheckEnginePolicy:
    def test_allowed_engine_returns_none(self, app_ctx):
        # PUBLIC_ONLY + a public engine (arxiv) => allowed => continue.
        mgr = _mgr({"policy.egress_scope": "public_only"})
        result = _precheck_engine_policy(mgr, {}, "arxiv", "user")
        assert result is None

    def test_forbidden_engine_returns_400(self, app_ctx):
        # PUBLIC_ONLY + a local engine (library) => refused => 400.
        mgr = _mgr({"policy.egress_scope": "public_only"})
        result = _precheck_engine_policy(mgr, {}, "library", "user")
        assert result is not None
        _resp, status = result
        assert status == 400

    def test_corrupt_scope_returns_400(self, app_ctx):
        mgr = _mgr({"policy.egress_scope": "garbage"})
        result = _precheck_engine_policy(mgr, {}, "arxiv", "user")
        assert result is not None
        assert result[1] == 400

    def test_non_dict_snapshot_returns_none(self, app_ctx):
        # No real snapshot => skip precheck (factory PEP backstops).
        mgr = _mgr(None)
        assert _precheck_engine_policy(mgr, {}, "library", "user") is None

    def test_missing_primary_returns_400(self, app_ctx):
        # No configured primary (empty search.tool) => the precheck fails
        # CLOSED at the API boundary (400), matching the worker — no silent
        # searxng fallback that would accept a run the worker then refuses.
        mgr = _mgr({"policy.egress_scope": "public_only", "search.tool": ""})
        result = _precheck_engine_policy(mgr, {}, "arxiv", "user")
        assert result is not None
        assert result[1] == 400

    def test_per_research_override_tightens_scope(self, app_ctx):
        # Saved scope is permissive (both) but the form overrides to
        # public_only for THIS run => a local engine must be refused.
        mgr = _mgr({"policy.egress_scope": "both"})
        params = {"policy_egress_scope": "public_only"}
        result = _precheck_engine_policy(mgr, params, "library", "user")
        assert result is not None
        assert result[1] == 400
