"""Integration tests for the REAL fetch/download Policy Enforcement Points.

These exercise the call sites that actually consult the egress PDP — NOT the
PDP functions themselves (those have their own ~390 unit tests). The point is
to prove that the policy decision actually fires at the call site:

  * ``content_fetcher.fetcher.ContentFetcher.fetch`` returns the structured
    egress-policy error for a scope-incompatible URL and never reaches a
    downloader, while a scope-compatible URL passes the gate.
  * ``ContentFetcher._apply_egress_policy_to_downloader`` relaxes a
    downloader's SafeSession only under PRIVATE_ONLY.
  * ``security.egress.fetch.policy_aware_validate_url`` threads the scope
    through SSRF validation (private IPs only under PRIVATE_ONLY; cloud
    metadata always blocked).
  * ``research_library.services.download_service.DownloadService.
    _check_url_against_policy`` denies / allows per scope and always blocks
    cloud-metadata.

All tests use literal IPs so no DNS lookup or outbound connect ever happens;
the policy gate decides before any network work. Each assertion is paired
(allow + deny) so it fails if the security property were reverted.
"""

from __future__ import annotations

from unittest.mock import Mock, patch

import pytest

from local_deep_research.content_fetcher.fetcher import ContentFetcher
from local_deep_research.security.egress.audit_hook import (
    clear_active_context,
    get_active_context,
)
from local_deep_research.security.egress.fetch import policy_aware_validate_url
from local_deep_research.security.egress.policy import (
    EgressContext,
    EgressScope,
)

# Literal IPs — classified without any DNS lookup.
PUBLIC_IP_URL = "http://8.8.8.8/paper.html"
PRIVATE_IP_URL = "http://192.168.0.50/paper.html"
METADATA_IP_URL = "http://169.254.169.254/latest/meta-data/"


def make_ctx(scope: EgressScope, primary: str = "arxiv") -> EgressContext:
    """Build a concrete-scope EgressContext directly (no snapshot/DNS)."""
    return EgressContext(
        scope=scope,
        primary_engine=primary,
        require_local_llm=False,
        require_local_embeddings=False,
    )


@pytest.fixture(autouse=True)
def _no_armed_context():
    """Keep the audit-hook thread-local clean so a leaked armed context from
    another test can't anchor evaluate_url's per-run denial quota here, and so
    we never leak one ourselves.
    """
    clear_active_context()
    try:
        yield
    finally:
        clear_active_context()
    assert get_active_context() is None


class _FakeResult:
    """Minimal stand-in for a downloader's download_with_result() return."""

    def __init__(self, content=b"hello world", is_success=True):
        self.is_success = is_success
        self.content = content
        self.skip_reason = None


# ---------------------------------------------------------------------------
# ContentFetcher.fetch — the central URL PEP
# ---------------------------------------------------------------------------


