using Data.Data.Repositories; using DeepDrftData.Data; using DeepDrftData.Repositories; using DeepDrftModels.DTOs; using DeepDrftModels.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Models.Common; namespace DeepDrftTests; /// /// Query-shape tests for the Phase 2.2/2.3 filter and distinct-browse repository methods. /// /// Provider note: these run on the EF in-memory provider, which executes LINQ in process. That /// covers exact-match equality, null passthrough, GroupBy/Count, and ordering — every predicate /// in except the free-text branch. That branch /// uses EF.Functions.ILike, an Npgsql-only relational function with no in-memory translation, /// so the SearchText case is a Postgres integration test gated on a DSN (see SearchText_*). It is /// ignored when no test database is configured rather than asserted against a provider that never /// runs the predicate. /// [TestFixture] public class TrackFilterQueryTests { private DeepDrftContext _context = null!; [SetUp] public void SetUp() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; _context = new DeepDrftContext(options); } [TearDown] public void TearDown() { _context.Dispose(); } private TrackRepository CreateRepository() => new(_context, NullLogger>.Instance); private static TrackEntity Track( string name, string artist, string? album = null, string? genre = null, string? image = null) => new() { EntryKey = Guid.NewGuid().ToString("N"), TrackName = name, Artist = artist, Album = album, Genre = genre, ImagePath = image, }; private async Task SeedAsync(params TrackEntity[] tracks) { _context.Tracks.AddRange(tracks); await _context.SaveChangesAsync(); } private static PagingParameters DefaultPaging() => new() { Page = 1, PageSize = 20, OrderBy = t => t.Id, IsDescending = false }; // Case 2 — exact album match: returns only rows whose Album equals the filter value, and // TotalCount reflects the filtered set, not the table. [Test] public async Task GetPagedFilteredAsync_WithExactAlbum_ReturnsOnlyThatAlbum() { await SeedAsync( Track("One", "A", album: "Blue"), Track("Two", "B", album: "Blue"), Track("Three", "C", album: "Red"), Track("Four", "D", album: null)); var repo = CreateRepository(); var result = await repo.GetPagedFilteredAsync(DefaultPaging(), new TrackFilter { Album = "Blue" }); Assert.That(result.TotalCount, Is.EqualTo(2)); Assert.That(result.Items.Select(t => t.TrackName), Is.EquivalentTo(new[] { "One", "Two" })); } // Case 2b — exact genre match composes the same way as album. [Test] public async Task GetPagedFilteredAsync_WithExactGenre_ReturnsOnlyThatGenre() { await SeedAsync( Track("One", "A", genre: "Techno"), Track("Two", "B", genre: "House"), Track("Three", "C", genre: "Techno")); var repo = CreateRepository(); var result = await repo.GetPagedFilteredAsync(DefaultPaging(), new TrackFilter { Genre = "Techno" }); Assert.That(result.TotalCount, Is.EqualTo(2)); Assert.That(result.Items.Select(t => t.TrackName), Is.EquivalentTo(new[] { "One", "Three" })); } // Case 3 — null filter is a passthrough: same items and count as the unfiltered base GetPagedAsync. [Test] public async Task GetPagedFilteredAsync_WithNullFilter_MatchesUnfilteredPagedQuery() { await SeedAsync( Track("One", "A", album: "Blue"), Track("Two", "B", album: "Red"), Track("Three", "C")); var repo = CreateRepository(); var baseline = await repo.GetPagedAsync(DefaultPaging()); var filtered = await repo.GetPagedFilteredAsync(DefaultPaging(), filter: null); Assert.That(filtered.TotalCount, Is.EqualTo(baseline.TotalCount)); Assert.That( filtered.Items.Select(t => t.Id), Is.EqualTo(baseline.Items.Select(t => t.Id)).AsCollection); } // Case 4 — distinct albums: excludes null-album rows, counts per group, and takes the cover from // the first track in the group that has a non-null ImagePath. Ordered by album ascending. [Test] public async Task GetDistinctAlbumsAsync_GroupsCountsAndPicksCover() { await SeedAsync( Track("One", "A", album: "Zephyr", image: null), Track("Two", "A", album: "Zephyr", image: "cover-z"), Track("Three", "B", album: "Aria", image: "cover-a"), Track("Four", "C", album: null, image: "ignored")); var repo = CreateRepository(); var albums = await repo.GetDistinctAlbumsAsync(); Assert.That(albums.Select(a => a.Album), Is.EqualTo(new[] { "Aria", "Zephyr" }).AsCollection, "albums sort ascending and the null-album track is excluded"); var zephyr = albums.Single(a => a.Album == "Zephyr"); Assert.That(zephyr.TrackCount, Is.EqualTo(2)); Assert.That(zephyr.CoverImageKey, Is.EqualTo("cover-z"), "cover is the first non-null ImagePath in the group"); var aria = albums.Single(a => a.Album == "Aria"); Assert.That(aria.TrackCount, Is.EqualTo(1)); Assert.That(aria.CoverImageKey, Is.EqualTo("cover-a")); } // Case 5 — distinct genres: excludes null-genre rows, counts per group, ordered genre ascending. [Test] public async Task GetDistinctGenresAsync_GroupsCountsAndExcludesNull() { await SeedAsync( Track("One", "A", genre: "Techno"), Track("Two", "B", genre: "Ambient"), Track("Three", "C", genre: "Techno"), Track("Four", "D", genre: null)); var repo = CreateRepository(); var genres = await repo.GetDistinctGenresAsync(); Assert.That(genres.Select(g => g.Genre), Is.EqualTo(new[] { "Ambient", "Techno" }).AsCollection, "genres sort ascending and the null-genre track is excluded"); Assert.That(genres.Single(g => g.Genre == "Techno").TrackCount, Is.EqualTo(2)); Assert.That(genres.Single(g => g.Genre == "Ambient").TrackCount, Is.EqualTo(1)); } // Case 1 — free-text search across TrackName/Artist/Album, case-insensitive. EF.Functions.ILike // is Npgsql-only and does not translate on the in-memory provider, so this runs only against a // real Postgres database supplied via the DEEPDRFT_TEST_PG environment variable. Without it the // test is ignored rather than asserted against a provider that cannot execute the predicate. [Test] public async Task GetPagedFilteredAsync_WithSearchText_MatchesNameArtistOrAlbumCaseInsensitive() { var dsn = Environment.GetEnvironmentVariable("DEEPDRFT_TEST_PG"); if (string.IsNullOrWhiteSpace(dsn)) Assert.Ignore("Set DEEPDRFT_TEST_PG to a Postgres connection string to run the ILike search test."); var options = new DbContextOptionsBuilder() .UseNpgsql(dsn) .Options; await using var pg = new DeepDrftContext(options); await pg.Database.EnsureCreatedAsync(); try { pg.Tracks.AddRange( Track("Jazz Odyssey", "Spinal Tap", album: "Smell the Glove"), Track("Quiet Storm", "jazzmin", album: "Nightfall"), Track("Loud Noises", "Brick", album: "All JAZZ Hands"), Track("Unrelated", "Nobody", album: "Silence")); await pg.SaveChangesAsync(); var repo = new TrackRepository( pg, NullLogger>.Instance); var result = await repo.GetPagedFilteredAsync(DefaultPaging(), new TrackFilter { SearchText = "jazz" }); Assert.That(result.Items.Select(t => t.TrackName), Is.EquivalentTo(new[] { "Jazz Odyssey", "Quiet Storm", "Loud Noises" }), "ILike matches 'jazz' case-insensitively in TrackName, Artist, or Album"); } finally { await pg.Database.EnsureDeletedAsync(); } } }