using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using Jellyfin.Plugin.RecentlyAddedGuard.Core; using Jellyfin.Plugin.RecentlyAddedGuard.Ledger; using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Jellyfin.Plugin.RecentlyAddedGuard.Tests; internal sealed class ManualTimeProvider : TimeProvider { public ManualTimeProvider(DateTimeOffset start) { Now = start; } public DateTimeOffset Now { get; set; } public override DateTimeOffset GetUtcNow() => Now; public void Advance(TimeSpan by) => Now += by; } internal sealed class RecordingDateWriter : IItemDateWriter { public List<(Guid ItemId, DateTime Date)> Writes { get; } = []; public bool ItemExists { get; set; } = true; public Task TrySetDateCreatedAsync(Guid itemId, DateTime dateCreated, CancellationToken cancellationToken) { if (!ItemExists) { return Task.FromResult(false); } Writes.Add((itemId, dateCreated)); return Task.FromResult(true); } } /// /// Wires the core classes against a temporary ledger file and a manual clock. /// internal sealed class GuardFixture : IDisposable { public static readonly DateTimeOffset Start = new(2026, 3, 1, 12, 0, 0, TimeSpan.Zero); public GuardFixture(GuardOptions? options = null) { DirectoryPath = Path.Combine(Path.GetTempPath(), "rag-tests", Guid.NewGuid().ToString("N")); Time = new ManualTimeProvider(Start); Options = options ?? GuardOptions.Default; Store = CreateStore(); Reconciler = new Reconciler(Store, () => Options, Time, NullLogger.Instance); Writer = new RecordingDateWriter(); Corrections = new CorrectionLog(); Processor = new GuardProcessor(Reconciler, Writer, Corrections, () => Options, Time, NullLogger.Instance); } public string DirectoryPath { get; } public string LedgerPath => Path.Combine(DirectoryPath, LedgerStore.DefaultFileName); public ManualTimeProvider Time { get; } public GuardOptions Options { get; set; } public LedgerStore Store { get; } public Reconciler Reconciler { get; } public RecordingDateWriter Writer { get; } public CorrectionLog Corrections { get; } public GuardProcessor Processor { get; } public DateTime Now => Time.GetUtcNow().UtcDateTime; /// /// Creates another store on the same file. The save delay is long, so tests control writes via Flush. /// public LedgerStore CreateStore(TimeSpan? saveDelay = null) => new(LedgerPath, NullLogger.Instance, Time, saveDelay ?? TimeSpan.FromHours(1)); public LedgerEntry SingleEntry() { var entries = Store.GetEntries(); Assert.Single(entries); return entries[0]; } public void Dispose() { Store.Dispose(); try { Directory.Delete(DirectoryPath, recursive: true); } catch (DirectoryNotFoundException) { } } } internal static class Media { public static MediaIdentity Movie( Guid itemId, DateTime dateCreated, string? tmdb = null, string? imdb = null, string? tvdb = null, string name = "The Matrix (1999)") => new(itemId, MediaKind.Movie, dateCreated, MediaKeys.ForMovie(tmdb, imdb, tvdb), name); public static MediaIdentity Episode( Guid itemId, DateTime dateCreated, Guid seriesId, int season, int episode, int? episodeEnd = null, string? seriesTvdb = null, string name = "Andor S01E01") => new( itemId, MediaKind.Episode, dateCreated, MediaKeys.ForEpisode(MediaKeys.EpisodePart(season, episode, episodeEnd)!, seriesTvdb, null, null, seriesId), name); }