class TestContentFetcherFetchPEP:
    def test_public_url_denied_under_private_only_never_reaches_downloader(
        self,
    ):
        """PRIVATE_ONLY must refuse a public host with the structured egress
        error AND short-circuit before any downloader is constructed/called.
        """
        fetcher = ContentFetcher(
            egress_context=make_ctx(EgressScope.PRIVATE_ONLY)
        )
        with patch.object(fetcher, "_get_downloader") as mock_get:
            result = fetcher.fetch(PUBLIC_IP_URL)
        assert result["status"] == "error"
        assert "egress policy" in result["error"]
        # The gate fired BEFORE downloader selection — the security property.
        mock_get.assert_not_called()

    def test_private_url_allowed_under_private_only_reaches_downloader(self):
        """The allow half of the pair: a private host under PRIVATE_ONLY passes
        both the SSRF and the egress gate and is handed to a downloader.
        """
        fetcher = ContentFetcher(
            egress_context=make_ctx(EgressScope.PRIVATE_ONLY)
        )
        downloader = Mock()
        downloader.download_with_result.return_value = _FakeResult()
        downloader.get_metadata.return_value = {"title": "T"}
        with patch.object(fetcher, "_get_downloader", return_value=downloader):
            result = fetcher.fetch(PRIVATE_IP_URL)
        assert result["status"] == "success"
        assert "egress policy" not in (result.get("error") or "")
        downloader.download_with_result.assert_called_once()

    def test_private_url_denied_under_public_only(self):
        """Mirror scope: PUBLIC_ONLY refuses the same private host that
        PRIVATE_ONLY allowed — proves the decision is scope-driven, not a
        blanket private-IP block.
        """
        fetcher = ContentFetcher(
            egress_context=make_ctx(EgressScope.PUBLIC_ONLY)
        )
        with patch.object(fetcher, "_get_downloader") as mock_get:
            result = fetcher.fetch(PRIVATE_IP_URL)
        assert result["status"] == "error"
        # Private host under PUBLIC_ONLY fails SSRF first (strict default),
        # so either the SSRF error or the egress error is acceptable — the
        # security property is simply "denied, no downloader".
        assert "egress policy" in result["error"] or "SSRF" in result["error"]
        mock_get.assert_not_called()

    def test_public_url_allowed_under_public_only(self):
        fetcher = ContentFetcher(
            egress_context=make_ctx(EgressScope.PUBLIC_ONLY)
        )
        downloader = Mock()
        downloader.download_with_result.return_value = _FakeResult()
        downloader.get_metadata.return_value = {}
        with patch.object(fetcher, "_get_downloader", return_value=downloader):
            result = fetcher.fetch(PUBLIC_IP_URL)
        assert result["status"] == "success"
        downloader.download_with_result.assert_called_once()

    def test_metadata_url_denied_even_under_private_only(self):
        """Cloud-metadata is NEVER fetchable, even under the scope that allows
        private hosts. Without an egress_context there is no scope gate, so we
        arm PRIVATE_ONLY and confirm the metadata IP is still refused.
        """
        fetcher = ContentFetcher(
            egress_context=make_ctx(EgressScope.PRIVATE_ONLY)
        )
        with patch.object(fetcher, "_get_downloader") as mock_get:
            result = fetcher.fetch(METADATA_IP_URL)
        assert result["status"] == "error"
        mock_get.assert_not_called()


# ---------------------------------------------------------------------------
# policy_aware_validate_url — the SSRF/scope-threading PEP helper
# ---------------------------------------------------------------------------


class TestPolicyAwareValidateUrl:
    def test_private_ip_allowed_only_under_private_only(self):
        # Deny under BOTH (strict SSRF default) ...
        assert (
            policy_aware_validate_url(
                PRIVATE_IP_URL, make_ctx(EgressScope.BOTH)
            )
            is False
        )
        # ... and under no-context (back-compat strict) ...
        assert policy_aware_validate_url(PRIVATE_IP_URL, None) is False
        # ... but allowed under PRIVATE_ONLY (the documented relaxation).
        assert (
            policy_aware_validate_url(
                PRIVATE_IP_URL, make_ctx(EgressScope.PRIVATE_ONLY)
            )
            is True
        )

    def test_metadata_ip_blocked_even_under_private_only(self):
        # A public IP under PRIVATE_ONLY relaxation passes SSRF, proving the
        # relaxation is live; the metadata IP under the SAME scope is still
        # blocked — the always-blocked invariant.
        assert (
            policy_aware_validate_url(
                PUBLIC_IP_URL, make_ctx(EgressScope.PRIVATE_ONLY)
            )
            is True
        )
        assert (
            policy_aware_validate_url(
                METADATA_IP_URL, make_ctx(EgressScope.PRIVATE_ONLY)
            )
            is False
        )


# ---------------------------------------------------------------------------
# ContentFetcher._apply_egress_policy_to_downloader — session relaxation PEP
# ---------------------------------------------------------------------------


