using Data.Data.Repositories; using DeepDrftData.Data; using DeepDrftData.Repositories; using DeepDrftModels.Entities; using DeepDrftModels.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; namespace DeepDrftTests; /// /// Aggregate-query tests for the public home hero stat row, exercising /// : the Cut track count, the per-ReleaseType Cut /// release breakdown (zero-count types absent), the Mix release count, and the Mix-runtime sum /// (null durations contributing 0). Runs on the EF in-memory provider like /// — every predicate here (Count, GroupBy, Sum with a /// null-coalesce) translates in process. /// [TestFixture] public class HomeStatsQueryTests { 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, ReleaseMedium medium, ReleaseType releaseType = ReleaseType.Single) => new() { EntryKey = Guid.NewGuid().ToString("N"), Title = title, Artist = "A", Medium = medium, ReleaseType = releaseType, }; private static TrackEntity Track(ReleaseEntity release, double? duration = null) => new() { EntryKey = Guid.NewGuid().ToString("N"), TrackName = "T", Release = release, DurationSeconds = duration, }; private async Task SeedAsync(IEnumerable releases, IEnumerable tracks) { _context.Releases.AddRange(releases); _context.Tracks.AddRange(tracks); await _context.SaveChangesAsync(); } // Cut track count reflects only tracks whose release is the Cut medium — Session and Mix tracks // are excluded — given a mix of all three media. [Test] public async Task GetHomeStatsAsync_CutTrackCount_CountsOnlyCutMediumTracks() { var cut = Release("Cut", ReleaseMedium.Cut, ReleaseType.Album); var session = Release("Session", ReleaseMedium.Session); var mix = Release("Mix", ReleaseMedium.Mix); await SeedAsync( new[] { cut, session, mix }, new[] { Track(cut), Track(cut), Track(session), Track(mix) }); var stats = await CreateRepository().GetHomeStatsAsync(); Assert.That(stats.CutTrackCount, Is.EqualTo(2)); } // The Cut release-type breakdown groups by ReleaseType, and a type with zero Cut releases is absent // from the result entirely (not present-with-zero). [Test] public async Task GetHomeStatsAsync_CutReleaseTypeBreakdown_OmitsZeroCountTypes() { await SeedAsync( new[] { Release("Cut Single 1", ReleaseMedium.Cut, ReleaseType.Single), Release("Cut Single 2", ReleaseMedium.Cut, ReleaseType.Single), Release("Cut Album", ReleaseMedium.Cut, ReleaseType.Album), // A Mix release with ReleaseType.EP must not leak into the Cut breakdown. Release("Mix EP", ReleaseMedium.Mix, ReleaseType.EP), }, Array.Empty()); var stats = await CreateRepository().GetHomeStatsAsync(); Assert.That(stats.CutReleaseTypeCounts.Any(c => c.ReleaseType == ReleaseType.EP), Is.False, "EP has zero Cut releases and must be absent, not present-with-zero"); Assert.That(stats.CutReleaseTypeCounts.Single(c => c.ReleaseType == ReleaseType.Single).Count, Is.EqualTo(2)); Assert.That(stats.CutReleaseTypeCounts.Single(c => c.ReleaseType == ReleaseType.Album).Count, Is.EqualTo(1)); } // Mix release count counts Mix-medium releases, and the runtime sum tolerates null durations: // not-yet-backfilled tracks contribute 0 rather than throwing or skewing the total. [Test] public async Task GetHomeStatsAsync_MixRuntime_TreatsNullDurationsAsZero() { var mixA = Release("Mix A", ReleaseMedium.Mix); var mixB = Release("Mix B", ReleaseMedium.Mix); var cut = Release("Cut", ReleaseMedium.Cut); await SeedAsync( new[] { mixA, mixB, cut }, new[] { Track(mixA, duration: 600d), Track(mixB, duration: null), // not yet backfilled — contributes 0 Track(cut, duration: 120d), // Cut track must not count toward mix runtime }); var stats = await CreateRepository().GetHomeStatsAsync(); Assert.That(stats.MixReleaseCount, Is.EqualTo(2)); Assert.That(stats.MixRuntimeSeconds, Is.EqualTo(600d)); } // Soft-deleted releases and tracks never count toward any figure. [Test] public async Task GetHomeStatsAsync_ExcludesSoftDeletedRowsFromAllFigures() { var liveCut = Release("Live Cut", ReleaseMedium.Cut, ReleaseType.Album); var deletedCut = Release("Dead Cut", ReleaseMedium.Cut, ReleaseType.Album); deletedCut.IsDeleted = true; var mix = Release("Mix", ReleaseMedium.Mix); var deletedMixTrack = Track(mix, duration: 999d); deletedMixTrack.IsDeleted = true; await SeedAsync( new[] { liveCut, deletedCut, mix }, new[] { Track(liveCut), deletedMixTrack }); var stats = await CreateRepository().GetHomeStatsAsync(); Assert.That(stats.CutTrackCount, Is.EqualTo(1)); Assert.That(stats.CutReleaseTypeCounts.Single(c => c.ReleaseType == ReleaseType.Album).Count, Is.EqualTo(1)); Assert.That(stats.MixRuntimeSeconds, Is.EqualTo(0d), "the only mix track is soft-deleted"); } // A live track under a directly-deleted release must be excluded from the track-based figures. // SoftDeleteReleaseAsync does not cascade to child tracks, so without the !t.Release.IsDeleted // guard the track-count and runtime figures are internally inconsistent with the release-level ones. [Test] public async Task GetHomeStatsAsync_ExcludesLiveTracksUnderSoftDeletedRelease() { var liveCut = Release("Live Cut", ReleaseMedium.Cut, ReleaseType.Album); var deletedCut = Release("Dead Cut", ReleaseMedium.Cut, ReleaseType.Album); deletedCut.IsDeleted = true; var deletedMix = Release("Dead Mix", ReleaseMedium.Mix); deletedMix.IsDeleted = true; // Both tracks are themselves live — only their parent release is soft-deleted. var liveTrackUnderDeletedCut = Track(deletedCut); var liveTrackUnderDeletedMix = Track(deletedMix, duration: 900d); await SeedAsync( new[] { liveCut, deletedCut, deletedMix }, new[] { Track(liveCut), liveTrackUnderDeletedCut, liveTrackUnderDeletedMix }); var stats = await CreateRepository().GetHomeStatsAsync(); Assert.That(stats.CutTrackCount, Is.EqualTo(1), "live track under deleted Cut release must not inflate CutTrackCount"); Assert.That(stats.MixReleaseCount, Is.EqualTo(0), "deleted Mix release must not count"); Assert.That(stats.MixRuntimeSeconds, Is.EqualTo(0d), "live track under deleted Mix release must not inflate MixRuntimeSeconds"); } }