using AvParser.Core.Proxies; using AvParser.Core.Settings; using AvParser.Infrastructure.Storage; using AvParser.UI.Services; using AvParser.UI.Tests.Fakes; using AvParser.UI.ViewModels; using ReactiveUI.Primitives; using ReactiveUI.Primitives.Concurrency; using Serilog.Core; namespace AvParser.UI.Tests; public class SettingsViewModelTests { /// Language service that applies nothing; these tests are not about translation. private sealed class FakeLocalizationService : ILocalizationService, IDisposable { private readonly ReactiveUI.Primitives.Signals.BehaviorSignal _current = new(AppLanguage.English); public AppLanguage Current => _current.Value; public IObservable Changes => _current; public void Apply(AppLanguage language) => _current.OnNext(language); public void Dispose() => _current.Dispose(); } /// A picker that answers with whatever the test decided, without a window. private sealed class FakeFolderPicker : IFolderPicker { public string? Answer { get; set; } public string? StartedAt { get; private set; } public Task PickAsync(string title, string? startAt = null) { StartedAt = startAt; return Task.FromResult(Answer); } } private static (SettingsViewModel Page, FakeSettingsService Settings, FakeFolderPicker Picker) Build( AppSettings? settings = null, string? mediaDirectory = null ) { var settingsService = new FakeSettingsService(settings); var picker = new FakeFolderPicker(); var paths = new AppPaths( Path.Combine(Path.GetTempPath(), "AvParserTests", Guid.NewGuid().ToString("N")), mediaDirectory ); var page = new SettingsViewModel( settingsService, new FakeThemeService(), paths, new LoggingLevelSwitch(), new ProxyPool([], new FakeProxyProbe(), new ProxyOptions()), new FakeLocalizationService(), picker, ImmediateSequencer.Instance ); return (page, settingsService, picker); } [Fact] public void An_unset_media_root_shows_as_empty_so_the_watermark_can_speak() { var (page, _, _) = Build(); page.MediaRootOverride.ShouldBeEmpty(); page.MediaDirectoryNeedsRestart.ShouldBeFalse(); } [Fact] public async Task Choosing_a_directory_records_it() { var chosen = Path.Combine(Path.GetTempPath(), "AvParserArchive"); var (page, settings, picker) = Build(); picker.Answer = chosen; await page.BrowseMediaDirectoryCommand.Execute().ToTask(TestContext.Current.CancellationToken); page.MediaRootOverride.ShouldBe(chosen); settings.Current.MediaRootOverride.ShouldBe(chosen); } [Fact] public async Task Cancelling_the_picker_changes_nothing() { var (page, _, picker) = Build(); page.MediaRootOverride = "D:/archive"; picker.Answer = null; await page.BrowseMediaDirectoryCommand.Execute().ToTask(TestContext.Current.CancellationToken); page.MediaRootOverride.ShouldBe("D:/archive"); } [Fact] public async Task Resetting_clears_the_override_so_the_default_applies_again() { var (page, settings, _) = Build(new AppSettings { MediaRootOverride = "D:/archive" }); page.MediaRootOverride.ShouldBe("D:/archive"); await page.ResetMediaDirectoryCommand.Execute().ToTask(TestContext.Current.CancellationToken); page.MediaRootOverride.ShouldBeEmpty(); settings.Current.MediaRootOverride.ShouldBeNull(); } [Fact] public void A_root_that_differs_from_this_run_asks_for_a_restart() { // Paths are resolved before the container exists, so the change cannot take effect now. // Saying so beats writing somewhere other than the box claims. var (page, _, _) = Build(mediaDirectory: "D:/current-archive"); page.MediaRootOverride = "D:/somewhere-else"; page.MediaDirectoryNeedsRestart.ShouldBeTrue(); } [Fact] public void A_root_that_matches_this_run_does_not() { var (page, _, _) = Build(mediaDirectory: "D:/current-archive"); page.MediaRootOverride = "D:/current-archive/"; page.MediaDirectoryNeedsRestart.ShouldBeFalse(); } [Fact] public async Task Whitespace_is_treated_as_no_override_at_all() { var (page, settings, _) = Build(new AppSettings { MediaRootOverride = "D:/archive" }); page.MediaRootOverride = " "; await Task.Delay(50, TestContext.Current.CancellationToken); settings.Current.MediaRootOverride.ShouldBeNull(); page.MediaDirectoryNeedsRestart.ShouldBeFalse(); } }