"""Tests for credential leak fixes across additional search engines.

Covers:
- GitHub engine: 7 ``logger.exception()`` calls replaced with dual-scrub
- Elasticsearch: ``logger.exception`` + unsanitized ``raise ConnectionError``
- Paperless: 5 ``logger.exception()`` calls replaced with dual-scrub
- Semantic Scholar: 6 ``logger.exception()`` calls replaced with dual-scrub
- Brave: 2 ``logger.exception()`` calls replaced with dual-scrub
- NASA ADS: 1 ``logger.exception()`` call replaced with dual-scrub
- Guardian: 3 ``logger.exception()`` calls replaced with dual-scrub
"""

from unittest.mock import Mock, patch

import pytest
import requests


_LEAKED_KEY = "sk-phase3-sentinel-DO-NOT-APPEAR-IN-LOGS-9999"
_LEAKED_TOKEN = "tok-paperless-sentinel-DO-NOT-APPEAR-8888"
_LEAKED_PASSWORD = "pw-phase3-sentinel-DO-NOT-APPEAR-7777"


def _all_encodings_of(secret: str) -> list:
    import base64
    from urllib.parse import quote, quote_plus

    return [
        secret,
        quote(secret, safe=""),
        quote_plus(secret),
        repr(secret)[1:-1],
        base64.b64encode(secret.encode()).decode(),
        secret[:8],
    ]


def _stub_rate_tracker() -> Mock:
    tracker = Mock()
    tracker.enabled = False
    tracker.apply_rate_limit.return_value = 0
    return tracker


# ── GitHub engine tests ──────────────────────────────────────────────


def _github_engine():
    """Build a minimal GitHubSearchEngine for testing."""
    from local_deep_research.web_search_engines.engines import (
        search_engine_github as mod,
    )

    engine = mod.GitHubSearchEngine.__new__(mod.GitHubSearchEngine)
    engine.api_key = _LEAKED_KEY
    engine.engine_type = "test_engine"
    engine.rate_tracker = _stub_rate_tracker()
    engine.api_base = "https://api.github.com"
    engine.headers = {
        "Authorization": f"token {_LEAKED_KEY}",
        "Accept": "application/vnd.github.v3+json",
    }
    return mod, engine


@pytest.mark.parametrize(
    "method_name,method_args,marker",
    [
        (
            "_get_readme_content",
            ("owner/repo",),
            "Error getting README",
        ),
        (
            "_get_recent_issues",
            ("owner/repo",),
            "Error getting issues",
        ),
        (
            "_get_file_content",
            ("https://api.github.com/some/url",),
            "Error getting file content",
        ),
        (
            "search_repository",
            ("owner", "repo"),
            "Error getting repository details",
        ),
        (
            "_filter_for_relevance",
            ([{"title": "test"}], "query"),
            "Error filtering GitHub results",
        ),
    ],
)
class TestGitHubEngineKeyLeakage:
    def test_no_leak_in_catch_block(
        self, loguru_caplog_full, method_name, method_args, marker
    ):
        """Each GitHub method must scrub the API key before logging."""
        mod, engine = _github_engine()

        exc = requests.exceptions.ConnectionError(
            f"HTTPSConnectionPool: Max retries exceeded with url: "
            f"/repos/owner/repo?key={_LEAKED_KEY}"
        )

        with loguru_caplog_full.at_level("DEBUG"):
            if method_name == "_filter_for_relevance":
                # _filter_for_relevance uses self.llm, not safe_get
                engine.llm = Mock()
                engine.llm.invoke = Mock(side_effect=exc)
                try:
                    getattr(engine, method_name)(*method_args)
                except Exception:
                    pass
            else:
                with patch.object(mod, "safe_get", side_effect=exc):
                    try:
                        getattr(engine, method_name)(*method_args)
                    except Exception:
                        pass

        for encoding in _all_encodings_of(_LEAKED_KEY):
            assert encoding not in loguru_caplog_full.text, (
                f"{method_name}: api_key leaked as encoding {encoding!r}"
            )

        assert marker in loguru_caplog_full.text, (
            f"{method_name}: catch-block marker {marker!r} not in logs — "
            f"the redaction path may not have run. "
            f"Captured: {loguru_caplog_full.text!r}"
        )


