"""Tests for HLS-related methods on ``HiFiClient``:

- ``_parse_hls_playlist(text, playlist_url)`` — parses m3u8 playlists
- ``_demux_flac(input_path, output_path)`` — ffmpeg demuxing error paths

The parsing logic is identical in ``tidal_download_client.py``; these tests
cover the shared behavior via ``hifi_client.HiFiClient``.
"""

import os
import shutil
import subprocess
import sys
import types
from pathlib import Path
from unittest.mock import patch

import pytest


# ── stubs for module-level imports ────────────────────────────────────────

if "spotipy" not in sys.modules:
    spotipy = types.ModuleType("spotipy")
    spotipy.Spotify = object
    oauth2 = types.ModuleType("spotipy.oauth2")
    oauth2.SpotifyOAuth = object
    oauth2.SpotifyClientCredentials = object
    spotipy.oauth2 = oauth2
    sys.modules["spotipy"] = spotipy
    sys.modules["spotipy.oauth2"] = oauth2

if "config.settings" not in sys.modules:
    config_pkg = types.ModuleType("config")
    settings_mod = types.ModuleType("config.settings")

    class _DummyConfigManager:
        def get(self, key, default=None):
            return default

        def get_active_media_server(self):
            return "primary"

    settings_mod.config_manager = _DummyConfigManager()
    config_pkg.settings = settings_mod
    sys.modules["config"] = config_pkg
    sys.modules["config.settings"] = settings_mod


from core.hifi_client import HiFiClient  # noqa: E402


# ── fixture ───────────────────────────────────────────────────────────────

@pytest.fixture
def client(tmp_path):
    """HiFiClient with a temp download dir and no DB dependency."""
    return HiFiClient(download_path=str(tmp_path / "downloads"))


# ── _parse_hls_playlist: master playlist ─────────────────────────────────

MASTER_PLAYLIST = """\
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=256000,CODECS="mp4a.40.2"
stream/low.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=512000,CODECS="mp4a.40.5"
stream/high.m3u8
"""


def test_parse_master_playlist_returns_variant_uri(client):
    init, segments = client._parse_hls_playlist(
        MASTER_PLAYLIST, "https://cdn.example.com/master.m3u8"
    )
    assert init is None
    assert segments == ["https://cdn.example.com/stream/low.m3u8"]


def test_parse_master_playlist_picks_first_variant(client):
    """Master playlists should return only the first variant URI."""
    init, segments = client._parse_hls_playlist(
        MASTER_PLAYLIST, "https://cdn.example.com/master.m3u8"
    )
    assert len(segments) == 1


def test_parse_master_playlist_resolves_relative_uri(client):
    init, segments = client._parse_hls_playlist(
        MASTER_PLAYLIST, "https://cdn.example.com/playlists/master.m3u8"
    )
    assert segments[0] == "https://cdn.example.com/playlists/stream/low.m3u8"


# ── _parse_hls_playlist: variant playlist with init segment ──────────────

VARIANT_WITH_INIT = """\
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:4
#EXT-X-MAP:URI="init.mp4"
#EXTINF:3.840000,
seg001.m4s
#EXTINF:3.840000,
seg002.m4s
#EXTINF:2.560000,
seg003.m4s
"""


def test_parse_variant_with_init_returns_init_and_segments(client):
    init, segments = client._parse_hls_playlist(
        VARIANT_WITH_INIT, "https://cdn.example.com/variant.m3u8"
    )
    assert init == "https://cdn.example.com/init.mp4"
    assert segments == [
        "https://cdn.example.com/seg001.m4s",
        "https://cdn.example.com/seg002.m4s",
        "https://cdn.example.com/seg003.m4s",
    ]


def test_parse_variant_with_init_resolves_relative_uris(client):
    init, segments = client._parse_hls_playlist(
        VARIANT_WITH_INIT, "https://cdn.example.com/audio/variant.m3u8"
    )
    assert init == "https://cdn.example.com/audio/init.mp4"
    assert segments[0] == "https://cdn.example.com/audio/seg001.m4s"


