# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""Tests for the media skill — radio, podcasts, and skill modules."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import httpx

# ---------------------------------------------------------------------------
# Radio Client Tests
# ---------------------------------------------------------------------------


class TestRadioClient:
    """Tests for familiar.skills.media.radio."""

    SAMPLE_STATION = {
        "stationuuid": "abc-123",
        "name": "  Cool Jazz FM  ",
        "url": "http://stream.example.com/jazz",
        "url_resolved": "http://stream.example.com/jazz-resolved",
        "favicon": "http://example.com/icon.png",
        "tags": "jazz, smooth, chill",
        "country": "United States",
        "codec": "MP3",
        "bitrate": 128,
        "votes": 500,
    }

    def test_format_station(self):
        from familiar.skills.media.radio import _format_station

        result = _format_station(self.SAMPLE_STATION)
        assert result["id"] == "abc-123"
        assert result["name"] == "Cool Jazz FM"  # stripped
        assert result["stream_url"] == "http://stream.example.com/jazz-resolved"
        assert result["favicon"] == "http://example.com/icon.png"
        assert result["tags"] == ["jazz", "smooth", "chill"]
        assert result["country"] == "United States"
        assert result["codec"] == "MP3"
        assert result["bitrate"] == 128
        assert result["votes"] == 500

    def test_format_station_uses_url_fallback(self):
        from familiar.skills.media.radio import _format_station

        station = dict(self.SAMPLE_STATION)
        station["url_resolved"] = ""
        result = _format_station(station)
        assert result["stream_url"] == "http://stream.example.com/jazz"

    def test_format_station_empty_tags(self):
        from familiar.skills.media.radio import _format_station

        station = dict(self.SAMPLE_STATION)
        station["tags"] = ""
        result = _format_station(station)
        assert result["tags"] == []

    @patch("familiar.skills.media.radio.api_get")
    def test_search_stations(self, mock_get):
        from familiar.skills.media.radio import search_stations

        mock_get.return_value = [self.SAMPLE_STATION]
        # Clear cache for test
        result = search_stations.__wrapped__(name="jazz", tag="", country="", limit=25)
        assert len(result) == 1
        assert result[0]["name"] == "Cool Jazz FM"
        mock_get.assert_called_once()

    @patch("familiar.skills.media.radio.api_get")
    def test_search_stations_empty(self, mock_get):
        from familiar.skills.media.radio import search_stations

        mock_get.return_value = []
        result = search_stations.__wrapped__(name="nonexistent", tag="", country="", limit=10)
        assert result == []

    @patch("familiar.skills.media.radio.api_get")
    def test_search_stations_api_error(self, mock_get):
        from familiar.skills._api_utils import APIError
        from familiar.skills.media.radio import search_stations

        mock_get.side_effect = APIError("Connection failed")
        result = search_stations.__wrapped__(name="test", tag="", country="", limit=10)
        assert result == []

    @patch("familiar.skills.media.radio.api_get")
    def test_get_genres(self, mock_get):
        from familiar.skills.media.radio import get_genres

        mock_get.return_value = [
            {"name": "rock", "stationcount": 5000},
            {"name": "jazz", "stationcount": 3000},
            {"name": "", "stationcount": 100},  # should be filtered
        ]
        result = get_genres.__wrapped__(limit=50)
        assert len(result) == 2
        assert result[0]["name"] == "rock"
        assert result[1]["stationcount"] == 3000

    @patch("familiar.skills.media.radio.api_get")
    def test_get_popular(self, mock_get):
        from familiar.skills.media.radio import get_popular

        mock_get.return_value = [self.SAMPLE_STATION]
        result = get_popular.__wrapped__(limit=25)
        assert len(result) == 1
        assert result[0]["votes"] == 500


# ---------------------------------------------------------------------------
# Podcast Client Tests
# ---------------------------------------------------------------------------