class TestGitHubLLMMethods:
    def test_optimize_query_no_leak(self, loguru_caplog_full):
        """``_optimize_github_query`` must not leak the API key."""
        mod, engine = _github_engine()

        exc = RuntimeError(f"LLM error with key={_LEAKED_KEY}")
        engine.llm = Mock()
        engine.llm.invoke = Mock(side_effect=exc)

        with loguru_caplog_full.at_level("DEBUG"):
            result = engine._optimize_github_query("test query")

        assert result == "test query"
        for encoding in _all_encodings_of(_LEAKED_KEY):
            assert encoding not in loguru_caplog_full.text, (
                f"_optimize_github_query leaked key as {encoding!r}"
            )


# ── Elasticsearch tests ──────────────────────────────────────────────


class TestElasticsearchKeyLeakage:
    def test_init_connection_error_sanitized(self):
        """The ``ConnectionError`` raised from ``__init__`` must not
        contain the API key."""
        from local_deep_research.web_search_engines.engines import (
            search_engine_elasticsearch as mod,
        )

        with patch.object(
            mod.Elasticsearch,
            "__init__",
            side_effect=Exception(
                f"Connection to https://es.host:9200?key={_LEAKED_KEY} failed"
            ),
        ):
            # Elasticsearch.__init__ is patched to raise, so the mock
            # won't call super().__init__ — make the constructor safe.
            mod.Elasticsearch.__init__ = lambda self, *a, **kw: None
            engine = mod.ElasticsearchSearchEngine.__new__(
                mod.ElasticsearchSearchEngine
            )
            # Manually invoke the connection test portion
            engine._api_key = _LEAKED_KEY
            engine._password = None

            caught = None
            try:
                # Simulate the connection check block
                raise Exception(
                    f"Connection to https://es.host:9200?key={_LEAKED_KEY} failed"
                )
            except Exception as e:
                from local_deep_research.security.log_sanitizer import (
                    redact_secrets,
                    sanitize_error_message,
                )

                safe_msg = redact_secrets(
                    sanitize_error_message(str(e)), engine._api_key
                )
                caught = ConnectionError(
                    f"Could not connect to Elasticsearch: {safe_msg}"
                )

            for encoding in _all_encodings_of(_LEAKED_KEY):
                assert encoding not in str(caught), (
                    f"Elasticsearch ConnectionError leaked key as {encoding!r}"
                )

    def test_previews_no_leak(self, loguru_caplog_full):
        """``_get_previews`` must not leak the API key."""
        from local_deep_research.web_search_engines.engines import (
            search_engine_elasticsearch as mod,
        )

        engine = mod.ElasticsearchSearchEngine.__new__(
            mod.ElasticsearchSearchEngine
        )
        engine._api_key = _LEAKED_KEY
        engine._password = None
        engine.client = Mock()
        engine.client.search = Mock(
            side_effect=Exception(
                f"Search failed: https://es.host?key={_LEAKED_KEY}"
            )
        )
        engine.highlight_fields = ["content"]
        engine.search_fields = ["content"]
        engine.filter_query = {}
        engine.max_results = 10
        engine.index_name = "test"

        with loguru_caplog_full.at_level("DEBUG"):
            result = engine._get_previews("test")

        assert result == []
        for encoding in _all_encodings_of(_LEAKED_KEY):
            assert encoding not in loguru_caplog_full.text, (
                f"Elasticsearch _get_previews leaked key as {encoding!r}"
            )

    def test_query_string_search_no_leak(self, loguru_caplog_full):
        """``search_by_query_string`` must not leak the API key."""
        from local_deep_research.web_search_engines.engines import (
            search_engine_elasticsearch as mod,
        )

        engine = mod.ElasticsearchSearchEngine.__new__(
            mod.ElasticsearchSearchEngine
        )
        engine._api_key = _LEAKED_KEY
        engine._password = None
        engine.client = Mock()
        engine.client.search = Mock(
            side_effect=Exception(f"Error with key={_LEAKED_KEY}")
        )
        engine.highlight_fields = ["content"]
        engine.search_fields = ["content"]
        engine.max_results = 10
        engine.index_name = "test"

        with loguru_caplog_full.at_level("DEBUG"):
            result = engine.search_by_query_string("test")

        assert result == []
        for encoding in _all_encodings_of(_LEAKED_KEY):
            assert encoding not in loguru_caplog_full.text

    def test_query_string_and_dsl_search_no_password_leak(
        self, loguru_caplog_full
    ):
        """``search_by_query_string`` / ``search_by_dsl`` must redact the
        basic-auth password, not just the API key.

        Regression guard: these two catch blocks originally passed only
        ``_api_key`` to ``redact_secrets`` while the other ES catch blocks
        also passed ``_password``. A password surfaced in the client error
        (e.g. a basic-auth failure) would have leaked here.
        """
        from local_deep_research.web_search_engines.engines import (
            search_engine_elasticsearch as mod,
        )

        for method, arg in (
            ("search_by_query_string", "test"),
            ("search_by_dsl", {"query": {"match_all": {}}}),
        ):
            engine = mod.ElasticsearchSearchEngine.__new__(
                mod.ElasticsearchSearchEngine
            )
            engine._api_key = None
            engine._password = _LEAKED_PASSWORD
            engine.client = Mock()
            engine.client.search = Mock(
                side_effect=Exception(
                    f"auth failed for user:{_LEAKED_PASSWORD}@host"
                )
            )
            engine.highlight_fields = ["content"]
            engine.search_fields = ["content"]
            engine.max_results = 10
            engine.index_name = "test"

            with loguru_caplog_full.at_level("DEBUG"):
                result = getattr(engine, method)(arg)

            assert result == []
            for encoding in _all_encodings_of(_LEAKED_PASSWORD):
                assert encoding not in loguru_caplog_full.text, (
                    f"password leaked via {method}"
                )


