import json
import tempfile
import unittest
import xml.etree.ElementTree as ET
from datetime import UTC, datetime, timedelta
from pathlib import Path
from unittest.mock import patch

import publish_clean_feed
from fetch_latest_long_episode import FeedCategory


class LockEvaluationTests(unittest.TestCase):
    def test_active_process_keeps_lock(self) -> None:
        status = publish_clean_feed.evaluate_lock({}, 60.0, True, 7200.0)
        self.assertTrue(status.is_locked)
        self.assertFalse(status.can_replace)
        self.assertEqual(status.reason, "active")

    def test_missing_process_replaces_lock(self) -> None:
        status = publish_clean_feed.evaluate_lock({}, 60.0, False, 7200.0)
        self.assertFalse(status.is_locked)
        self.assertTrue(status.can_replace)
        self.assertEqual(status.reason, "missing_owner")


class StateRecordTests(unittest.TestCase):
    def test_upsert_replaces_existing_guid(self) -> None:
        state = {"episodes": [{"source_guid": "abc", "title": "old"}]}
        publish_clean_feed.upsert_episode_record(state, {"source_guid": "abc", "title": "new"})
        self.assertEqual(state["episodes"], [{"source_guid": "abc", "title": "new"}])

    def test_episode_is_ready_checks_file_presence(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            path = Path(temp_dir) / "episode.mp3"
            path.write_bytes(b"ok")
            record = {"cleaned_mp3_path": str(path)}
            self.assertTrue(publish_clean_feed.episode_is_ready(record))

    def test_load_state_normalizes_legacy_http_public_url(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            state_path = Path(temp_dir) / "processed_episodes.json"
            payload = {
                "version": 1,
                "channel": None,
                "episodes": [
                    {
                        "source_guid": "guid-1",
                        "cleaned_mp3_path": str(Path(temp_dir) / "episode.sans_pub.mp3"),
                        "public_mp3_url": "http://rss.mongueulin.dns.army/episodes/episode.sans_pub.mp3",
                    }
                ],
            }
            state_path.write_text(json.dumps(payload), encoding="utf-8")

            with patch.object(publish_clean_feed, "STATE_FILE", state_path):
                state = publish_clean_feed.load_state()

            self.assertEqual(
                state["episodes"][0]["public_mp3_url"],
                "https://rss.mongueulin.dns.army/episodes/episode.sans_pub.mp3",
            )


class FeedGenerationTests(unittest.TestCase):
    def test_build_feed_tree_contains_expected_metadata(self) -> None:
        state_channel = {
            "title": "Les Grandes Gueules",
            "description": "Description source",
            "link": "https://example.com",
            "language": "fr",
            "copyright": "Copyright",
            "image_url": "https://example.com/image.jpg",
            "author": "RMC",
            "summary": "Resume",
            "explicit": "false",
            "owner_name": "BFM",
            "owner_email": "owner@example.com",
            "categories": [{"primary": "News", "secondary": "Daily News"}],
            "source_rss_url": "https://feeds.simplecast.com/MBdw4Qrw",
        }
        records = [
            {
                "source_guid": "guid-1",
                "title": "Episode 1",
                "description": "<p>desc</p>",
                "pub_date": "Mon, 6 Jul 2026 10:06:24 +0000",
                "cleaned_mp3_path": r"C:\podcasts\episode-1.sans_pub.mp3",
                "cleaned_size_bytes": 12345,
                "cleaned_duration_seconds": 7853.99,
                "public_mp3_url": "http://rss.mongueulin.dns.army/episodes/episode-1.sans_pub.mp3",
            }
        ]

        tree = publish_clean_feed.build_feed_tree(state_channel, records)
        feed_xml = publish_clean_feed.serialize_feed_tree(tree)
        root = tree.getroot()
        channel = root.find("channel")
        self.assertEqual(channel.findtext("title"), "Les Grandes Gueules sans pub")
        self.assertEqual(channel.findtext("link"), "https://rss.mongueulin.dns.army")
        atom_link = channel.find("{http://www.w3.org/2005/Atom}link")
        self.assertEqual(atom_link.attrib["href"], "https://rss.mongueulin.dns.army/podcast.xml")
        item = channel.find("item")
        self.assertEqual(item.findtext("title"), "Episode 1")
        self.assertEqual(item.findtext("guid"), "guid-1-sans-pub")
        self.assertEqual(
            item.find("enclosure").attrib["url"],
            "https://rss.mongueulin.dns.army/episodes/episode-1.sans_pub.mp3",
        )
        self.assertEqual(
            item.findtext("link"),
            "https://rss.mongueulin.dns.army/episodes/episode-1.sans_pub.mp3",
        )
        duration = item.findtext("{http://www.itunes.com/dtds/podcast-1.0.dtd}duration")
        self.assertEqual(duration, "02:10:54")
        self.assertNotIn("http://rss.mongueulin.dns.army", feed_xml)

    def test_build_feed_tree_supports_cache_bust_variant(self) -> None:
        state_channel = {
            "title": "Les Grandes Gueules",
            "description": "Description source",
            "link": "https://example.com",
            "language": "fr",
            "copyright": "Copyright",
            "image_url": "https://example.com/image.jpg",
            "author": "RMC",
            "summary": "Resume",
            "explicit": "false",
            "owner_name": "BFM",
            "owner_email": "owner@example.com",
            "categories": [{"primary": "News", "secondary": "Daily News"}],
            "source_rss_url": "https://feeds.simplecast.com/MBdw4Qrw",
        }
        records = [
            {
                "source_guid": "guid-1",
                "title": "Episode 1",
                "description": "<p>desc</p>",
                "pub_date": "Mon, 6 Jul 2026 10:06:24 +0000",
                "cleaned_mp3_path": r"C:\podcasts\episode-1.sans_pub.mp3",
                "cleaned_size_bytes": 12345,
                "cleaned_duration_seconds": 7853.99,
                "public_mp3_url": "https://rss.mongueulin.dns.army/episodes/episode-1.sans_pub.mp3",
            }
        ]

        tree = publish_clean_feed.build_feed_tree(
            state_channel,
            records,
            feed_url="https://rss.mongueulin.dns.army/podcast-v2.xml",
            guid_suffix="-sans-pub-v2",
        )
        root = tree.getroot()
        channel = root.find("channel")
        atom_link = channel.find("{http://www.w3.org/2005/Atom}link")
        item = channel.find("item")

        self.assertEqual(atom_link.attrib["href"], "https://rss.mongueulin.dns.army/podcast-v2.xml")
        self.assertEqual(item.findtext("guid"), "guid-1-sans-pub-v2")

    def test_serialize_feed_tree_rejects_legacy_internal_http_urls(self) -> None:
        rss = ET.Element("rss", {"version": "2.0"})
        channel = ET.SubElement(rss, "channel")
        ET.SubElement(channel, "link").text = "http://rss.mongueulin.dns.army"

        with self.assertRaises(publish_clean_feed.PublishFeedError):
            publish_clean_feed.serialize_feed_tree(ET.ElementTree(rss))


class CleanupTests(unittest.TestCase):
    def test_purge_old_originals_deletes_files_older_than_three_days(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            original_dir = Path(temp_dir)
            old_file = original_dir / "old.mp3"
            new_file = original_dir / "new.mp3"
            old_file.write_bytes(b"old")
            new_file.write_bytes(b"new")

            reference_time = datetime.now(UTC)
            old_timestamp = (reference_time - timedelta(days=4)).timestamp()
            new_timestamp = (reference_time - timedelta(days=1)).timestamp()
            os = __import__("os")
            os.utime(old_file, (old_timestamp, old_timestamp))
            os.utime(new_file, (new_timestamp, new_timestamp))

            with patch.object(publish_clean_feed, "ORIGINALS_DIR", original_dir):
                deleted = publish_clean_feed.purge_old_originals(reference_time)

            self.assertEqual(deleted, [old_file])
            self.assertFalse(old_file.exists())
            self.assertTrue(new_file.exists())


class ChannelSerializationTests(unittest.TestCase):
    def test_serialize_and_deserialize_categories_round_trip(self) -> None:
        categories = (FeedCategory(primary="News", secondary="Daily News"), FeedCategory(primary="Society"))
        raw = publish_clean_feed.serialize_categories(categories)
        restored = publish_clean_feed.deserialize_categories(raw)
        self.assertEqual(categories, restored)


if __name__ == "__main__":
    unittest.main()
