"""
Tests for RSS feed route (/feed.xml)

Tests cover:
- Feed route returns valid XML
- Correct Content-Type header
- Caching behavior (server-side and client-side)
- ETag generation and validation
- Only published notes included
- Feed item limit configuration
- Cache expiration behavior
"""

import pytest
import time
from xml.etree import ElementTree as ET

from starpunk import create_app
from starpunk.notes import create_note


@pytest.fixture
def app(tmp_path):
    """Create test application"""
    test_data_dir = tmp_path / "data"
    test_data_dir.mkdir(parents=True, exist_ok=True)

    test_config = {
        "TESTING": True,
        "DATABASE_PATH": test_data_dir / "starpunk.db",
        "DATA_PATH": test_data_dir,
        "NOTES_PATH": test_data_dir / "notes",
        "SESSION_SECRET": "test-secret-key",
        "ADMIN_ME": "https://test.example.com",
        "SITE_URL": "https://example.com",
        "SITE_NAME": "Test Blog",
        "SITE_DESCRIPTION": "A test blog",
        "DEV_MODE": False,
        "FEED_MAX_ITEMS": 50,
        "FEED_CACHE_SECONDS": 2,  # Short cache for testing
    }
    app = create_app(config=test_config)
    yield app


@pytest.fixture
def client(app):
    """Test client for making requests"""
    return app.test_client()


@pytest.fixture(autouse=True)
def clear_feed_cache():
    """Clear feed cache before each test"""
    from starpunk.routes import public
    public._feed_cache["notes"] = None
    public._feed_cache["timestamp"] = None
    yield
    # Clear again after test
    public._feed_cache["notes"] = None
    public._feed_cache["timestamp"] = None


@pytest.fixture
def sample_notes(app):
    """Create sample notes (mix of published and drafts)"""
    with app.app_context():
        notes = []
        for i in range(10):
            note = create_note(
                content=f"# Test Note {i}\n\nContent for note {i}.",
                published=(i < 7),  # First 7 published, last 3 drafts
            )
            notes.append(note)
        return notes


class TestFeedRoute:
    """Test /feed.xml route"""

    def test_feed_route_exists(self, client):
        """Test /feed.xml route exists and returns 200"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

    def test_feed_route_returns_xml(self, client):
        """Test /feed.xml returns valid XML"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

        # Should be valid XML
        root = ET.fromstring(response.data)
        assert root.tag == "rss"

    def test_feed_route_content_type(self, client):
        """Test /feed.xml has correct Content-Type header"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

        # Should have RSS content type
        assert "application/rss+xml" in response.content_type
        assert "charset=utf-8" in response.content_type.lower()

    def test_feed_route_cache_control_header(self, client, app):
        """Test /feed.xml has Cache-Control header"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

        # Should have Cache-Control header
        assert "Cache-Control" in response.headers
        assert "public" in response.headers["Cache-Control"]

        # Should include max-age matching config
        cache_seconds = app.config.get("FEED_CACHE_SECONDS", 300)
        assert f"max-age={cache_seconds}" in response.headers["Cache-Control"]

    def test_feed_route_caching(self, client):
        """Test /feed.xml uses cached response (with ETag)"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

        # Cached responses include ETags for conditional requests
        # (Phase 3 caching was added, replacing streaming for better performance)
        assert "ETag" in response.headers

        # Should also have cache control
        assert "Cache-Control" in response.headers


class TestFeedContent:
    """Test feed content and structure"""

    def test_feed_only_published_notes(self, client, sample_notes):
        """Test feed only includes published notes"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

        root = ET.fromstring(response.data)
        channel = root.find("channel")
        items = channel.findall("item")

        # Should have 7 items (only published notes)
        assert len(items) == 7

        # Check that draft notes don't appear in feed
        feed_text = response.data.decode("utf-8")
        assert "Test Note 0" in feed_text  # Published
        assert "Test Note 6" in feed_text  # Published
        assert "Test Note 7" not in feed_text  # Draft
        assert "Test Note 8" not in feed_text  # Draft
        assert "Test Note 9" not in feed_text  # Draft

    def test_feed_respects_limit_config(self, client, app):
        """Test feed respects FEED_MAX_ITEMS configuration"""
        # Create more notes than limit
        with app.app_context():
            for i in range(60):
                create_note(content=f"Note {i}", published=True)

        response = client.get("/feed.xml")
        assert response.status_code == 200

        root = ET.fromstring(response.data)
        channel = root.find("channel")
        items = channel.findall("item")

        # Should respect configured limit (50)
        max_items = app.config.get("FEED_MAX_ITEMS", 50)
        assert len(items) <= max_items

    def test_feed_empty_when_no_notes(self, client):
        """Test feed with no published notes"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

        root = ET.fromstring(response.data)
        channel = root.find("channel")
        items = channel.findall("item")

        # Should have no items but still valid feed
        assert len(items) == 0

        # Channel should still have required elements
        assert channel.find("title") is not None
        assert channel.find("link") is not None

    def test_feed_has_required_channel_elements(self, client, app):
        """Test feed has all required RSS channel elements"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

        root = ET.fromstring(response.data)
        channel = root.find("channel")

        # Check required elements
        assert channel.find("title").text == app.config["SITE_NAME"]
        # Channel may have multiple links (alternate and self), just check links exist
        assert len(channel.findall("link")) > 0
        assert channel.find("description") is not None
        assert channel.find("language") is not None

    def test_feed_items_have_required_elements(self, client, sample_notes):
        """Test feed items have all required RSS item elements"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

        root = ET.fromstring(response.data)
        channel = root.find("channel")
        items = channel.findall("item")

        # Check first item has required elements
        if len(items) > 0:
            item = items[0]
            assert item.find("title") is not None
            assert item.find("link") is not None
            assert item.find("guid") is not None
            assert item.find("pubDate") is not None
            assert item.find("description") is not None

    def test_feed_item_links_are_absolute(self, client, sample_notes, app):
        """Test feed item links are absolute URLs"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

        root = ET.fromstring(response.data)
        channel = root.find("channel")
        items = channel.findall("item")

        if len(items) > 0:
            link = items[0].find("link").text
            # Should start with site URL
            assert link.startswith(app.config["SITE_URL"])
            # Should be full URL, not relative path
            assert link.startswith("http")


