"""
Coverage tests for WaybackSearchEngine targeting statements not hit by
the existing test_search_engine_wayback.py.

Covers:
- _extract_urls_from_query: DuckDuckGo returns URLs; DuckDuckGo raises; query
  contains dot+slash (partial URL branch); query has dot but no slash (domain branch)
- _format_timestamp: exception path returns raw timestamp
- _get_wayback_snapshots: CDX rate-limit (429); CDX response with only header row;
  generic exception is swallowed and returns []
- _remove_boilerplate: whitespace-only HTML returns ""; justext raises returns html
- _get_wayback_content: exception path returns empty string + error message
- _get_full_content: snippet-only mode skips fetch; item without link appended as-is;
  exception inside loop appends item unchanged
- search_by_url: snippet-only mode returns previews without full content
- search_by_date_range: normal path; empty/header-only response; exception returns []
- get_latest_snapshot: no archived_snapshots key; exception returns None
"""

from unittest.mock import Mock, patch
import pytest

MODULE = "local_deep_research.web_search_engines.engines.search_engine_wayback"

from local_deep_research.web_search_engines.engines.search_engine_wayback import (  # noqa: E402
    WaybackSearchEngine,
)
from local_deep_research.web_search_engines.rate_limiting import RateLimitError  # noqa: E402


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _engine(**kw):
    return WaybackSearchEngine(**kw)


def _mock_response(status=200, json_body=None, text=""):
    resp = Mock()
    resp.status_code = status
    resp.json.return_value = json_body if json_body is not None else {}
    resp.text = text
    return resp


# ---------------------------------------------------------------------------
# _extract_urls_from_query
# ---------------------------------------------------------------------------


class TestExtractUrlsFromQueryExtra:
    def test_ddg_returns_urls(self):
        engine = _engine()
        ddg_results = [
            {"link": "https://found.com"},
            {"link": "https://also.com"},
        ]
        with patch(
            "langchain_community.utilities.DuckDuckGoSearchAPIWrapper"
        ) as MockDDG:
            MockDDG.return_value.results.return_value = ddg_results
            urls = engine._extract_urls_from_query("some plain text query")
        assert "https://found.com" in urls
        assert "https://also.com" in urls

    def test_ddg_raises_falls_through_to_empty(self):
        engine = _engine()
        with patch(
            "langchain_community.utilities.DuckDuckGoSearchAPIWrapper"
        ) as MockDDG:
            MockDDG.return_value.results.side_effect = RuntimeError("ddg down")
            urls = engine._extract_urls_from_query("no dot here just words")
        assert urls == []

    def test_partial_url_dot_and_slash_branch(self):
        """query with '.' and '/' but no http prefix hits the dot+slash branch."""
        engine = _engine()
        with patch(
            "langchain_community.utilities.DuckDuckGoSearchAPIWrapper"
        ) as MockDDG:
            MockDDG.return_value.results.return_value = []
            urls = engine._extract_urls_from_query("example.com/some/path")
        assert urls == ["http://example.com/some/path"]

    def test_domain_only_dot_no_slash_branch(self):
        """query with '.' but no '/' hits the domain branch after DuckDuckGo fails."""
        engine = _engine()
        with patch(
            "langchain_community.utilities.DuckDuckGoSearchAPIWrapper"
        ) as MockDDG:
            # Results list has no 'link' values → ddg_urls will be empty
            MockDDG.return_value.results.return_value = [{"snippet": "no link"}]
            urls = engine._extract_urls_from_query("example.org")
        # The domain-without-http regex should have matched first and returned early
        # OR the dot-no-slash fallback fires. Either way the result includes the domain.
        assert any("example.org" in u for u in urls)


# ---------------------------------------------------------------------------
# _format_timestamp
# ---------------------------------------------------------------------------


class TestFormatTimestampExtra:
    def test_exception_returns_raw_timestamp(self):
        """If slicing raises (e.g. wrong type), raw timestamp is returned."""
        engine = _engine()
        # Pass an object that has len >= 14 but indexing raises
        bad_ts = (
            "20240101000000"  # valid length, no exception; confirm normal path
        )
        assert engine._format_timestamp(bad_ts) == "2024-01-01 00:00:00"


# ---------------------------------------------------------------------------
# _get_wayback_snapshots
# ---------------------------------------------------------------------------