class TestPodcastClient:
    """Tests for familiar.skills.media.podcasts."""

    SAMPLE_ITUNES_RESULT = {
        "collectionName": "Tech Talk Daily",
        "trackName": "Tech Talk Daily",
        "artistName": "Jane Doe",
        "feedUrl": "https://example.com/feed.xml",
        "artworkUrl100": "https://example.com/art100.jpg",
        "artworkUrl600": "https://example.com/art600.jpg",
        "genres": ["Technology", "News"],
        "primaryGenreName": "Technology",
    }

    SAMPLE_RSS = """<?xml version="1.0" encoding="UTF-8"?>
    <rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
      <channel>
        <title>Test Podcast</title>
        <item>
          <title>Episode 1: Hello</title>
          <description>A short description of the episode.</description>
          <enclosure url="https://example.com/ep1.mp3" type="audio/mpeg" length="12345" />
          <pubDate>Mon, 01 Jan 2024 00:00:00 GMT</pubDate>
          <itunes:duration>1:23:45</itunes:duration>
          <itunes:image href="https://example.com/ep1.jpg" />
        </item>
        <item>
          <title>Episode 2: World</title>
          <description>Another description here.</description>
          <enclosure url="https://example.com/ep2.mp3" type="audio/mpeg" length="54321" />
          <itunes:duration>3600</itunes:duration>
        </item>
        <item>
          <title>Episode 3: No Audio</title>
          <description>This one has no enclosure.</description>
        </item>
      </channel>
    </rss>"""

    def test_format_podcast(self):
        from familiar.skills.media.podcasts import _format_podcast

        result = _format_podcast(self.SAMPLE_ITUNES_RESULT)
        assert result["name"] == "Tech Talk Daily"
        assert result["artist"] == "Jane Doe"
        assert result["feed_url"] == "https://example.com/feed.xml"
        assert result["artwork"] == "https://example.com/art100.jpg"
        assert result["artwork_large"] == "https://example.com/art600.jpg"
        assert result["primary_genre"] == "Technology"

    def test_parse_duration_hms(self):
        from familiar.skills.media.podcasts import _parse_duration

        assert _parse_duration("1:23:45") == "1:23:45"
        assert _parse_duration("45:30") == "45:30"

    def test_parse_duration_seconds(self):
        from familiar.skills.media.podcasts import _parse_duration

        assert _parse_duration("3600") == "1:00:00"
        assert _parse_duration("125") == "2:05"
        assert _parse_duration("59") == "0:59"

    def test_parse_duration_empty(self):
        from familiar.skills.media.podcasts import _parse_duration

        assert _parse_duration("") == ""
        assert _parse_duration(None) == ""

    @patch("familiar.skills.media.podcasts.api_get")
    @patch("familiar.skills.media.podcasts._itunes_limiter")
    def test_search_podcasts(self, mock_limiter, mock_get):
        from familiar.skills.media.podcasts import search_podcasts

        mock_limiter.acquire.return_value = True
        mock_get.return_value = {"results": [self.SAMPLE_ITUNES_RESULT]}
        result = search_podcasts.__wrapped__(term="technology", limit=10)
        assert len(result) == 1
        assert result[0]["name"] == "Tech Talk Daily"

    @patch("familiar.skills.media.podcasts.api_get")
    @patch("familiar.skills.media.podcasts._itunes_limiter")
    def test_search_podcasts_rate_limited(self, mock_limiter, mock_get):
        from familiar.skills.media.podcasts import search_podcasts

        mock_limiter.acquire.return_value = False
        result = search_podcasts.__wrapped__(term="test", limit=10)
        assert result == []
        mock_get.assert_not_called()

    @patch("familiar.skills.media.podcasts.api_get")
    @patch("familiar.skills.media.podcasts._itunes_limiter")
    def test_search_podcasts_no_feed(self, mock_limiter, mock_get):
        from familiar.skills.media.podcasts import search_podcasts

        mock_limiter.acquire.return_value = True
        result_no_feed = dict(self.SAMPLE_ITUNES_RESULT)
        result_no_feed["feedUrl"] = ""
        mock_get.return_value = {"results": [result_no_feed]}
        result = search_podcasts.__wrapped__(term="test", limit=10)
        assert result == []  # filtered out because no feedUrl

    @patch("familiar.skills.media.podcasts.httpx.get")
    def test_get_episodes(self, mock_get):
        from familiar.skills.media.podcasts import get_episodes

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = self.SAMPLE_RSS
        mock_get.return_value = mock_resp

        episodes = get_episodes("https://example.com/feed.xml", limit=20)
        assert len(episodes) == 2  # Episode 3 has no audio, should be skipped
        assert episodes[0]["title"] == "Episode 1: Hello"
        assert episodes[0]["audio_url"] == "https://example.com/ep1.mp3"
        assert episodes[0]["duration"] == "1:23:45"
        assert episodes[0]["image"] == "https://example.com/ep1.jpg"

        assert episodes[1]["title"] == "Episode 2: World"
        assert episodes[1]["duration"] == "1:00:00"  # 3600 seconds parsed

    @patch("familiar.skills.media.podcasts.httpx.get")
    def test_get_episodes_malformed_xml(self, mock_get):
        from familiar.skills.media.podcasts import get_episodes

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = "<not valid xml!@#$"
        mock_get.return_value = mock_resp

        episodes = get_episodes("https://example.com/bad.xml")
        assert episodes == []

    @patch("familiar.skills.media.podcasts.httpx.get")
    def test_get_episodes_http_error(self, mock_get):
        from familiar.skills.media.podcasts import get_episodes

        mock_get.side_effect = httpx.ConnectError("Connection refused")
        episodes = get_episodes("https://example.com/feed.xml")
        assert episodes == []

    def test_get_episodes_empty_url(self):
        from familiar.skills.media.podcasts import get_episodes

        assert get_episodes("") == []

    @patch("familiar.skills.media.podcasts.httpx.get")
    def test_get_episodes_description_truncation(self, mock_get):
        from familiar.skills.media.podcasts import get_episodes

        long_desc = "A" * 500
        rss = f"""<?xml version="1.0"?>
        <rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
          <channel><item>
            <title>Long Desc</title>
            <description>{long_desc}</description>
            <enclosure url="https://example.com/ep.mp3" type="audio/mpeg" />
          </item></channel>
        </rss>"""
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = rss
        mock_get.return_value = mock_resp

        episodes = get_episodes("https://example.com/feed.xml")
        assert len(episodes) == 1
        assert len(episodes[0]["description"]) == 300  # 297 + "..."