class TestFeedCaching:
    """Test feed caching behavior"""

    def test_feed_caches_note_list(self, client, sample_notes):
        """Test feed caches note list on server side (not full XML)"""
        # First request - generates and caches note list
        response1 = client.get("/feed.xml")

        # Second request - should use cached note list (but still stream XML)
        response2 = client.get("/feed.xml")

        # Content should be identical (same notes)
        assert response1.data == response2.data

        # Note: We don't use ETags anymore due to streaming optimization
        # The note list is cached to avoid repeated DB queries,
        # but XML is still streamed for memory efficiency

    def test_feed_cache_expires(self, client, sample_notes, app):
        """Test feed note list cache expires after configured duration"""
        # First request
        response1 = client.get("/feed.xml")
        content1 = response1.data

        # Wait for cache to expire (cache is 2 seconds in test config)
        time.sleep(3)

        # Create new note (changes feed content)
        with app.app_context():
            create_note(content="New note after cache expiry", published=True)

        # Second request (cache should be expired and regenerated with new note)
        response2 = client.get("/feed.xml")
        content2 = response2.data

        # Content should be different (new note added)
        assert content1 != content2
        assert b"New note after cache expiry" in content2

    def test_feed_content_changes_with_new_notes(self, client, app):
        """Test feed content changes when notes are added"""
        # First request
        response1 = client.get("/feed.xml")
        content1 = response1.data

        # Wait for cache expiry
        time.sleep(3)

        # Add new note
        with app.app_context():
            create_note(content="New note changes content", published=True)

        # Second request
        response2 = client.get("/feed.xml")
        content2 = response2.data

        # Content should be different (new note added)
        assert content1 != content2
        assert b"New note changes content" in content2

    def test_feed_cache_consistent_within_window(self, client, sample_notes):
        """Test cache returns consistent content within cache window"""
        # Multiple requests within cache window
        responses = []
        for _ in range(5):
            response = client.get("/feed.xml")
            responses.append(response)

        # All responses should be identical (same cached note list)
        first_content = responses[0].data

        for response in responses[1:]:
            assert response.data == first_content


class TestFeedEdgeCases:
    """Test edge cases for feed route"""

    def test_feed_with_special_characters_in_content(self, client, app):
        """Test feed handles special characters correctly"""
        with app.app_context():
            create_note(
                content="# Test & Special <Characters>\n\n'Quotes' and \"doubles\".",
                published=True,
            )

        response = client.get("/feed.xml")
        assert response.status_code == 200

        # Should produce valid XML despite special characters
        root = ET.fromstring(response.data)
        assert root is not None

    def test_feed_with_unicode_content(self, client, app):
        """Test feed handles Unicode content"""
        with app.app_context():
            create_note(content="# Test Unicode 你好 🚀\n\nEmojis and ümlauts.", published=True)

        response = client.get("/feed.xml")
        assert response.status_code == 200

        # Should handle UTF-8 correctly
        root = ET.fromstring(response.data)
        assert root is not None

    def test_feed_with_very_long_note(self, client, app):
        """Test feed handles very long note content"""
        with app.app_context():
            long_content = "# Long Note\n\n" + ("This is a very long paragraph. " * 100)
            create_note(content=long_content, published=True)

        response = client.get("/feed.xml")
        assert response.status_code == 200

        # Should include full content (no truncation by default)
        root = ET.fromstring(response.data)
        assert root is not None


class TestFeedConfiguration:
    """Test feed configuration options"""

    def test_feed_uses_site_name_from_config(self, client, app):
        """Test feed uses SITE_NAME from config"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

        root = ET.fromstring(response.data)
        channel = root.find("channel")
        title = channel.find("title").text

        assert title == app.config["SITE_NAME"]

    def test_feed_uses_site_url_from_config(self, client, app):
        """Test feed uses SITE_URL from config"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

        # Site URL should appear somewhere in the feed
        feed_text = response.data.decode("utf-8")
        assert app.config["SITE_URL"] in feed_text

    def test_feed_uses_site_description_from_config(self, client, app):
        """Test feed uses SITE_DESCRIPTION from config"""
        response = client.get("/feed.xml")
        assert response.status_code == 200

        root = ET.fromstring(response.data)
        channel = root.find("channel")
        description = channel.find("description").text

        assert description == app.config["SITE_DESCRIPTION"]
