"""Tests for subtitle value objects, entities, and the ``utils`` service. Targets the quick-win surface of the subtitle domain that was largely uncovered: - ``TestSubtitleFormat`` — extension matching (case-insensitive). - ``TestSubtitleLanguage`` — token matching (case-insensitive). - ``TestSubtitleCandidateDestName`` — ``destination_name`` property: standard / SDH / forced naming, error on missing language or format. - ``TestSubtitleCandidateRepr`` — debug repr for embedded vs external. - ``TestMediaSubtitleMetadata`` — ``all_tracks`` / ``total_count`` / ``unresolved_tracks``. - ``TestAvailableSubtitles`` — utility dedup by (lang, type). - ``TestSubtitleRuleSet`` — scope inheritance + ``override`` mutation + ``to_dict`` shape. All pure-Python — no I/O. """ from __future__ import annotations from pathlib import Path import pytest from alfred.domain.subtitles.aggregates import SubtitleRuleSet from alfred.domain.subtitles.entities import MediaSubtitleMetadata, SubtitleCandidate from alfred.domain.subtitles.services.utils import available_subtitles from alfred.domain.subtitles.value_objects import ( RuleScope, SubtitleFormat, SubtitleLanguage, SubtitleType, ) # --------------------------------------------------------------------------- # # Value objects # # --------------------------------------------------------------------------- # class TestSubtitleFormat: def test_matches_extension_case_insensitive(self): fmt = SubtitleFormat(id="srt", extensions=[".srt"]) assert fmt.matches_extension(".srt") assert fmt.matches_extension(".SRT") assert not fmt.matches_extension(".ass") def test_multiple_extensions(self): fmt = SubtitleFormat(id="ass", extensions=[".ass", ".ssa"]) assert fmt.matches_extension(".ass") assert fmt.matches_extension(".ssa") assert fmt.matches_extension(".SSA") assert not fmt.matches_extension(".srt") class TestSubtitleLanguage: def test_matches_token_case_insensitive(self): lang = SubtitleLanguage(code="fra", tokens=["fr", "fre", "french"]) assert lang.matches_token("fr") assert lang.matches_token("FRENCH") assert lang.matches_token("French") assert not lang.matches_token("eng") # --------------------------------------------------------------------------- # # SubtitleCandidate # # --------------------------------------------------------------------------- # SRT = SubtitleFormat(id="srt", extensions=[".srt"]) FRA = SubtitleLanguage(code="fra", tokens=["fr", "fre"]) class TestSubtitleCandidateDestName: def test_standard(self): t = SubtitleCandidate( language=FRA, format=SRT, subtitle_type=SubtitleType.STANDARD ) assert t.destination_name == "fra.srt" def test_sdh(self): t = SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.SDH) assert t.destination_name == "fra.sdh.srt" def test_forced(self): t = SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.FORCED) assert t.destination_name == "fra.forced.srt" def test_unknown_treated_as_standard(self): t = SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.UNKNOWN) # UNKNOWN doesn't add a suffix → same as standard. assert t.destination_name == "fra.srt" def test_missing_language_raises(self): t = SubtitleCandidate(language=None, format=SRT) with pytest.raises(ValueError, match="language or format missing"): t.destination_name def test_missing_format_raises(self): t = SubtitleCandidate(language=FRA, format=None) with pytest.raises(ValueError, match="language or format missing"): t.destination_name def test_extension_dot_stripped(self): # Format extension is ".srt" — leading dot must not be duplicated. t = SubtitleCandidate(language=FRA, format=SRT) assert t.destination_name.endswith(".srt") assert ".." not in t.destination_name class TestSubtitleCandidateRepr: def test_embedded_repr(self): t = SubtitleCandidate(language=FRA, format=None, is_embedded=True, confidence=1.0) r = repr(t) assert "fra" in r assert "embedded" in r def test_external_repr_uses_filename(self, tmp_path): f = tmp_path / "fr.srt" f.write_text("") t = SubtitleCandidate( language=FRA, format=SRT, file_path=f, confidence=0.85 ) r = repr(t) assert "fra" in r assert "fr.srt" in r assert "0.85" in r def test_unresolved_repr(self): t = SubtitleCandidate(language=None, format=None) r = repr(t) assert "?" in r # --------------------------------------------------------------------------- # # MediaSubtitleMetadata # # --------------------------------------------------------------------------- # class TestMediaSubtitleMetadata: def test_empty(self): m = MediaSubtitleMetadata(media_id=None, media_type="movie") assert m.all_tracks == [] assert m.total_count == 0 assert m.unresolved_tracks == [] def test_aggregates_embedded_and_external(self): e = SubtitleCandidate(language=FRA, format=None, is_embedded=True) x = SubtitleCandidate(language=FRA, format=SRT, file_path=Path("/x.srt")) m = MediaSubtitleMetadata( media_id=None, media_type="movie", embedded_tracks=[e], external_tracks=[x], ) assert m.total_count == 2 assert m.all_tracks == [e, x] def test_unresolved_tracks_only_external_with_none_lang(self): # An embedded with None language must NOT appear in unresolved_tracks # (the property only iterates external_tracks). embedded_unknown = SubtitleCandidate(language=None, format=None, is_embedded=True) external_known = SubtitleCandidate( language=FRA, format=SRT, file_path=Path("/a.srt") ) external_unknown = SubtitleCandidate( language=None, format=SRT, file_path=Path("/b.srt") ) m = MediaSubtitleMetadata( media_id=None, media_type="movie", embedded_tracks=[embedded_unknown], external_tracks=[external_known, external_unknown], ) assert m.unresolved_tracks == [external_unknown] # --------------------------------------------------------------------------- # # available_subtitles utility # # --------------------------------------------------------------------------- # class TestAvailableSubtitles: def test_dedup_by_lang_and_type(self): ENG = SubtitleLanguage(code="eng", tokens=["en"]) tracks = [ SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.STANDARD), SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.STANDARD), SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.SDH), SubtitleCandidate(language=ENG, format=SRT, subtitle_type=SubtitleType.STANDARD), ] result = available_subtitles(tracks) keys = [(t.language.code, t.subtitle_type) for t in result] assert keys == [ ("fra", SubtitleType.STANDARD), ("fra", SubtitleType.SDH), ("eng", SubtitleType.STANDARD), ] def test_none_language_treated_as_key(self): # Tracks with no language form a single None-keyed bucket. t1 = SubtitleCandidate( language=None, format=SRT, subtitle_type=SubtitleType.UNKNOWN ) t2 = SubtitleCandidate( language=None, format=SRT, subtitle_type=SubtitleType.UNKNOWN ) result = available_subtitles([t1, t2]) assert len(result) == 1 def test_empty(self): assert available_subtitles([]) == [] # --------------------------------------------------------------------------- # # SubtitleRuleSet inheritance # # --------------------------------------------------------------------------- # class TestSubtitleRuleSet: def test_global_default_uses_kb_defaults(self): rs = SubtitleRuleSet.global_default() rules = rs.resolve() # Loaded from subtitles.yaml — defaults must be non-empty. assert rules.preferred_languages assert rules.preferred_formats assert 0 < rules.min_confidence <= 1 def test_override_persists(self): rs = SubtitleRuleSet.global_default() rs.override(languages=["eng"], min_confidence=0.9) rules = rs.resolve() assert rules.preferred_languages == ["eng"] assert rules.min_confidence == 0.9 def test_override_partial_keeps_parent_for_unset_fields(self): parent = SubtitleRuleSet.global_default() child = SubtitleRuleSet( scope=RuleScope(level="show", identifier="tt1"), parent=parent, ) child.override(languages=["jpn"]) rules = child.resolve() assert rules.preferred_languages == ["jpn"] # min_confidence not overridden at child or parent → falls back to defaults assert rules.min_confidence == parent.resolve().min_confidence def test_to_dict_only_emits_set_deltas(self): rs = SubtitleRuleSet(scope=RuleScope(level="show", identifier="tt1")) rs.override(languages=["fra"]) out = rs.to_dict() assert out["scope"] == {"level": "show", "identifier": "tt1"} assert out["override"] == {"languages": ["fra"]} def test_to_dict_full_override(self): rs = SubtitleRuleSet(scope=RuleScope(level="global")) rs.override( languages=["fra"], formats=["srt"], types=["standard"], format_priority=["srt", "ass"], min_confidence=0.8, ) out = rs.to_dict() ov = out["override"] assert ov["languages"] == ["fra"] assert ov["formats"] == ["srt"] assert ov["types"] == ["standard"] assert ov["format_priority"] == ["srt", "ass"] assert ov["min_confidence"] == 0.8 def test_min_confidence_zero_is_respected(self): # `_min_confidence or base.min_confidence` would be a bug here — the # code uses `is not None` explicitly. Verify 0.0 doesn't fall back. rs = SubtitleRuleSet.global_default() rs.override(min_confidence=0.0) assert rs.resolve().min_confidence == 0.0