"""
Tests for uncovered code paths in OpenLibrarySearchEngine.

Targets:
- _get_full_content: language/subject/publisher normalization, content building, has_fulltext
- _fetch_work_details: success, validation, error handling
- get_author: success, rate limit, exception
"""

from unittest.mock import Mock, patch

import pytest

from local_deep_research.web_search_engines.engines.search_engine_openlibrary import (
    OpenLibrarySearchEngine,
)
from local_deep_research.web_search_engines.rate_limiting import RateLimitError

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


@pytest.fixture
def engine():
    eng = OpenLibrarySearchEngine(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):
    resp = Mock()
    resp.status_code = status_code
    resp.json.return_value = json_data 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_all_fields(self, engine):
        """Content string includes authors, year, subjects, description."""
        items = [
            {
                "id": "/works/OL27448W",
                "title": "The Lord of the Rings",
                "authors": ["J.R.R. Tolkien"],
                "first_publish_year": 1954,
                "edition_count": 200,
                "has_fulltext": True,
                "_raw": {
                    "language": ["eng", "fre"],
                    "subject": ["Fantasy", "Adventure", "Epic"],
                    "publisher": ["HarperCollins", "Houghton Mifflin"],
                },
            }
        ]

        with patch.object(
            engine,
            "_fetch_work_details",
            return_value={
                "description": "An epic fantasy novel.",
                "excerpts": [{"excerpt": "One ring to rule them all."}],
            },
        ):
            results = engine._get_full_content(items)

        assert len(results) == 1
        content = results[0]["content"]
        assert "Authors: J.R.R. Tolkien" in content
        assert "First published: 1954" in content
        assert "Fantasy" in content
        assert "An epic fantasy novel." in content
        assert "One ring to rule them all." in content
        assert "Full text available" in content
        assert results[0]["languages"] == ["eng", "fre"]
        assert results[0]["publishers"] == ["HarperCollins", "Houghton Mifflin"]
        assert "_raw" not in results[0]

    def test_language_string_normalized_to_list(self, engine):
        """String language values are converted to list."""
        items = [
            {
                "title": "Test",
                "_raw": {"language": "eng", "subject": [], "publisher": []},
            }
        ]

        with patch.object(engine, "_fetch_work_details", return_value=None):
            results = engine._get_full_content(items)
        assert results[0]["languages"] == ["eng"]

    def test_subject_string_normalized_to_list(self, engine):
        """String subject values are converted to list."""
        items = [
            {
                "title": "Test",
                "_raw": {"language": [], "subject": "Science", "publisher": []},
            }
        ]

        with patch.object(engine, "_fetch_work_details", return_value=None):
            results = engine._get_full_content(items)
        assert results[0]["subjects"] == ["Science"]

    def test_publisher_string_normalized_to_list(self, engine):
        """String publisher values are converted to list."""
        items = [
            {
                "title": "Test",
                "_raw": {"language": [], "subject": [], "publisher": "Penguin"},
            }
        ]

        with patch.object(engine, "_fetch_work_details", return_value=None):
            results = engine._get_full_content(items)
        assert results[0]["publishers"] == ["Penguin"]

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

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

    def test_no_fulltext_omitted(self, engine):
        """Content omits fulltext line when has_fulltext is False."""
        items = [
            {
                "title": "Test",
                "has_fulltext": False,
                "_raw": {"language": [], "subject": [], "publisher": []},
            }
        ]

        with patch.object(engine, "_fetch_work_details", return_value=None):
            results = engine._get_full_content(items)
        assert "Full text" not in results[0].get("content", "")

    def test_description_dict_format(self, engine):
        """Handles description in {value: ...} dict format from Works API."""
        items = [
            {
                "id": "/works/OL1W",
                "title": "Test",
                "_raw": {"language": [], "subject": [], "publisher": []},
            }
        ]

        with patch.object(
            engine,
            "_fetch_work_details",
            return_value={"description": {"value": "A dict description."}},
        ):
            results = engine._get_full_content(items)

        assert "A dict description." in results[0]["content"]

    def test_fallback_to_raw_description(self, engine):
        """Falls back to _raw description when Works API has none."""
        items = [
            {
                "id": "/works/OL1W",
                "title": "Test",
                "_raw": {
                    "language": [],
                    "subject": [],
                    "publisher": [],
                    "description": "Raw description.",
                },
            }
        ]

        with patch.object(engine, "_fetch_work_details", return_value={}):
            results = engine._get_full_content(items)

        assert "Raw description." in results[0]["content"]


# ---------------------------------------------------------------------------
# _fetch_work_details
# ---------------------------------------------------------------------------


class TestFetchWorkDetails:
    @patch(f"{MODULE}.safe_get")
    def test_success(self, mock_get, engine):
        """Returns work data on success."""
        work_data = {"description": "A great book.", "excerpts": []}
        mock_get.return_value = _mock_response(200, work_data)

        result = engine._fetch_work_details("/works/OL1234W")

        assert result["description"] == "A great book."

    def test_rejects_empty_key(self, engine):
        """Returns None for empty work_key."""
        assert engine._fetch_work_details("") is None

    def test_rejects_invalid_prefix(self, engine):
        """Returns None for work_key not starting with /works/."""
        assert engine._fetch_work_details("/authors/OL123A") is None
        assert engine._fetch_work_details("//evil.com/x") 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_work_details("/works/OL999W")
        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_work_details("/works/OL999W")
        assert result is None


# ---------------------------------------------------------------------------
# get_author
# ---------------------------------------------------------------------------


class TestGetAuthor:
    @patch(f"{MODULE}.safe_get")
    def test_success(self, mock_get, engine):
        """Returns author dict on success."""
        author = {"name": "Tolkien", "birth_date": "1892"}
        mock_get.return_value = _mock_response(200, author)

        result = engine.get_author("/authors/OL26320A")

        assert result["name"] == "Tolkien"

    @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_author("/authors/OL26320A")

    @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_author("/authors/OL26320A")
        assert result is None