class TestApplyEgressPolicyToDownloader:
    def _downloader_with_session(self):
        downloader = Mock()
        session = Mock()
        session.allow_private_ips = False
        downloader.session = session
        return downloader, session

    def test_relaxes_session_under_private_only(self):
        fetcher = ContentFetcher(
            egress_context=make_ctx(EgressScope.PRIVATE_ONLY)
        )
        downloader, session = self._downloader_with_session()
        fetcher._apply_egress_policy_to_downloader(downloader)
        assert session.allow_private_ips is True

    def test_does_not_relax_session_under_public_only(self):
        fetcher = ContentFetcher(
            egress_context=make_ctx(EgressScope.PUBLIC_ONLY)
        )
        downloader, session = self._downloader_with_session()
        fetcher._apply_egress_policy_to_downloader(downloader)
        assert session.allow_private_ips is False

    def test_does_not_relax_session_under_both(self):
        fetcher = ContentFetcher(egress_context=make_ctx(EgressScope.BOTH))
        downloader, session = self._downloader_with_session()
        fetcher._apply_egress_policy_to_downloader(downloader)
        assert session.allow_private_ips is False

    def test_no_context_does_not_relax_session(self):
        fetcher = ContentFetcher(egress_context=None)
        downloader, session = self._downloader_with_session()
        fetcher._apply_egress_policy_to_downloader(downloader)
        assert session.allow_private_ips is False


# ---------------------------------------------------------------------------
# DownloadService._check_url_against_policy — library download PEP
# ---------------------------------------------------------------------------


def _make_download_service(tmp_path, scope_value):
    """Construct a real DownloadService whose EgressContext is built from a
    snapshot carrying ``scope_value``. Heavy deps (settings backend, retry
    manager, library dir) are mocked; the policy gate under test is real.
    """
    mock_settings = Mock()
    mock_settings.get_setting.side_effect = lambda key, default=None: {
        "research_library.storage_path": str(tmp_path),
        "search.engine.web.semantic_scholar.api_key": "",
    }.get(key, default)

    snapshot = {
        "policy.egress_scope": scope_value,
        "search.tool": "arxiv",
    }

    with (
        patch(
            "local_deep_research.research_library.services."
            "download_service.get_settings_manager",
            return_value=mock_settings,
        ),
        patch(
            "local_deep_research.research_library.services."
            "download_service.RetryManager",
        ),
        patch(
            "local_deep_research.research_library.services."
            "download_service.get_library_directory",
            return_value=tmp_path,
        ),
    ):
        from local_deep_research.research_library.services.download_service import (  # noqa: E501
            DownloadService,
        )

        return DownloadService("tester", "pw", settings_snapshot=snapshot)


class TestDownloadServiceCheckUrlPEP:
    def test_public_denied_private_allowed_under_private_only(self, tmp_path):
        service = _make_download_service(tmp_path, "private_only")
        try:
            assert service._egress_context is not None
            assert service._egress_context.scope == EgressScope.PRIVATE_ONLY
            pub_allowed, _ = service._check_url_against_policy(PUBLIC_IP_URL)
            priv_allowed, _ = service._check_url_against_policy(PRIVATE_IP_URL)
            assert pub_allowed is False
            assert priv_allowed is True
        finally:
            service.close()

    def test_public_allowed_private_denied_under_public_only(self, tmp_path):
        service = _make_download_service(tmp_path, "public_only")
        try:
            assert service._egress_context.scope == EgressScope.PUBLIC_ONLY
            pub_allowed, _ = service._check_url_against_policy(PUBLIC_IP_URL)
            priv_allowed, _ = service._check_url_against_policy(PRIVATE_IP_URL)
            assert pub_allowed is True
            assert priv_allowed is False
        finally:
            service.close()

    def test_metadata_url_always_denied(self, tmp_path):
        # Denied under the scope that otherwise permits private hosts.
        service = _make_download_service(tmp_path, "private_only")
        try:
            allowed, reason = service._check_url_against_policy(METADATA_IP_URL)
            assert allowed is False
            assert reason == "blocked_metadata_ip"
        finally:
            service.close()