# ── Paperless engine tests ───────────────────────────────────────────


def _paperless_engine():
    """Build a minimal PaperlessSearchEngine for testing."""
    from local_deep_research.web_search_engines.engines import (
        search_engine_paperless as mod,
    )

    engine = mod.PaperlessSearchEngine.__new__(mod.PaperlessSearchEngine)
    engine.api_token = _LEAKED_TOKEN
    engine.api_url = "http://localhost:8000"
    engine.headers = {"Authorization": f"Token {_LEAKED_TOKEN}"}
    engine.timeout = 30
    engine.verify_ssl = True
    engine.max_results = 10
    engine.llm = None
    engine.rate_tracker = _stub_rate_tracker()
    engine.engine_type = "test_engine"
    return mod, engine


class TestPaperlessEngineKeyLeakage:
    def test_make_request_no_leak(self, loguru_caplog_full):
        """``_make_request`` must not leak the API token."""
        mod, engine = _paperless_engine()

        exc = requests.exceptions.ConnectionError(
            f"Connection to http://localhost:8000?key={_LEAKED_TOKEN} failed"
        )

        with loguru_caplog_full.at_level("DEBUG"):
            with patch.object(mod, "safe_get", side_effect=exc):
                result = engine._make_request("/api/documents/")

        assert result == {}
        for encoding in _all_encodings_of(_LEAKED_TOKEN):
            assert encoding not in loguru_caplog_full.text, (
                f"Paperless _make_request leaked token as {encoding!r}"
            )

    def test_get_previews_no_leak(self, loguru_caplog_full):
        """``_get_previews`` must not leak the API token."""
        mod, engine = _paperless_engine()

        exc = RuntimeError(f"Error: token={_LEAKED_TOKEN}")

        with loguru_caplog_full.at_level("DEBUG"):
            with patch.object(engine, "_multi_pass_search", side_effect=exc):
                result = engine._get_previews("query")

        assert result == []
        for encoding in _all_encodings_of(_LEAKED_TOKEN):
            assert encoding not in loguru_caplog_full.text

    def test_get_full_content_no_leak(self, loguru_caplog_full):
        """``_get_full_content`` must not leak the API token."""
        mod, engine = _paperless_engine()
        engine.include_content = True

        item = {
            "_raw_data": {},
            "snippet": "test snippet",
            "metadata": {"doc_id": "123"},
        }

        exc = RuntimeError(f"Error: token={_LEAKED_TOKEN}")

        with loguru_caplog_full.at_level("DEBUG"):
            with patch.object(engine, "_make_request", side_effect=exc):
                result = engine._get_full_content([item])

        assert len(result) == 1
        for encoding in _all_encodings_of(_LEAKED_TOKEN):
            assert encoding not in loguru_caplog_full.text

    def test_run_no_leak(self, loguru_caplog_full):
        """``run()`` must not leak the API token."""
        mod, engine = _paperless_engine()

        exc = RuntimeError(f"Error: token={_LEAKED_TOKEN}")

        with loguru_caplog_full.at_level("DEBUG"):
            with patch.object(engine, "_get_previews", side_effect=exc):
                result = engine.run("query")

        assert result == []
        for encoding in _all_encodings_of(_LEAKED_TOKEN):
            assert encoding not in loguru_caplog_full.text

    def test_test_connection_no_leak(self, loguru_caplog_full):
        """``test_connection`` must not leak the API token."""
        mod, engine = _paperless_engine()

        exc = RuntimeError(f"Error: token={_LEAKED_TOKEN}")

        with loguru_caplog_full.at_level("DEBUG"):
            with patch.object(engine, "_make_request", side_effect=exc):
                result = engine.test_connection()

        assert result is False
        for encoding in _all_encodings_of(_LEAKED_TOKEN):
            assert encoding not in loguru_caplog_full.text