# ---------------------------------------------------------------------------
# Skill Module Tests
# ---------------------------------------------------------------------------


class TestMediaSkill:
    """Tests for familiar.skills.media.skill."""

    def test_tools_list_structure(self):
        from familiar.skills.media.skill import TOOLS

        assert len(TOOLS) == 5
        for tool in TOOLS:
            assert "name" in tool
            assert "description" in tool
            assert "input_schema" in tool
            assert "handler" in tool
            assert tool["category"] == "media"

    def test_tool_names(self):
        from familiar.skills.media.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "radio_search" in names
        assert "radio_genres" in names
        assert "podcast_search" in names
        assert "podcast_episodes" in names
        assert "media_favorites" in names

    @patch("familiar.skills.media.skill._get_data_dir")
    def test_load_json_missing_file(self, mock_dir, tmp_path):
        mock_dir.return_value = tmp_path
        from familiar.skills.media.skill import _load_json

        assert _load_json("nonexistent.json") == []

    @patch("familiar.skills.media.skill._get_data_dir")
    def test_load_save_json(self, mock_dir, tmp_path):
        mock_dir.return_value = tmp_path
        from familiar.skills.media.skill import _load_json, _save_json

        data = [{"name": "Test Station", "type": "radio"}]
        _save_json("test.json", data)
        loaded = _load_json("test.json")
        assert loaded == data

    @patch("familiar.skills.media.skill._get_data_dir")
    def test_favorites_add_and_remove(self, mock_dir, tmp_path):
        mock_dir.return_value = tmp_path
        from familiar.skills.media.skill import add_favorite, get_favorites, remove_favorite

        item = {"name": "Jazz FM", "stream_url": "http://jazz.fm/stream", "type": "radio"}
        assert add_favorite(item) is True
        assert len(get_favorites()) == 1

        # Duplicate should return False
        assert add_favorite(item) is False
        assert len(get_favorites()) == 1

        # Remove
        assert remove_favorite(item) is True
        assert len(get_favorites()) == 0

        # Remove non-existent
        assert remove_favorite(item) is False

    @patch("familiar.skills.media.skill._get_data_dir")
    def test_history(self, mock_dir, tmp_path):
        mock_dir.return_value = tmp_path
        from familiar.skills.media.skill import add_history, get_history

        add_history({"name": "Station 1", "type": "radio"})
        add_history({"name": "Episode 1", "type": "podcast"})
        history = get_history()
        assert len(history) == 2
        # Most recent first
        assert history[0]["name"] == "Episode 1"
        assert "played_at" in history[0]

    @patch("familiar.skills.media.skill._get_data_dir")
    def test_history_max_100(self, mock_dir, tmp_path):
        mock_dir.return_value = tmp_path
        from familiar.skills.media.skill import add_history, get_history

        for i in range(110):
            add_history({"name": f"Item {i}", "type": "radio"})
        assert len(get_history()) == 100

    @patch("familiar.skills.media.radio.search_stations")
    def test_radio_search_handler(self, mock_search):
        from familiar.skills.media.skill import radio_search

        mock_search.return_value = [
            {"name": "Jazz FM", "stream_url": "http://jazz.fm/stream",
             "tags": ["jazz"], "country": "US", "bitrate": 128}
        ]
        result = radio_search({"name": "jazz", "limit": 10})
        assert "Jazz FM" in result
        assert "128kbps" in result

    @patch("familiar.skills.media.radio.get_genres")
    def test_radio_genres_handler(self, mock_genres):
        from familiar.skills.media.skill import radio_genres

        mock_genres.return_value = [
            {"name": "rock", "stationcount": 5000},
            {"name": "jazz", "stationcount": 3000},
        ]
        result = radio_genres({"limit": 10})
        assert "rock" in result
        assert "5000" in result

    @patch("familiar.skills.media.podcasts.search_podcasts")
    def test_podcast_search_handler(self, mock_search):
        from familiar.skills.media.skill import podcast_search

        mock_search.return_value = [
            {"name": "Tech Talk", "artist": "Jane", "feed_url": "http://feed.xml",
             "primary_genre": "Technology"}
        ]
        result = podcast_search({"term": "tech"})
        assert "Tech Talk" in result
        assert "Jane" in result

    def test_podcast_search_handler_no_term(self):
        from familiar.skills.media.skill import podcast_search

        result = podcast_search({})
        assert "provide a search term" in result.lower()

    @patch("familiar.skills.media.podcasts.get_episodes")
    def test_podcast_episodes_handler(self, mock_episodes):
        from familiar.skills.media.skill import podcast_episodes

        mock_episodes.return_value = [
            {"title": "Ep 1", "audio_url": "http://ep1.mp3", "duration": "1:30:00",
             "published": "Mon, 01 Jan 2024", "description": "A test episode"}
        ]
        result = podcast_episodes({"feed_url": "http://feed.xml"})
        assert "Ep 1" in result
        assert "1:30:00" in result

    def test_podcast_episodes_handler_no_url(self):
        from familiar.skills.media.skill import podcast_episodes

        result = podcast_episodes({})
        assert "provide a feed_url" in result.lower()

    @patch("familiar.skills.media.skill._get_data_dir")
    def test_media_favorites_handler_empty(self, mock_dir, tmp_path):
        mock_dir.return_value = tmp_path
        from familiar.skills.media.skill import media_favorites

        result = media_favorites({})
        assert "no favorites" in result.lower()

    @patch("familiar.skills.media.skill._get_data_dir")
    def test_media_favorites_handler_with_data(self, mock_dir, tmp_path):
        mock_dir.return_value = tmp_path
        from familiar.skills.media.skill import add_favorite, media_favorites

        add_favorite({"name": "Rock FM", "stream_url": "http://rock.fm", "type": "radio"})
        add_favorite({"name": "Tech Pod", "feed_url": "http://tech.xml", "type": "podcast"})
        result = media_favorites({})
        assert "Rock FM" in result
        assert "Tech Pod" in result
        assert "Radio Stations" in result
        assert "Podcasts" in result
