using Data.Data.Repositories; using DeepDrftData; 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; /// /// Backs the public read path that the /cuts/{id} album page consumes (Phase 11 §3a, §3.3). /// CutDetailViewModel.Load fetches an album's tracks through the releaseId-filtered track page /// sorted by "TrackNumber"; that maps to with a /// predicate and an OrderBy(t => t.TrackNumber) /// expression. These tests exercise that exact query — the join narrowing, the explicit-ordinal /// ordering (not insertion order), and the projection of TrackNumber onto the DTO the page renders. /// /// Provider note: runs on the EF in-memory provider, which executes the ReleaseId equality, the /// ordinal sort, and the count in process — every predicate this path uses (no ILike branch here). /// Mirrors . /// [TestFixture] public class CutDetailTrackOrderingTests { 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 ReleaseEntity Release(string title, string artist) => new() { EntryKey = Guid.NewGuid().ToString("N"), Title = title, Artist = artist }; // A track linked to the given release with an explicit ordinal. private static TrackEntity Track(string name, int trackNumber, ReleaseEntity? release = null) => new() { EntryKey = Guid.NewGuid().ToString("N"), TrackName = name, TrackNumber = trackNumber, Release = release, }; private async Task SeedAsync(params TrackEntity[] tracks) { _context.Tracks.AddRange(tracks); await _context.SaveChangesAsync(); } // The album page's query: filter to one release, order by the explicit ordinal. private static PagingParameters OrderedByTrackNumber() => new() { Page = 1, PageSize = 100, OrderBy = t => t.TrackNumber, IsDescending = false }; // The release-id filter narrows to that album only — a sibling release's tracks never leak in. [Test] public async Task ReleaseIdFilter_ReturnsOnlyThatReleasesTracks() { var albumA = Release("Album A", "Artist"); var albumB = Release("Album B", "Artist"); await SeedAsync( Track("A-one", 1, albumA), Track("A-two", 2, albumA), Track("B-one", 1, albumB), Track("Loose", 1)); var repo = CreateRepository(); var result = await repo.GetPagedFilteredAsync( OrderedByTrackNumber(), new TrackFilter { ReleaseId = albumA.Id }); Assert.That(result.TotalCount, Is.EqualTo(2)); Assert.That(result.Items.Select(t => t.TrackName), Is.EquivalentTo(new[] { "A-one", "A-two" })); } // The ordering is by the explicit ordinal, not insertion order: tracks seeded out of order // come back ascending by TrackNumber. This is the guarantee /cuts/{id} relies on for its rows. [Test] public async Task OrderByTrackNumber_SortsByExplicitOrdinalNotInsertionOrder() { var album = Release("Album", "Artist"); // Insert deliberately scrambled relative to the intended track order. await SeedAsync( Track("Third", 3, album), Track("First", 1, album), Track("Second", 2, album)); var repo = CreateRepository(); var result = await repo.GetPagedFilteredAsync( OrderedByTrackNumber(), new TrackFilter { ReleaseId = album.Id }); Assert.That( result.Items.Select(t => t.TrackName).ToList(), Is.EqualTo(new[] { "First", "Second", "Third" }), "rows must order by the explicit TrackNumber ordinal, not the order they were inserted"); Assert.That( result.Items.Select(t => t.TrackNumber).ToList(), Is.EqualTo(new[] { 1, 2, 3 })); } // The DTO the page renders carries the ordinal — TrackConverter projects TrackNumber onto // TrackDto, so the row's number label and the saved order survive the entity -> DTO mapping. [Test] public async Task TrackConverter_ProjectsTrackNumberOntoDto() { var album = Release("Album", "Artist"); await SeedAsync( Track("First", 1, album), Track("Second", 2, album)); var repo = CreateRepository(); var result = await repo.GetPagedFilteredAsync( OrderedByTrackNumber(), new TrackFilter { ReleaseId = album.Id }); var dtos = result.Items.Select(TrackConverter.Convert).ToList(); Assert.That(dtos.Select(d => d.TrackNumber).ToList(), Is.EqualTo(new[] { 1, 2 })); Assert.That(dtos.Select(d => d.TrackName).ToList(), Is.EqualTo(new[] { "First", "Second" })); } // An album with no streamable tracks yields an empty page (no rows, no error) — the page header // still renders; the track list is simply empty. [Test] public async Task ReleaseIdFilter_WithNoTracks_ReturnsEmptyPage() { var empty = Release("Empty Album", "Artist"); var other = Release("Other", "Artist"); await SeedAsync(Track("Other-one", 1, other)); // Persist the empty release with no tracks linked to it. _context.Releases.Add(empty); await _context.SaveChangesAsync(); var repo = CreateRepository(); var result = await repo.GetPagedFilteredAsync( OrderedByTrackNumber(), new TrackFilter { ReleaseId = empty.Id }); Assert.That(result.TotalCount, Is.EqualTo(0)); Assert.That(result.Items, Is.Empty); } }