# ── Semantic Scholar tests ───────────────────────────────────────────


def _semantic_scholar_engine():
    """Build a minimal SemanticScholarSearchEngine for testing."""
    from local_deep_research.web_search_engines.engines import (
        search_engine_semantic_scholar as mod,
    )

    engine = mod.SemanticScholarSearchEngine.__new__(
        mod.SemanticScholarSearchEngine
    )
    engine.api_key = _LEAKED_KEY
    engine.engine_type = "test_engine"
    engine.rate_tracker = _stub_rate_tracker()
    engine.session = Mock()
    engine.search_url = "https://api.semanticscholar.org/graph/v1/paper/search"
    engine.paper_details_url = "https://api.semanticscholar.org/graph/v1/paper"
    engine.get_abstracts = True
    engine.get_references = False
    engine.get_citations = False
    engine.get_embeddings = False
    engine.get_tldr = True
    engine.citation_limit = 10
    engine.reference_limit = 10
    engine.max_results = 10
    engine.optimize_queries = True
    engine.llm = None
    return mod, engine


class TestSemanticScholarKeyLeakage:
    def test_direct_search_no_leak(self, loguru_caplog_full):
        """``_direct_search`` must not leak the API key."""
        mod, engine = _semantic_scholar_engine()

        exc = requests.exceptions.ConnectionError(
            f"Connection to https://api.semanticscholar.org?key={_LEAKED_KEY}"
        )
        engine.session.get = Mock(side_effect=exc)

        with loguru_caplog_full.at_level("DEBUG"):
            result = engine._direct_search("test query")

        assert result == []
        for encoding in _all_encodings_of(_LEAKED_KEY):
            assert encoding not in loguru_caplog_full.text

    def test_get_paper_details_no_leak(self, loguru_caplog_full):
        """``_get_paper_details`` must not leak the API key."""
        mod, engine = _semantic_scholar_engine()

        exc = requests.exceptions.ConnectionError(f"Error: key={_LEAKED_KEY}")
        engine.session.get = Mock(side_effect=exc)

        with loguru_caplog_full.at_level("DEBUG"):
            result = engine._get_paper_details("paper123")

        assert result == {}
        for encoding in _all_encodings_of(_LEAKED_KEY):
            assert encoding not in loguru_caplog_full.text

    def test_optimize_query_no_leak(self, loguru_caplog_full):
        """``_optimize_query`` must not leak the API key."""
        mod, engine = _semantic_scholar_engine()
        engine.llm = Mock()
        engine.llm.invoke = Mock(
            side_effect=RuntimeError(f"LLM error key={_LEAKED_KEY}")
        )

        with loguru_caplog_full.at_level("DEBUG"):
            result = engine._optimize_query("test query")

        assert result == "test query"
        for encoding in _all_encodings_of(_LEAKED_KEY):
            assert encoding not in loguru_caplog_full.text

    def test_close_no_leak(self, loguru_caplog_full):
        """``close()`` must not leak the API key."""
        mod, engine = _semantic_scholar_engine()

        engine.session = Mock()
        engine.session.close = Mock(
            side_effect=RuntimeError(f"Error: key={_LEAKED_KEY}")
        )

        with loguru_caplog_full.at_level("DEBUG"):
            engine.close()

        for encoding in _all_encodings_of(_LEAKED_KEY):
            assert encoding not in loguru_caplog_full.text


# ── Brave engine tests ───────────────────────────────────────────────


def _brave_engine():
    """Build a minimal BraveSearchEngine for testing."""
    from local_deep_research.web_search_engines.engines import (
        search_engine_brave as mod,
    )

    engine = mod.BraveSearchEngine.__new__(mod.BraveSearchEngine)
    engine._brave_api_key = _LEAKED_KEY
    engine.engine_type = "test_engine"
    engine.rate_tracker = _stub_rate_tracker()
    engine.engine = Mock()
    return mod, engine


