"""
Tests for engine.fetch module.
"""

import json
from unittest.mock import MagicMock, patch

from engine.fetch import (
    _fetch_gutenberg,
    fetch_all,
    fetch_feed,
    fetch_poetry,
    load_cache,
    save_cache,
)


class TestFetchFeed:
    """Tests for fetch_feed function."""

    @patch("engine.fetch.urllib.request.urlopen")
    def test_fetch_success(self, mock_urlopen):
        """Successful feed fetch returns parsed feed."""
        mock_response = MagicMock()
        mock_response.read.return_value = b"<rss>test</rss>"
        mock_urlopen.return_value = mock_response

        result = fetch_feed("http://example.com/feed")

        assert result is not None

    @patch("engine.fetch.urllib.request.urlopen")
    def test_fetch_network_error(self, mock_urlopen):
        """Network error returns tuple with None feed."""
        mock_urlopen.side_effect = Exception("Network error")

        url, feed = fetch_feed("http://example.com/feed")

        assert feed is None


class TestFetchAll:
    """Tests for fetch_all function."""

    @patch("engine.fetch.fetch_feed")
    @patch("engine.fetch.strip_tags")
    @patch("engine.fetch.skip")
    @patch("engine.fetch.boot_ln")
    def test_fetch_all_success(self, mock_boot, mock_skip, mock_strip, mock_fetch_feed):
        """Successful fetch returns items."""
        mock_feed = MagicMock()
        mock_feed.bozo = False
        mock_feed.entries = [
            {"title": "Headline 1", "published_parsed": (2024, 1, 1, 12, 0, 0)},
            {"title": "Headline 2", "updated_parsed": (2024, 1, 2, 12, 0, 0)},
        ]
        mock_fetch_feed.return_value = ("http://example.com", mock_feed)
        mock_skip.return_value = False
        mock_strip.side_effect = lambda x: x

        items, linked, failed = fetch_all()

        assert linked > 0
        assert failed == 0

    @patch("engine.fetch.fetch_feed")
    @patch("engine.fetch.boot_ln")
    def test_fetch_all_feed_error(self, mock_boot, mock_fetch_feed):
        """Feed error increments failed count."""
        mock_fetch_feed.return_value = ("http://example.com", None)

        items, linked, failed = fetch_all()

        assert failed > 0

    @patch("engine.fetch.fetch_feed")
    @patch("engine.fetch.strip_tags")
    @patch("engine.fetch.skip")
    @patch("engine.fetch.boot_ln")
    def test_fetch_all_skips_filtered(
        self, mock_boot, mock_skip, mock_strip, mock_fetch_feed
    ):
        """Filtered headlines are skipped."""
        mock_feed = MagicMock()
        mock_feed.bozo = False
        mock_feed.entries = [
            {"title": "Sports scores"},
            {"title": "Valid headline"},
        ]
        mock_fetch_feed.return_value = ("http://example.com", mock_feed)
        mock_skip.side_effect = lambda x: x == "Sports scores"
        mock_strip.side_effect = lambda x: x

        items, linked, failed = fetch_all()

        assert any("Valid headline" in item[0] for item in items)


class TestFetchGutenberg:
    """Tests for _fetch_gutenberg function."""

    @patch("engine.fetch.urllib.request.urlopen")
    def test_gutenberg_success(self, mock_urlopen):
        """Successful gutenberg fetch returns items."""
        text = """Project Gutenberg

*** START OF THE PROJECT GUTENBERG ***
This is a test poem with multiple lines
that should be parsed as a block.

Another stanza with more content here.

*** END OF THE PROJECT GUTENBERG ***
"""
        mock_response = MagicMock()
        mock_response.read.return_value = text.encode("utf-8")
        mock_urlopen.return_value = mock_response

        result = _fetch_gutenberg("http://example.com/test", "Test")

        assert len(result) > 0

    @patch("engine.fetch.urllib.request.urlopen")
    def test_gutenberg_network_error(self, mock_urlopen):
        """Network error returns empty list."""
        mock_urlopen.side_effect = Exception("Network error")

        result = _fetch_gutenberg("http://example.com/test", "Test")

        assert result == []

    @patch("engine.fetch.urllib.request.urlopen")
    def test_gutenberg_skips_short_blocks(self, mock_urlopen):
        """Blocks shorter than 20 chars are skipped."""
        text = """*** START OF THE ***
Short
*** END OF THE ***
"""
        mock_response = MagicMock()
        mock_response.read.return_value = text.encode("utf-8")
        mock_urlopen.return_value = mock_response

        result = _fetch_gutenberg("http://example.com/test", "Test")

        assert result == []

    @patch("engine.fetch.urllib.request.urlopen")
    def test_gutenberg_skips_all_caps_headers(self, mock_urlopen):
        """All-caps lines are skipped as headers."""
        text = """*** START OF THE ***
THIS IS ALL CAPS HEADER
more content here
*** END OF THE ***
"""
        mock_response = MagicMock()
        mock_response.read.return_value = text.encode("utf-8")
        mock_urlopen.return_value = mock_response

        result = _fetch_gutenberg("http://example.com/test", "Test")

        assert len(result) > 0


class TestFetchPoetry:
    """Tests for fetch_poetry function."""

    @patch("engine.fetch._fetch_gutenberg")
    @patch("engine.fetch.boot_ln")
    def test_fetch_poetry_success(self, mock_boot, mock_fetch):
        """Successful poetry fetch returns items."""
        mock_fetch.return_value = [
            ("Stanza 1 content here", "Test", ""),
            ("Stanza 2 content here", "Test", ""),
        ]

        items, linked, failed = fetch_poetry()

        assert linked > 0
        assert failed == 0

    @patch("engine.fetch._fetch_gutenberg")
    @patch("engine.fetch.boot_ln")
    def test_fetch_poetry_failure(self, mock_boot, mock_fetch):
        """Failed fetch increments failed count."""
        mock_fetch.return_value = []

        items, linked, failed = fetch_poetry()

        assert failed > 0


class TestCache:
    """Tests for cache functions."""

    @patch("engine.fetch._cache_path")
    def test_load_cache_success(self, mock_path):
        """Successful cache load returns items."""
        mock_path.return_value.__str__ = MagicMock(return_value="/tmp/cache")
        mock_path.return_value.exists.return_value = True
        mock_path.return_value.read_text.return_value = json.dumps(
            {"items": [("title", "source", "time")]}
        )

        result = load_cache()

        assert result is not None

    @patch("engine.fetch._cache_path")
    def test_load_cache_missing_file(self, mock_path):
        """Missing cache file returns None."""
        mock_path.return_value.exists.return_value = False

        result = load_cache()

        assert result is None

    @patch("engine.fetch._cache_path")
    def test_load_cache_invalid_json(self, mock_path):
        """Invalid JSON returns None."""
        mock_path.return_value.exists.return_value = True
        mock_path.return_value.read_text.side_effect = json.JSONDecodeError("", "", 0)

        result = load_cache()

        assert result is None

    @patch("engine.fetch._cache_path")
    def test_save_cache_success(self, mock_path):
        """Save cache writes to file."""
        mock_path.return_value.__truediv__ = MagicMock(
            return_value=mock_path.return_value
        )

        save_cache([("title", "source", "time")])