def test_parse_variant_with_absolute_init_uri(client):
    playlist = """\
#EXTM3U
#EXT-X-MAP:URI="https://other.cdn/init.mp4"
#EXTINF:3.0,
https://cdn.example.com/seg001.m4s
"""
    init, segments = client._parse_hls_playlist(
        playlist, "https://cdn.example.com/variant.m3u8"
    )
    assert init == "https://other.cdn/init.mp4"
    assert segments == ["https://cdn.example.com/seg001.m4s"]


# ── _parse_hls_playlist: variant playlist without init segment ───────────

VARIANT_NO_INIT = """\
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXTINF:9.984000,
seg001.ts
#EXTINF:9.984000,
seg002.ts
#EXT-X-ENDLIST
"""


def test_parse_variant_without_init_returns_none_init(client):
    init, segments = client._parse_hls_playlist(
        VARIANT_NO_INIT, "https://cdn.example.com/variant.m3u8"
    )
    assert init is None
    assert segments == [
        "https://cdn.example.com/seg001.ts",
        "https://cdn.example.com/seg002.ts",
    ]


# ── _parse_hls_playlist: error cases ─────────────────────────────────────


def test_parse_empty_playlist_raises_value_error(client):
    with pytest.raises(ValueError, match="No segment URIs"):
        client._parse_hls_playlist("#EXTM3U", "https://cdn.example.com/x.m3u8")


def test_parse_only_tags_raises_value_error(client):
    """Playlist with only header and MAP tag but no segment URIs."""
    playlist = """\
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-MAP:URI="init.mp4"
"""
    with pytest.raises(ValueError, match="No segment URIs"):
        client._parse_hls_playlist(playlist, "https://cdn.example.com/x.m3u8")


def test_parse_only_extm3u_raises_value_error(client):
    with pytest.raises(ValueError, match="No segment URIs"):
        client._parse_hls_playlist("#EXTM3U\n", "https://cdn.example.com/x.m3u8")


# ── _parse_hls_playlist: edge cases ──────────────────────────────────────


def test_parse_skips_blank_lines(client):
    playlist = """\
#EXTM3U

#EXT-X-MAP:URI="init.mp4"

#EXTINF:3.0,

seg001.m4s

"""
    init, segments = client._parse_hls_playlist(
        playlist, "https://cdn.example.com/x.m3u8"
    )
    assert init == "https://cdn.example.com/init.mp4"
    assert segments == ["https://cdn.example.com/seg001.m4s"]


def test_parse_skips_unknown_tags(client):
    """Tags like #EXT-X-VERSION, #EXT-X-TARGETDURATION should be ignored."""
    playlist = """\
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:5.0,
seg001.ts
"""
    init, segments = client._parse_hls_playlist(
        playlist, "https://cdn.example.com/x.m3u8"
    )
    assert init is None
    assert segments == ["https://cdn.example.com/seg001.ts"]


def test_parse_captures_last_map_tag(client):
    """If multiple EXT-X-MAP tags appear, the last one wins (overwrites init_uri)."""
    playlist = """\
#EXTM3U
#EXT-X-MAP:URI="init-first.mp4"
#EXTINF:3.0,
seg001.m4s
#EXT-X-MAP:URI="init-second.mp4"
#EXTINF:3.0,
seg002.m4s
"""
    init, segments = client._parse_hls_playlist(
        playlist, "https://cdn.example.com/x.m3u8"
    )
    assert init == "https://cdn.example.com/init-second.mp4"
    assert len(segments) == 2


def test_parse_master_breaks_on_first_variant(client):
    """Parser should stop after finding the first variant URI."""
    playlist = """\
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=256000
variant-low.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=512000
variant-high.m3u8
#EXTINF:3.0,
should-not-appear.ts
"""
    init, segments = client._parse_hls_playlist(
        playlist, "https://cdn.example.com/master.m3u8"
    )
    assert init is None
    assert segments == ["https://cdn.example.com/variant-low.m3u8"]