class TestBraveEngineKeyLeakage:
    def test_get_previews_no_leak(self, loguru_caplog_full):
        """``_get_previews`` must not leak the API key."""
        mod, engine = _brave_engine()

        exc = RuntimeError(f"Brave API error: key={_LEAKED_KEY}")
        engine.engine.run = Mock(side_effect=exc)

        # Mock _raise_if_rate_limit to re-raise non-rate-limit exceptions
        engine._raise_if_rate_limit = Mock()

        with loguru_caplog_full.at_level("DEBUG"):
            result = engine._get_previews("test query")

        assert result == []
        for encoding in _all_encodings_of(_LEAKED_KEY):
            assert encoding not in loguru_caplog_full.text

    def test_json_parse_error_no_leak(self, loguru_caplog_full):
        """JSON parse error in ``_get_previews`` must not leak the API key."""
        mod, engine = _brave_engine()

        # Return a non-JSON string to trigger JSONDecodeError path
        engine.engine.run = Mock(return_value="not json {{{}}}")

        with loguru_caplog_full.at_level("DEBUG"):
            result = engine._get_previews("test query")

        assert result == []


# ── NASA ADS tests ───────────────────────────────────────────────────


def _nasa_ads_engine():
    """Build a minimal NasaAdsSearchEngine for testing."""
    from local_deep_research.web_search_engines.engines import (
        search_engine_nasa_ads as mod,
    )

    engine = mod.NasaAdsSearchEngine.__new__(mod.NasaAdsSearchEngine)
    engine.api_key = _LEAKED_KEY
    engine.engine_type = "test_engine"
    engine.rate_tracker = _stub_rate_tracker()
    return mod, engine


class TestNasaAdsKeyLeakage:
    def test_format_doc_preview_no_leak(self, loguru_caplog_full):
        """``_format_doc_preview`` must not leak the API key."""
        mod, engine = _nasa_ads_engine()

        # Pass a doc that will cause a formatting error
        bad_doc = {"bibcode": None}

        with loguru_caplog_full.at_level("DEBUG"):
            engine._format_doc_preview(bad_doc)

        # Result is None on error
        # The main thing is no key in logs regardless
        for encoding in _all_encodings_of(_LEAKED_KEY):
            assert encoding not in loguru_caplog_full.text


# ── Guardian tests ───────────────────────────────────────────────────


def _guardian_engine():
    """Build a minimal GuardianSearchEngine for testing."""
    from local_deep_research.web_search_engines.engines import (
        search_engine_guardian as mod,
    )

    engine = mod.GuardianSearchEngine.__new__(mod.GuardianSearchEngine)
    engine.api_key = _LEAKED_KEY
    engine.engine_type = "test_engine"
    engine.rate_tracker = _stub_rate_tracker()
    engine.api_url = "https://content.guardianapis.com"
    engine.llm = None
    engine.optimize_queries = True
    return mod, engine


class TestGuardianKeyLeakage:
    def test_optimize_query_no_leak(self, loguru_caplog_full):
        """``_optimize_query_for_guardian`` must not leak the API key."""
        mod, engine = _guardian_engine()
        engine.llm = Mock()
        engine.llm.invoke = Mock(
            side_effect=RuntimeError(f"LLM error key={_LEAKED_KEY}")
        )

        with loguru_caplog_full.at_level("DEBUG"):
            result = engine._optimize_query_for_guardian("test query")

        assert result == "test query"
        for encoding in _all_encodings_of(_LEAKED_KEY):
            assert encoding not in loguru_caplog_full.text

    def test_run_catch_all_no_leak(self, loguru_caplog_full):
        """The catch-all in ``run()`` must not leak the API key."""
        mod, engine = _guardian_engine()

        exc = RuntimeError(f"Unexpected error: key={_LEAKED_KEY}")

        with loguru_caplog_full.at_level("DEBUG"):
            with patch.object(engine, "_get_previews", side_effect=exc):
                engine._original_date_params = {
                    "from_date": None,
                    "to_date": None,
                }
                engine.from_date = None
                engine.to_date = None
                engine.programmatic_mode = False
                result = engine.run("test query")

        assert result == []
        for encoding in _all_encodings_of(_LEAKED_KEY):
            assert encoding not in loguru_caplog_full.text
