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);
}
}