"""
Tests for uncovered code paths in GutenbergSearchEngine.

Targets:
- _get_full_content: subjects, bookshelves, translators, content building, book text
- _fetch_book_text: success, boilerplate stripping, truncation, error handling
- get_book: success, rate limit, exception
- search_by_topic: topic switching, cleanup on exception
"""

from unittest.mock import Mock, patch

import pytest

from local_deep_research.web_search_engines.engines.search_engine_gutenberg import (
    GutenbergSearchEngine,
)
from local_deep_research.web_search_engines.rate_limiting import RateLimitError

MODULE = (
    "local_deep_research.web_search_engines.engines.search_engine_gutenberg"
)


@pytest.fixture
def engine():
    eng = GutenbergSearchEngine(max_results=5)
    eng.rate_tracker = Mock()
    eng.rate_tracker.apply_rate_limit.return_value = 0
    return eng


def _mock_response(status_code=200, json_data=None, text=None):
    resp = Mock()
    resp.status_code = status_code
    resp.json.return_value = json_data or {}
    resp.text = text or ""
    resp.raise_for_status = Mock()
    if status_code >= 400:
        resp.raise_for_status.side_effect = Exception(f"HTTP {status_code}")
    return resp


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


class TestGetFullContent:
    def test_builds_content_with_book_text(self, engine):
        """Content includes metadata header + actual book text when available."""
        items = [
            {
                "title": "Alice in Wonderland",
                "authors": ["Lewis Carroll"],
                "download_count": 50000,
                "read_url": "https://gutenberg.org/ebooks/11",
                "_raw": {
                    "subjects": ["Fantasy", "Children's literature"],
                    "bookshelves": ["Children's Fiction"],
                    "translators": [{"name": "Someone", "birth_year": 1900}],
                    "formats": {
                        "text/plain; charset=utf-8": "https://www.gutenberg.org/files/11/11-0.txt"
                    },
                },
            }
        ]

        with patch.object(
            engine, "_fetch_book_text", return_value="Alice was beginning..."
        ) as mock_fetch:
            results = engine._get_full_content(items)
            mock_fetch.assert_called_once_with(
                "https://www.gutenberg.org/files/11/11-0.txt"
            )

        assert len(results) == 1
        content = results[0]["content"]
        assert "Authors: Lewis Carroll" in content
        assert "Subjects: Fantasy, Children's literature" in content
        assert "Alice was beginning..." in content
        # Metadata-only fields should NOT appear when text is available
        assert "Bookshelves:" not in content
        assert "Downloads:" not in content
        assert "_raw" not in results[0]

    def test_metadata_fallback_when_no_text(self, engine):
        """Falls back to metadata when book text is unavailable."""
        items = [
            {
                "title": "Alice in Wonderland",
                "authors": ["Lewis Carroll"],
                "download_count": 50000,
                "read_url": "https://gutenberg.org/ebooks/11",
                "_raw": {
                    "subjects": ["Fantasy"],
                    "bookshelves": ["Children's Fiction"],
                    "translators": [],
                },
            }
        ]

        results = engine._get_full_content(items)

        content = results[0]["content"]
        assert "Bookshelves: Children's Fiction" in content
        assert "Downloads: 50000" in content
        assert "Read online:" in content

    def test_rejects_non_gutenberg_url(self, engine):
        """Skips text URLs not from gutenberg.org."""
        items = [
            {
                "title": "Test",
                "authors": [],
                "_raw": {
                    "subjects": [],
                    "bookshelves": [],
                    "translators": [],
                    "formats": {
                        "text/plain; charset=utf-8": "https://evil.com/malicious.txt"
                    },
                },
            }
        ]

        with patch.object(engine, "_fetch_book_text") as mock_fetch:
            engine._get_full_content(items)
            mock_fetch.assert_not_called()

    def test_without_raw(self, engine):
        """Items without _raw pass through."""
        items = [{"title": "Minimal"}]

        results = engine._get_full_content(items)
        assert len(results) == 1
        assert "_raw" not in results[0]

    def test_empty_raw_fields(self, engine):
        """Handles empty subjects, bookshelves, translators."""
        items = [
            {
                "title": "Test",
                "_raw": {
                    "subjects": [],
                    "bookshelves": [],
                    "translators": [],
                },
            }
        ]

        results = engine._get_full_content(items)
        content = results[0].get("content", "")
        assert "Subjects:" not in content
        assert "Bookshelves:" not in content


# ---------------------------------------------------------------------------
# _fetch_book_text
# ---------------------------------------------------------------------------


