"""Unit tests for the analysis engine. Tests series completeness analysis, duplicate detection, and quality comparison. """ from datetime import datetime, timezone from pathlib import Path from vlm.analysis import analyze_series_completeness, compare_quality, detect_duplicates from vlm.models import MovieIdentity, SeriesIdentity, VideoFile class TestSeriesCompletenessAnalysis: """Test series completeness analysis functionality.""" def test_detect_single_gap(self): """Test detection of a single missing episode.""" episodes = [ SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), SeriesIdentity("Show Name", 1, [2], 0.9, False, "Show.Name.S01E02.mkv"), SeriesIdentity("Show Name", 1, [4], 0.9, False, "Show.Name.S01E04.mkv"), SeriesIdentity("Show Name", 1, [5], 0.9, False, "Show.Name.S01E05.mkv"), ] result = analyze_series_completeness(episodes) assert len(result) == 1 assert result[0].series_title == "Show Name" assert result[0].season == 1 assert result[0].episodes_found == [1, 2, 4, 5] assert result[0].episodes_missing == [3] def test_detect_multiple_gaps(self): """Test detection of multiple missing episodes.""" episodes = [ SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), SeriesIdentity("Show Name", 1, [3], 0.9, False, "Show.Name.S01E03.mkv"), SeriesIdentity("Show Name", 1, [5], 0.9, False, "Show.Name.S01E05.mkv"), SeriesIdentity("Show Name", 1, [7], 0.9, False, "Show.Name.S01E07.mkv"), ] result = analyze_series_completeness(episodes) assert len(result) == 1 assert result[0].episodes_found == [1, 3, 5, 7] assert result[0].episodes_missing == [2, 4, 6] def test_no_gaps_returns_empty(self): """Test that complete seasons are not included in results.""" episodes = [ SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), SeriesIdentity("Show Name", 1, [2], 0.9, False, "Show.Name.S01E02.mkv"), SeriesIdentity("Show Name", 1, [3], 0.9, False, "Show.Name.S01E03.mkv"), ] result = analyze_series_completeness(episodes) assert len(result) == 0 def test_multi_season_independence(self): """Test that gap detection for one season doesn't affect others.""" episodes = [ # Season 1 - has gap at episode 2 SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), SeriesIdentity("Show Name", 1, [3], 0.9, False, "Show.Name.S01E03.mkv"), # Season 2 - complete SeriesIdentity("Show Name", 2, [1], 0.9, False, "Show.Name.S02E01.mkv"), SeriesIdentity("Show Name", 2, [2], 0.9, False, "Show.Name.S02E02.mkv"), # Season 3 - has gap at episode 5 SeriesIdentity("Show Name", 3, [4], 0.9, False, "Show.Name.S03E04.mkv"), SeriesIdentity("Show Name", 3, [6], 0.9, False, "Show.Name.S03E06.mkv"), ] result = analyze_series_completeness(episodes) # Should have 2 results (seasons 1 and 3 with gaps) assert len(result) == 2 # Find season 1 result season1 = next(r for r in result if r.season == 1) assert season1.episodes_found == [1, 3] assert season1.episodes_missing == [2] # Find season 3 result season3 = next(r for r in result if r.season == 3) assert season3.episodes_found == [4, 6] assert season3.episodes_missing == [5] def test_multi_episode_files(self): """Test handling of multi-episode files.""" episodes = [ SeriesIdentity("Show Name", 1, [1, 2], 0.9, False, "Show.Name.S01E01-E02.mkv"), SeriesIdentity("Show Name", 1, [4], 0.9, False, "Show.Name.S01E04.mkv"), ] result = analyze_series_completeness(episodes) assert len(result) == 1 assert result[0].episodes_found == [1, 2, 4] assert result[0].episodes_missing == [3] def test_different_series_separate_analysis(self): """Test that different series are analyzed separately.""" episodes = [ # Series A - has gap SeriesIdentity("Series A", 1, [1], 0.9, False, "Series.A.S01E01.mkv"), SeriesIdentity("Series A", 1, [3], 0.9, False, "Series.A.S01E03.mkv"), # Series B - complete SeriesIdentity("Series B", 1, [1], 0.9, False, "Series.B.S01E01.mkv"), SeriesIdentity("Series B", 1, [2], 0.9, False, "Series.B.S01E02.mkv"), ] result = analyze_series_completeness(episodes) # Only Series A should be in results assert len(result) == 1 assert result[0].series_title == "Series A" assert result[0].episodes_missing == [2] def test_skip_episodes_without_season(self): """Test that episodes with season=None are excluded from analysis.""" episodes = [ SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), SeriesIdentity("Show Name", 1, [2], 0.9, False, "Show.Name.S01E02.mkv"), SeriesIdentity("Show Name", None, [1], 0.3, True, "Show.Name.Episode.1.mkv"), ] result = analyze_series_completeness(episodes) # Should only analyze season 1, which is complete assert len(result) == 0 def test_skip_episodes_with_empty_episode_list(self): """Test that episodes with empty episode list are excluded from analysis.""" episodes = [ SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), SeriesIdentity("Show Name", 1, [3], 0.9, False, "Show.Name.S01E03.mkv"), SeriesIdentity("Show Name", 1, [], 0.3, True, "Show.Name.S01.mkv"), ] result = analyze_series_completeness(episodes) # Should detect gap at episode 2 assert len(result) == 1 assert result[0].episodes_missing == [2] def test_non_sequential_start(self): """Test gap detection when episodes don't start at 1.""" episodes = [ SeriesIdentity("Show Name", 1, [5], 0.9, False, "Show.Name.S01E05.mkv"), SeriesIdentity("Show Name", 1, [6], 0.9, False, "Show.Name.S01E06.mkv"), SeriesIdentity("Show Name", 1, [8], 0.9, False, "Show.Name.S01E08.mkv"), ] result = analyze_series_completeness(episodes) # Should detect gap at episode 7 in range [5, 8] assert len(result) == 1 assert result[0].episodes_found == [5, 6, 8] assert result[0].episodes_missing == [7] def test_empty_input(self): """Test handling of empty episode list.""" result = analyze_series_completeness([]) assert len(result) == 0 def test_single_episode_no_gap(self): """Test that a single episode has no gaps.""" episodes = [ SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), ] result = analyze_series_completeness(episodes) # Single episode has no gaps assert len(result) == 0 class TestDuplicateDetection: """Test duplicate detection functionality.""" def test_detect_movie_duplicates(self): """Test detection of duplicate movies with identical title and year.""" identities = [ MovieIdentity("The Matrix", 1999, 0.9, False, "The.Matrix.1999.1080p.mkv"), MovieIdentity("The Matrix", 1999, 0.9, False, "The.Matrix.1999.720p.mkv"), MovieIdentity("Inception", 2010, 0.9, False, "Inception.2010.mkv"), ] files = [ VideoFile( Path("/movies/The.Matrix.1999.1080p.mkv"), "The.Matrix.1999.1080p.mkv", 2000000000, datetime.now(timezone.utc), "movie", resolution="1920x1080", codec="h264" ), VideoFile( Path("/movies/The.Matrix.1999.720p.mkv"), "The.Matrix.1999.720p.mkv", 1000000000, datetime.now(timezone.utc), "movie", resolution="1280x720", codec="h264" ), VideoFile( Path("/movies/Inception.2010.mkv"), "Inception.2010.mkv", 1500000000, datetime.now(timezone.utc), "movie" ), ] result = detect_duplicates(list(zip(identities, files))) # Should find one duplicate group (The Matrix) assert len(result) == 1 assert isinstance(result[0].identity, MovieIdentity) assert result[0].identity.title == "The Matrix" assert result[0].identity.year == 1999 assert len(result[0].files) == 2 assert len(result[0].quality_comparison) == 2 def test_detect_series_duplicates(self): """Test detection of duplicate series episodes.""" identities = [ SeriesIdentity("Breaking Bad", 1, [1], 0.9, False, "Breaking.Bad.S01E01.1080p.mkv"), SeriesIdentity("Breaking Bad", 1, [1], 0.9, False, "Breaking.Bad.S01E01.720p.mkv"), SeriesIdentity("Breaking Bad", 1, [2], 0.9, False, "Breaking.Bad.S01E02.mkv"), ] files = [ VideoFile( Path("/series/Breaking.Bad.S01E01.1080p.mkv"), "Breaking.Bad.S01E01.1080p.mkv", 1500000000, datetime.now(timezone.utc), "series", resolution="1920x1080" ), VideoFile( Path("/series/Breaking.Bad.S01E01.720p.mkv"), "Breaking.Bad.S01E01.720p.mkv", 800000000, datetime.now(timezone.utc), "series", resolution="1280x720" ), VideoFile( Path("/series/Breaking.Bad.S01E02.mkv"), "Breaking.Bad.S01E02.mkv", 1200000000, datetime.now(timezone.utc), "series" ), ] result = detect_duplicates(list(zip(identities, files))) # Should find one duplicate group (S01E01) assert len(result) == 1 assert isinstance(result[0].identity, SeriesIdentity) assert result[0].identity.title == "Breaking Bad" assert result[0].identity.season == 1 assert 1 in result[0].identity.episodes assert len(result[0].files) == 2 def test_no_duplicates(self): """Test that unique files are not flagged as duplicates.""" identities = [ MovieIdentity("Movie A", 2020, 0.9, False, "Movie.A.2020.mkv"), MovieIdentity("Movie B", 2021, 0.9, False, "Movie.B.2021.mkv"), ] files = [ VideoFile(Path("/movies/Movie.A.2020.mkv"), "Movie.A.2020.mkv", 1000000000, datetime.now(timezone.utc), "movie"), VideoFile(Path("/movies/Movie.B.2021.mkv"), "Movie.B.2021.mkv", 1000000000, datetime.now(timezone.utc), "movie"), ] result = detect_duplicates(list(zip(identities, files))) assert len(result) == 0 def test_skip_movies_without_year(self): """Test that movies without year are excluded from duplicate detection.""" identities = [ MovieIdentity("Unknown Movie", None, 0.3, True, "Unknown.Movie.mkv"), MovieIdentity("Unknown Movie", None, 0.3, True, "Unknown.Movie.2.mkv"), ] files = [ VideoFile(Path("/movies/Unknown.Movie.mkv"), "Unknown.Movie.mkv", 1000000000, datetime.now(timezone.utc), "movie"), VideoFile(Path("/movies/Unknown.Movie.2.mkv"), "Unknown.Movie.2.mkv", 1000000000, datetime.now(timezone.utc), "movie"), ] result = detect_duplicates(list(zip(identities, files))) # Should not detect duplicates for files needing review assert len(result) == 0 def test_skip_series_without_season(self): """Test that series without season are excluded from duplicate detection.""" identities = [ SeriesIdentity("Unknown Show", None, [1], 0.3, True, "Unknown.Show.E01.mkv"), SeriesIdentity("Unknown Show", None, [1], 0.3, True, "Unknown.Show.Episode.1.mkv"), ] files = [ VideoFile(Path("/series/Unknown.Show.E01.mkv"), "Unknown.Show.E01.mkv", 1000000000, datetime.now(timezone.utc), "series"), VideoFile(Path("/series/Unknown.Show.Episode.1.mkv"), "Unknown.Show.Episode.1.mkv", 1000000000, datetime.now(timezone.utc), "series"), ] result = detect_duplicates(list(zip(identities, files))) assert len(result) == 0 def test_skip_series_with_empty_episodes(self): """Test that series with empty episode list are excluded.""" identities = [ SeriesIdentity("Show Name", 1, [], 0.3, True, "Show.Name.S01.mkv"), SeriesIdentity("Show Name", 1, [], 0.3, True, "Show.Name.Season.1.mkv"), ] files = [ VideoFile(Path("/series/Show.Name.S01.mkv"), "Show.Name.S01.mkv", 1000000000, datetime.now(timezone.utc), "series"), VideoFile(Path("/series/Show.Name.Season.1.mkv"), "Show.Name.Season.1.mkv", 1000000000, datetime.now(timezone.utc), "series"), ] result = detect_duplicates(list(zip(identities, files))) assert len(result) == 0 def test_quality_comparison_includes_all_metadata(self): """Test that quality comparison includes all available metadata.""" identities = [ MovieIdentity("Test Movie", 2020, 0.9, False, "Test.Movie.2020.1080p.mkv"), MovieIdentity("Test Movie", 2020, 0.9, False, "Test.Movie.2020.720p.mkv"), ] files = [ VideoFile( Path("/movies/Test.Movie.2020.1080p.mkv"), "Test.Movie.2020.1080p.mkv", 2000000000, datetime.now(timezone.utc), "movie", resolution="1920x1080", codec="h264", duration_seconds=7200.0, bitrate_kbps=5000 ), VideoFile( Path("/movies/Test.Movie.2020.720p.mkv"), "Test.Movie.2020.720p.mkv", 1000000000, datetime.now(timezone.utc), "movie", resolution="1280x720", codec="h264", duration_seconds=7200.0, bitrate_kbps=2500 ), ] result = detect_duplicates(list(zip(identities, files))) assert len(result) == 1 comparison = result[0].quality_comparison assert len(comparison) == 2 # Check first file comparison data assert comparison[0]['filename'] == "Test.Movie.2020.1080p.mkv" assert comparison[0]['size_bytes'] == 2000000000 assert comparison[0]['resolution'] == "1920x1080" assert comparison[0]['codec'] == "h264" assert comparison[0]['duration_seconds'] == 7200.0 assert comparison[0]['bitrate_kbps'] == 5000 # Check second file comparison data assert comparison[1]['filename'] == "Test.Movie.2020.720p.mkv" assert comparison[1]['size_bytes'] == 1000000000 assert comparison[1]['resolution'] == "1280x720" def test_multi_episode_file_duplicates(self): """Test duplicate detection for multi-episode files.""" identities = [ SeriesIdentity("Show", 1, [1, 2], 0.9, False, "Show.S01E01-E02.mkv"), SeriesIdentity("Show", 1, [1], 0.9, False, "Show.S01E01.mkv"), SeriesIdentity("Show", 1, [2], 0.9, False, "Show.S01E02.mkv"), ] files = [ VideoFile(Path("/series/Show.S01E01-E02.mkv"), "Show.S01E01-E02.mkv", 2000000000, datetime.now(timezone.utc), "series"), VideoFile(Path("/series/Show.S01E01.mkv"), "Show.S01E01.mkv", 1000000000, datetime.now(timezone.utc), "series"), VideoFile(Path("/series/Show.S01E02.mkv"), "Show.S01E02.mkv", 1000000000, datetime.now(timezone.utc), "series"), ] result = detect_duplicates(list(zip(identities, files))) # Should find duplicates for both E01 and E02 assert len(result) == 2 def test_different_years_not_duplicates(self): """Test that same title with different years are not duplicates.""" identities = [ MovieIdentity("The Thing", 1982, 0.9, False, "The.Thing.1982.mkv"), MovieIdentity("The Thing", 2011, 0.9, False, "The.Thing.2011.mkv"), ] files = [ VideoFile(Path("/movies/The.Thing.1982.mkv"), "The.Thing.1982.mkv", 1000000000, datetime.now(timezone.utc), "movie"), VideoFile(Path("/movies/The.Thing.2011.mkv"), "The.Thing.2011.mkv", 1000000000, datetime.now(timezone.utc), "movie"), ] result = detect_duplicates(list(zip(identities, files))) assert len(result) == 0 def test_different_seasons_not_duplicates(self): """Test that same series/episode in different seasons are not duplicates.""" identities = [ SeriesIdentity("Show", 1, [1], 0.9, False, "Show.S01E01.mkv"), SeriesIdentity("Show", 2, [1], 0.9, False, "Show.S02E01.mkv"), ] files = [ VideoFile(Path("/series/Show.S01E01.mkv"), "Show.S01E01.mkv", 1000000000, datetime.now(timezone.utc), "series"), VideoFile(Path("/series/Show.S02E01.mkv"), "Show.S02E01.mkv", 1000000000, datetime.now(timezone.utc), "series"), ] result = detect_duplicates(list(zip(identities, files))) assert len(result) == 0 class TestQualityComparison: """Test quality comparison functionality.""" def test_compare_quality_with_all_metadata(self): """Test quality comparison with all metadata available.""" files = [ VideoFile( Path("/test/file1.mkv"), "file1.mkv", 2000000000, datetime.now(timezone.utc), "movie", resolution="1920x1080", codec="h264", duration_seconds=7200.0, bitrate_kbps=5000 ), VideoFile( Path("/test/file2.mkv"), "file2.mkv", 1000000000, datetime.now(timezone.utc), "movie", resolution="1280x720", codec="h265", duration_seconds=7200.0, bitrate_kbps=2500 ), ] result = compare_quality(files) assert len(result) == 2 assert result[0]['filename'] == "file1.mkv" assert result[0]['size_bytes'] == 2000000000 assert result[0]['resolution'] == "1920x1080" assert result[0]['codec'] == "h264" assert result[0]['duration_seconds'] == 7200.0 assert result[0]['bitrate_kbps'] == 5000 assert result[1]['filename'] == "file2.mkv" assert result[1]['size_bytes'] == 1000000000 assert result[1]['resolution'] == "1280x720" assert result[1]['codec'] == "h265" def test_compare_quality_with_partial_metadata(self): """Test quality comparison when some metadata is missing.""" files = [ VideoFile( Path("/test/file1.mkv"), "file1.mkv", 2000000000, datetime.now(timezone.utc), "movie", resolution="1920x1080" # codec, duration, bitrate not available ), VideoFile( Path("/test/file2.mkv"), "file2.mkv", 1000000000, datetime.now(timezone.utc), "movie" # No optional metadata ), ] result = compare_quality(files) assert len(result) == 2 assert result[0]['filename'] == "file1.mkv" assert result[0]['size_bytes'] == 2000000000 assert result[0]['resolution'] == "1920x1080" assert 'codec' not in result[0] assert 'duration_seconds' not in result[0] assert 'bitrate_kbps' not in result[0] assert result[1]['filename'] == "file2.mkv" assert result[1]['size_bytes'] == 1000000000 assert 'resolution' not in result[1] assert 'codec' not in result[1] def test_compare_quality_empty_list(self): """Test quality comparison with empty file list.""" result = compare_quality([]) assert len(result) == 0 def test_compare_quality_single_file(self): """Test quality comparison with single file.""" files = [ VideoFile( Path("/test/file.mkv"), "file.mkv", 1500000000, datetime.now(timezone.utc), "movie", resolution="1920x1080", codec="h264" ), ] result = compare_quality(files) assert len(result) == 1 assert result[0]['filename'] == "file.mkv" assert result[0]['size_bytes'] == 1500000000