def test_parse_master_skips_comment_after_stream_inf(client):
    """The line immediately after #EXT-X-STREAM-INF must be a non-comment URI."""
    playlist = """\
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=256000
# some comment
variant.m3u8
"""
    init, segments = client._parse_hls_playlist(
        playlist, "https://cdn.example.com/master.m3u8"
    )
    assert segments == ["https://cdn.example.com/variant.m3u8"]


def test_parse_handles_mixed_absolute_and_relative_uris(client):
    playlist = """\
#EXTM3U
#EXTINF:3.0,
https://cdn-a.example.com/seg001.m4s
#EXTINF:3.0,
seg002.m4s
#EXTINF:3.0,
https://cdn-b.example.com/seg003.m4s
"""
    init, segments = client._parse_hls_playlist(
        playlist, "https://cdn.example.com/variant.m3u8"
    )
    assert segments == [
        "https://cdn-a.example.com/seg001.m4s",
        "https://cdn.example.com/seg002.m4s",
        "https://cdn-b.example.com/seg003.m4s",
    ]


def test_parse_single_segment(client):
    playlist = """\
#EXTM3U
#EXTINF:3.0,
only-segment.m4s
"""
    init, segments = client._parse_hls_playlist(
        playlist, "https://cdn.example.com/x.m3u8"
    )
    assert init is None
    assert segments == ["https://cdn.example.com/only-segment.m4s"]


# ── _demux_flac: error paths ─────────────────────────────────────────────


def test_demux_flac_raises_when_ffmpeg_not_found(client, tmp_path, monkeypatch):
    """When ffmpeg is nowhere on PATH and not in tools/, RuntimeError is raised."""
    monkeypatch.setattr("shutil.which", lambda _: None)
    inp = tmp_path / "input.mp4"
    inp.touch()
    out = tmp_path / "output.flac"

    with patch.object(Path, "exists", return_value=False):
        with pytest.raises(RuntimeError, match="ffmpeg is required"):
            client._demux_flac(inp, out)


def test_demux_flac_raises_on_ffmpeg_failure(client, tmp_path, monkeypatch):
    """When ffmpeg exits non-zero, RuntimeError includes stderr."""
    monkeypatch.setattr("shutil.which", lambda _: "/usr/bin/ffmpeg")

    inp = tmp_path / "input.mp4"
    inp.touch()
    out = tmp_path / "output.flac"

    fake_result = subprocess.CalledProcessError(
        returncode=1,
        cmd=["ffmpeg"],
        stderr="Invalid data found when processing input",
    )

    with patch("subprocess.run", side_effect=fake_result):
        with pytest.raises(RuntimeError, match="ffmpeg failed"):
            client._demux_flac(inp, out)


def test_demux_flac_uses_tools_dir_fallback(client, tmp_path, monkeypatch):
    """When shutil.which fails but tools/ffmpeg exists, it should be used."""
    monkeypatch.setattr("shutil.which", lambda _: None)

    inp = tmp_path / "input.mp4"
    inp.touch()
    out = tmp_path / "output.flac"

    tools_dir = Path(__file__).parent.parent / "tools"
    # _demux_flac picks the OS-specific binary name (ffmpeg.exe on Windows).
    candidate_name = "ffmpeg.exe" if os.name == "nt" else "ffmpeg"
    expected_candidate = tools_dir / candidate_name

    original_exists = Path.exists
    original_which = shutil.which

    def fake_exists(self):
        if str(self) == str(expected_candidate):
            return True
        return original_exists(self)

    def fake_which(name):
        if name == "ffmpeg":
            return None
        return original_which(name)

    monkeypatch.setattr(shutil, "which", fake_which)
    monkeypatch.setattr(Path, "exists", fake_exists)

    fake_result = subprocess.CalledProcessError(
        returncode=1, cmd=["ffmpeg"], stderr="fail"
    )
    with patch("subprocess.run", side_effect=fake_result) as mock_run:
        with pytest.raises(RuntimeError, match="ffmpeg failed"):
            client._demux_flac(inp, out)

    call_args = mock_run.call_args[0][0]
    assert call_args[0] == str(expected_candidate)