class TestFetchBookText:
    @patch(f"{MODULE}.safe_get")
    def test_strips_start_and_end_markers(self, mock_get, engine):
        """Strips Project Gutenberg header and footer boilerplate."""
        raw_text = (
            "The Project Gutenberg EBook of Test\n"
            "*** START OF THE PROJECT GUTENBERG EBOOK TEST ***\n"
            "Actual book content here.\n"
            "*** END OF THE PROJECT GUTENBERG EBOOK TEST ***\n"
            "End of Project Gutenberg..."
        )
        mock_get.return_value = _mock_response(200, text=raw_text)

        result = engine._fetch_book_text(
            "https://www.gutenberg.org/files/1/1-0.txt"
        )

        assert result == "Actual book content here."

    @patch(f"{MODULE}.safe_get")
    def test_handles_variant_markers(self, mock_get, engine):
        """Handles 'THIS' variant of the start/end markers."""
        raw_text = (
            "Header\n"
            "*** START OF THIS PROJECT GUTENBERG EBOOK X ***\n"
            "The real content.\n"
            "*** END OF THIS PROJECT GUTENBERG EBOOK X ***\n"
        )
        mock_get.return_value = _mock_response(200, text=raw_text)

        result = engine._fetch_book_text(
            "https://www.gutenberg.org/files/1/1-0.txt"
        )

        assert result == "The real content."

    @patch(f"{MODULE}.safe_get")
    def test_truncates_to_max_content_chars(self, mock_get, engine):
        """Truncates text to max_content_chars."""
        engine.max_content_chars = 100
        body = "A" * 200
        raw_text = f"*** START OF THE PROJECT GUTENBERG EBOOK T ***\n{body}\n*** END OF THE PROJECT GUTENBERG EBOOK T ***"
        mock_get.return_value = _mock_response(200, text=raw_text)

        result = engine._fetch_book_text(
            "https://www.gutenberg.org/files/1/1-0.txt"
        )

        assert result.startswith("A" * 100)
        assert "[... truncated ...]" in result

    @patch(f"{MODULE}.safe_get")
    def test_empty_response_returns_none(self, mock_get, engine):
        """Returns None for empty response text."""
        mock_get.return_value = _mock_response(200, text="")

        result = engine._fetch_book_text(
            "https://www.gutenberg.org/files/1/1-0.txt"
        )
        assert result is None

    @patch(f"{MODULE}.safe_get")
    def test_http_error_returns_none(self, mock_get, engine):
        """Returns None on HTTP errors."""
        mock_get.return_value = _mock_response(404)

        result = engine._fetch_book_text(
            "https://www.gutenberg.org/files/1/1-0.txt"
        )
        assert result is None

    @patch(f"{MODULE}.safe_get")
    def test_network_error_returns_none(self, mock_get, engine):
        """Returns None on network errors."""
        mock_get.side_effect = ConnectionError("fail")

        result = engine._fetch_book_text(
            "https://www.gutenberg.org/files/1/1-0.txt"
        )
        assert result is None


# ---------------------------------------------------------------------------
# get_book
# ---------------------------------------------------------------------------


class TestGetBook:
    @patch(f"{MODULE}.safe_get")
    def test_success(self, mock_get, engine):
        """Returns book dict on success."""
        book = {"id": 11, "title": "Alice in Wonderland"}
        mock_get.return_value = _mock_response(200, book)

        result = engine.get_book(11)

        assert result["id"] == 11
        assert result["title"] == "Alice in Wonderland"

    @patch(f"{MODULE}.safe_get")
    def test_rate_limit_propagates(self, mock_get, engine):
        """RateLimitError is re-raised."""
        mock_get.return_value = _mock_response(429)
        engine._raise_if_rate_limit = Mock(side_effect=RateLimitError("429"))

        with pytest.raises(RateLimitError):
            engine.get_book(11)

    @patch(f"{MODULE}.safe_get")
    def test_exception_returns_none(self, mock_get, engine):
        """Other exceptions return None."""
        mock_get.side_effect = ConnectionError("fail")

        result = engine.get_book(11)
        assert result is None


# ---------------------------------------------------------------------------
# search_by_topic
# ---------------------------------------------------------------------------


class TestSearchByTopic:
    def test_sets_topic_and_restores(self, engine):
        """Temporarily sets topic filter and restores."""
        engine.topic = None
        with patch.object(engine, "run", return_value=[]) as mock_run:
            engine.search_by_topic("science fiction")

        mock_run.assert_called_once_with("")
        assert engine.topic is None

    def test_restores_on_exception(self, engine):
        """Topic is restored even on exception."""
        engine.topic = "original"
        with patch.object(engine, "run", side_effect=Exception("fail")):
            with pytest.raises(Exception, match="fail"):
                engine.search_by_topic("new topic")

        assert engine.topic == "original"
