"""Tests for core/downloads/validation.py — SoundCloud preview filter.

The SoundCloud anonymous tier serves a ~30s preview clip for tracks
gated behind Go+ / login. ``filter_soundcloud_previews`` drops these
candidates before they reach the matcher, the modal cache, or the
manual-pick download path.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

from core.downloads import validation
from core.downloads.validation import filter_soundcloud_previews, get_valid_candidates


@dataclass
class _Track:
    duration_ms: int
    name: str = 'Song'
    artists: tuple[str, ...] = ('Artist',)


@dataclass
class _Candidate:
    username: str
    duration: Optional[int]  # milliseconds
    title: str = 'Song'
    artist: str = 'Artist'
    filename: str = 'candidate'


class _MatchingEngine:
    def score_track_match(self, **kwargs):
        return 0.99, 'core_title_match'

    def normalize_string(self, text):
        return (text or '').lower()


def test_drops_soundcloud_30s_preview_when_expected_long():
    """A 30s SC candidate against a 5-minute expected track is the
    canonical preview-snippet case — must be dropped."""
    expected = _Track(duration_ms=338_000)  # ~5:38
    cands = [
        _Candidate(username='soundcloud', duration=30_000, title='Preview'),
        _Candidate(username='soundcloud', duration=338_000, title='Real'),
    ]
    result = filter_soundcloud_previews(cands, expected)
    assert len(result) == 1
    assert result[0].title == 'Real'


def test_drops_under_half_expected_duration():
    """SC candidate at 100s against 300s expected = clearly truncated /
    wrong content. Must be dropped even if not at the 30s boundary."""
    expected = _Track(duration_ms=300_000)
    cand = _Candidate(username='soundcloud', duration=100_000)
    assert filter_soundcloud_previews([cand], expected) == []


def test_keeps_soundcloud_when_expected_track_is_short():
    """Genuinely short SC tracks (intros, sound effects, sub-minute
    songs) must pass through when the expected track is also short.
    Filter only kicks in when expected > 60s."""
    expected = _Track(duration_ms=45_000)  # 45s expected
    cand = _Candidate(username='soundcloud', duration=30_000)
    result = filter_soundcloud_previews([cand], expected)
    assert result == [cand]


def test_does_not_filter_non_soundcloud_sources():
    """A 30s candidate from another streaming source isn't a SoundCloud
    preview — leave it for the generic matching engine to score."""
    expected = _Track(duration_ms=338_000)
    yt = _Candidate(username='youtube', duration=30_000)
    tidal = _Candidate(username='tidal', duration=30_000)
    assert filter_soundcloud_previews([yt, tidal], expected) == [yt, tidal]


def test_returns_input_unchanged_without_expected_duration():
    """Without a Spotify-track / expected duration we can't reason
    about previews — pass everything through."""
    cands = [
        _Candidate(username='soundcloud', duration=30_000),
        _Candidate(username='soundcloud', duration=300_000),
    ]
    assert filter_soundcloud_previews(cands, None) == cands
    assert filter_soundcloud_previews(cands, _Track(duration_ms=0)) == cands


def test_empty_input_returns_empty_list():
    assert filter_soundcloud_previews([], _Track(duration_ms=200_000)) == []


def test_keeps_soundcloud_candidate_at_threshold():
    """Boundary check: 35s candidate against 200s expected — exactly
    at the 35s preview boundary, but 35s is also above
    expected*0.5 (100s) check (35 < 100, so still drops). Use a
    higher value to confirm the just-above threshold passes."""
    expected = _Track(duration_ms=200_000)  # 200s
    # 110s passes both checks: > 35s AND > 100s (half of 200s)
    cand = _Candidate(username='soundcloud', duration=110_000)
    assert filter_soundcloud_previews([cand], expected) == [cand]


def test_rejects_tidal_candidate_that_would_fail_integrity_duration(monkeypatch):
    """Structured sources should not download candidates that post-processing
    will immediately quarantine for the same duration mismatch."""
    monkeypatch.setattr(validation, 'matching_engine', _MatchingEngine())
    expected = _Track(duration_ms=338_000)
    wrong_tidal = _Candidate(username='tidal', duration=30_000)

    assert get_valid_candidates([wrong_tidal], expected, 'Artist Song') == []


def test_keeps_tidal_candidate_inside_integrity_duration_tolerance(monkeypatch):
    monkeypatch.setattr(validation, 'matching_engine', _MatchingEngine())
    expected = _Track(duration_ms=338_000)
    tidal = _Candidate(username='tidal', duration=340_000)

    result = get_valid_candidates([tidal], expected, 'Artist Song')

    assert result == [tidal]


def test_rejects_torrent_title_match_from_wrong_artist(monkeypatch):
    monkeypatch.setattr(validation, 'matching_engine', _MatchingEngine())
    expected = _Track(duration_ms=180_000, name='The Man I Need', artists=('Olivia Dean',))
    wrong_artist = _Candidate(
        username='torrent',
        duration=None,
        title='The Man I Need',
        artist='Tinkabelle',
    )

    assert get_valid_candidates([wrong_artist], expected, 'Olivia Dean The Man I Need') == []


def test_keeps_torrent_title_match_from_expected_artist(monkeypatch):
    monkeypatch.setattr(validation, 'matching_engine', _MatchingEngine())
    expected = _Track(duration_ms=180_000, name='The Man I Need', artists=('Olivia Dean',))
    correct_artist = _Candidate(
        username='torrent',
        duration=None,
        title='The Man I Need',
        artist='Olivia Dean',
    )

    result = get_valid_candidates([correct_artist], expected, 'Olivia Dean The Man I Need')

    assert result == [correct_artist]


def test_keeps_torrent_title_match_when_artist_is_indexer_fallback(monkeypatch):
    monkeypatch.setattr(validation, 'matching_engine', _MatchingEngine())
    expected = _Track(duration_ms=180_000, name='The Man I Need', artists=('Olivia Dean',))
    candidate = _Candidate(
        username='torrent',
        duration=None,
        title='The Man I Need',
        artist='Indexer',
    )
    candidate._source_metadata = {'indexer': 'Indexer'}

    result = get_valid_candidates([candidate], expected, 'Olivia Dean The Man I Need')

    assert result == [candidate]