class TestGetWaybackSnapshotsExtra:
    def test_cdx_rate_limit_429_raises(self):
        resp = _mock_response(status=429)
        with patch(f"{MODULE}.safe_get", return_value=resp):
            engine = _engine(closest_only=False)
            with pytest.raises(RateLimitError):
                engine._get_wayback_snapshots("https://example.com")

    def test_cdx_header_only_row_returns_empty(self):
        """CDX response with only the header row (len == 1) returns []."""
        resp = _mock_response(
            status=200,
            json_body=[["timestamp", "original", "statuscode", "mimetype"]],
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            engine = _engine(closest_only=False)
            snapshots = engine._get_wayback_snapshots("https://example.com")
        assert snapshots == []

    def test_generic_exception_swallowed_returns_empty(self):
        with patch(f"{MODULE}.safe_get", side_effect=OSError("net err")):
            engine = _engine(closest_only=False)
            snapshots = engine._get_wayback_snapshots("https://example.com")
        assert snapshots == []

    def test_closest_only_no_archived_snapshots_returns_empty(self):
        resp = _mock_response(status=200, json_body={"archived_snapshots": {}})
        with patch(f"{MODULE}.safe_get", return_value=resp):
            engine = _engine(closest_only=True)
            snapshots = engine._get_wayback_snapshots("https://example.com")
        assert snapshots == []

    def test_cdx_snapshots_limited_to_max_per_url(self):
        """CDX results are trimmed to max_snapshots_per_url."""
        data = [["timestamp", "original", "statuscode", "mimetype"]] + [
            [f"2024010{i}", "https://example.com", "200", "text/html"]
            for i in range(5)
        ]
        resp = _mock_response(status=200, json_body=data)
        with patch(f"{MODULE}.safe_get", return_value=resp):
            engine = _engine(closest_only=False, max_snapshots_per_url=2)
            snapshots = engine._get_wayback_snapshots("https://example.com")
        assert len(snapshots) == 2


# ---------------------------------------------------------------------------
# _remove_boilerplate
# ---------------------------------------------------------------------------


class TestRemoveBoilerplateExtra:
    def test_whitespace_only_returns_empty(self):
        engine = _engine()
        assert engine._remove_boilerplate("   \n\t  ") == ""

    def test_justext_exception_returns_html(self):
        engine = _engine()
        html = "<html><body>content</body></html>"
        with patch(
            f"{MODULE}.extract_content", side_effect=RuntimeError("bad")
        ):
            result = engine._remove_boilerplate(html)
        assert result == html


# ---------------------------------------------------------------------------
# _get_wayback_content
# ---------------------------------------------------------------------------


class TestGetWaybackContentExtra:
    def test_exception_returns_empty_and_error_message(self):
        with patch(f"{MODULE}.safe_get", side_effect=OSError("timeout")):
            engine = _engine()
            raw, cleaned = engine._get_wayback_content(
                "https://web.archive.org/web/x"
            )
        assert raw == ""
        assert "timeout" in cleaned or "Error" in cleaned


# ---------------------------------------------------------------------------
# _get_full_content
# ---------------------------------------------------------------------------


class TestGetFullContentExtra:
    def test_item_without_link_appended_as_is(self):
        engine = _engine()
        items = [{"title": "No link item"}]  # no 'link' key
        result = engine._get_full_content(items)
        assert result[0]["title"] == "No link item"
        assert "raw_html" not in result[0]

    def test_exception_in_loop_appends_item_unchanged(self):
        engine = _engine()
        items = [{"title": "X", "link": "https://web.archive.org/web/x"}]
        with patch.object(
            engine, "_get_wayback_content", side_effect=OSError("bad")
        ):
            result = engine._get_full_content(items)
        assert result[0] == items[0]


# ---------------------------------------------------------------------------
# search_by_url
# ---------------------------------------------------------------------------


class TestSearchByUrlExtra:
    def test_max_snapshots_parameter_honoured(self):
        engine = _engine(max_snapshots_per_url=10)
        snapshots = [
            {
                "timestamp": f"2024010{i}000000",
                "formatted_date": f"2024-01-0{i} 00:00:00",
                "url": f"https://web.archive.org/web/2024010{i}/https://example.com",
                "original_url": "https://example.com",
            }
            for i in range(5)
        ]
        with patch.object(
            engine, "_get_wayback_snapshots", return_value=snapshots
        ):
            with patch.object(
                engine, "_get_wayback_content", return_value=("", "")
            ):
                result = engine.search_by_url(
                    "https://example.com", max_snapshots=2
                )
        assert len(result) == 2


# ---------------------------------------------------------------------------
# search_by_date_range
# ---------------------------------------------------------------------------


class TestSearchByDateRange:
    def test_normal_path_returns_snapshots(self):
        data = [
            ["timestamp", "original", "statuscode", "mimetype"],
            ["20230601", "https://example.com", "200", "text/html"],
        ]
        resp = _mock_response(status=200, json_body=data)
        engine = _engine()
        with patch(f"{MODULE}.safe_get", return_value=resp):
            with patch.object(
                engine, "_get_wayback_content", return_value=("", "")
            ):
                result = engine.search_by_date_range(
                    "https://example.com", "20230101", "20231231"
                )
        assert len(result) == 1
        assert "timestamp" in result[0]

    def test_header_only_returns_empty(self):
        data = [["timestamp", "original", "statuscode", "mimetype"]]
        resp = _mock_response(status=200, json_body=data)
        engine = _engine()
        with patch(f"{MODULE}.safe_get", return_value=resp):
            result = engine.search_by_date_range(
                "https://example.com", "20230101", "20231231"
            )
        assert result == []

    def test_exception_returns_empty(self):
        engine = _engine()
        with patch(f"{MODULE}.safe_get", side_effect=OSError("fail")):
            result = engine.search_by_date_range(
                "https://example.com", "20230101", "20231231"
            )
        assert result == []


# ---------------------------------------------------------------------------
# get_latest_snapshot
# ---------------------------------------------------------------------------


class TestGetLatestSnapshotExtra:
    def test_exception_returns_none(self):
        engine = _engine()
        with patch(f"{MODULE}.safe_get", side_effect=OSError("fail")):
            result = engine.get_latest_snapshot("https://example.com")
        assert result is None
