using System.IO; using AcDream.UI.Abstractions.Panels.Settings; namespace AcDream.UI.Abstractions.Tests.Panels.Settings; /// /// L.0: reads / writes settings.json. /// Tests use a temp-file path so they don't touch the user's /// %LOCALAPPDATA% file. /// public sealed class SettingsStoreTests : System.IDisposable { private readonly string _tempPath; public SettingsStoreTests() { // Unique per-test file under the system temp dir so parallel test // runners don't clobber each other. _tempPath = Path.Combine( Path.GetTempPath(), $"acdream-settings-test-{System.Guid.NewGuid():N}.json"); } public void Dispose() { if (File.Exists(_tempPath)) File.Delete(_tempPath); } [Fact] public void LoadDisplay_returns_defaults_when_file_is_missing() { var store = new SettingsStore(_tempPath); var loaded = store.LoadDisplay(); Assert.Equal(DisplaySettings.Default, loaded); } [Fact] public void SaveDisplay_then_LoadDisplay_round_trips_all_fields() { var store = new SettingsStore(_tempPath); var original = new DisplaySettings( Resolution: "2560x1440", Fullscreen: true, VSync: false, FieldOfView: 100f, Gamma: 1.4f, ShowFps: true, Quality: AcDream.UI.Abstractions.Settings.QualityPreset.Ultra); store.SaveDisplay(original); var loaded = store.LoadDisplay(); Assert.Equal(original, loaded); } [Fact] public void LoadDisplay_falls_back_to_defaults_when_file_is_corrupt() { File.WriteAllText(_tempPath, "{ this is not valid json"); var store = new SettingsStore(_tempPath); var loaded = store.LoadDisplay(); Assert.Equal(DisplaySettings.Default, loaded); } [Fact] public void LoadDisplay_falls_back_per_field_when_keys_missing() { // Partial file — only resolution set; everything else should // pick up DisplaySettings.Default values. File.WriteAllText(_tempPath, """ { "version": 1, "display": { "resolution": "1366x768" } } """); var store = new SettingsStore(_tempPath); var loaded = store.LoadDisplay(); Assert.Equal("1366x768", loaded.Resolution); Assert.Equal(DisplaySettings.Default.Fullscreen, loaded.Fullscreen); Assert.Equal(DisplaySettings.Default.VSync, loaded.VSync); Assert.Equal(DisplaySettings.Default.FieldOfView, loaded.FieldOfView); } [Fact] public void SaveDisplay_preserves_unknown_top_level_keys() { // Forward-compat: a newer client may have written sections we // don't know about (audio, gameplay). Saving display must not // delete those, otherwise running an older client would silently // drop the user's other-tab preferences. File.WriteAllText(_tempPath, """ { "version": 1, "display": { "resolution": "1280x720" }, "audio": { "master": 0.5, "music": 0.7 } } """); var store = new SettingsStore(_tempPath); store.SaveDisplay(DisplaySettings.Default with { Resolution = "1920x1080" }); var raw = File.ReadAllText(_tempPath); Assert.Contains("\"audio\"", raw); Assert.Contains("\"master\"", raw); Assert.Contains("0.5", raw); // And the new display value did get written. Assert.Contains("1920x1080", raw); } [Fact] public void DefaultPath_is_under_LocalAppData_acdream() { var path = SettingsStore.DefaultPath(); Assert.EndsWith("acdream" + Path.DirectorySeparatorChar + "settings.json", path); } // -- Audio section round-trip ---------------------------------------- [Fact] public void LoadAudio_returns_defaults_when_file_is_missing() { var store = new SettingsStore(_tempPath); Assert.Equal(AudioSettings.Default, store.LoadAudio()); } [Fact] public void SaveAudio_then_LoadAudio_round_trips_all_fields() { var store = new SettingsStore(_tempPath); var original = new AudioSettings(Master: 0.3f, Music: 0.45f, Sfx: 0.9f, Ambient: 0.6f); store.SaveAudio(original); var loaded = store.LoadAudio(); Assert.Equal(original, loaded); } [Fact] public void LoadAudio_falls_back_per_field_when_keys_missing() { File.WriteAllText(_tempPath, """ { "version": 1, "audio": { "master": 0.25 } } """); var store = new SettingsStore(_tempPath); var loaded = store.LoadAudio(); Assert.Equal(0.25f, loaded.Master); Assert.Equal(AudioSettings.Default.Music, loaded.Music); Assert.Equal(AudioSettings.Default.Sfx, loaded.Sfx); Assert.Equal(AudioSettings.Default.Ambient, loaded.Ambient); } [Fact] public void SaveAudio_preserves_display_section() { // Save display first, then audio — display values must survive. var store = new SettingsStore(_tempPath); store.SaveDisplay(DisplaySettings.Default with { Resolution = "2560x1440" }); store.SaveAudio(AudioSettings.Default with { Master = 0.4f }); Assert.Equal("2560x1440", store.LoadDisplay().Resolution); Assert.Equal(0.4f, store.LoadAudio().Master); } [Fact] public void SaveDisplay_after_SaveAudio_preserves_audio_section() { // Reverse order — audio must survive a subsequent display save. var store = new SettingsStore(_tempPath); store.SaveAudio(AudioSettings.Default with { Music = 0.1f }); store.SaveDisplay(DisplaySettings.Default with { ShowFps = true }); Assert.Equal(0.1f, store.LoadAudio().Music); Assert.True(store.LoadDisplay().ShowFps); } // -- Gameplay section round-trip -------------------------------------- [Fact] public void LoadGameplay_returns_defaults_when_file_is_missing() { var store = new SettingsStore(_tempPath); Assert.Equal(GameplaySettings.Default, store.LoadGameplay()); } [Fact] public void SaveGameplay_then_LoadGameplay_round_trips_all_fields() { var store = new SettingsStore(_tempPath); var original = GameplaySettings.Default with { AutoTarget = false, AdvancedCombatUI = true, ShowHelm = false, LockUI = true, UseMouseTurning = true, }; store.SaveGameplay(original); var loaded = store.LoadGameplay(); Assert.Equal(original, loaded); } [Fact] public void LoadGameplay_falls_back_per_field_when_keys_missing() { File.WriteAllText(_tempPath, """ { "version": 1, "gameplay": { "lockUI": true } } """); var store = new SettingsStore(_tempPath); var loaded = store.LoadGameplay(); Assert.True(loaded.LockUI); Assert.Equal(GameplaySettings.Default.AutoTarget, loaded.AutoTarget); Assert.Equal(GameplaySettings.Default.ShowHelm, loaded.ShowHelm); } [Fact] public void All_three_sections_coexist_in_one_settings_json() { var store = new SettingsStore(_tempPath); store.SaveDisplay(DisplaySettings.Default with { Resolution = "2560x1440" }); store.SaveAudio(AudioSettings.Default with { Master = 0.5f }); store.SaveGameplay(GameplaySettings.Default with { LockUI = true }); // All three load correctly from the same file. Assert.Equal("2560x1440", store.LoadDisplay().Resolution); Assert.Equal(0.5f, store.LoadAudio().Master); Assert.True(store.LoadGameplay().LockUI); } // -- Chat section round-trip ------------------------------------------ [Fact] public void LoadChat_returns_defaults_when_file_is_missing() { var store = new SettingsStore(_tempPath); Assert.Equal(ChatSettings.Default, store.LoadChat()); } [Fact] public void SaveChat_then_LoadChat_round_trips_all_fields() { var store = new SettingsStore(_tempPath); var original = new ChatSettings( HearGeneralChat: false, HearTradeChat: false, HearLFGChat: false, HearRoleplayChat: true, HearSocietyChat: true, AppearOffline: true, ShowTimestamps: false, FilterProfanity: false, FontSize: 16f); store.SaveChat(original); Assert.Equal(original, store.LoadChat()); } [Fact] public void All_four_sections_coexist_in_one_settings_json() { var store = new SettingsStore(_tempPath); store.SaveDisplay(DisplaySettings.Default with { Resolution = "2560x1440" }); store.SaveAudio(AudioSettings.Default with { Master = 0.5f }); store.SaveGameplay(GameplaySettings.Default with { LockUI = true }); store.SaveChat(ChatSettings.Default with { HearTradeChat = false, FontSize = 14f }); Assert.Equal("2560x1440", store.LoadDisplay().Resolution); Assert.Equal(0.5f, store.LoadAudio().Master); Assert.True(store.LoadGameplay().LockUI); Assert.False(store.LoadChat().HearTradeChat); Assert.Equal(14f, store.LoadChat().FontSize); } // -- Character section round-trip (per-toon) -------------------------- [Fact] public void LoadCharacter_returns_defaults_when_file_is_missing() { var store = new SettingsStore(_tempPath); Assert.Equal(CharacterSettings.Default, store.LoadCharacter("default")); } [Fact] public void LoadCharacter_returns_defaults_when_toonKey_not_in_file() { // File exists with a different toon's data; asking for "+Acdream" // returns defaults rather than the other toon's data. var store = new SettingsStore(_tempPath); store.SaveCharacter("Bob", CharacterSettings.Default with { AutoAttack = true }); var loaded = store.LoadCharacter("+Acdream"); Assert.Equal(CharacterSettings.Default, loaded); } [Fact] public void SaveCharacter_then_LoadCharacter_round_trips_all_fields() { var store = new SettingsStore(_tempPath); var original = new CharacterSettings( DefaultChatChannel: "Allegiance", AutoAttack: true, ConfirmSalvage: false, ShowPickupMessages: false); store.SaveCharacter("+Acdream", original); Assert.Equal(original, store.LoadCharacter("+Acdream")); } [Fact] public void SaveCharacter_preserves_other_toons_within_character_section() { // Two different toons, each with distinct settings — saving one // must not clobber the other. var store = new SettingsStore(_tempPath); var alice = CharacterSettings.Default with { DefaultChatChannel = "Allegiance" }; var bob = CharacterSettings.Default with { DefaultChatChannel = "Fellowship", AutoAttack = true }; store.SaveCharacter("Alice", alice); store.SaveCharacter("Bob", bob); Assert.Equal(alice, store.LoadCharacter("Alice")); Assert.Equal(bob, store.LoadCharacter("Bob")); } [Fact] public void SaveCharacter_preserves_other_top_level_sections() { // Display/audio survive when SaveCharacter writes its nested map. var store = new SettingsStore(_tempPath); store.SaveDisplay(DisplaySettings.Default with { Resolution = "2560x1440" }); store.SaveAudio(AudioSettings.Default with { Master = 0.4f }); store.SaveCharacter("+Acdream", CharacterSettings.Default with { AutoAttack = true }); Assert.Equal("2560x1440", store.LoadDisplay().Resolution); Assert.Equal(0.4f, store.LoadAudio().Master); Assert.True(store.LoadCharacter("+Acdream").AutoAttack); } [Fact] public void All_five_sections_coexist_in_one_settings_json() { var store = new SettingsStore(_tempPath); store.SaveDisplay(DisplaySettings.Default with { Resolution = "2560x1440" }); store.SaveAudio(AudioSettings.Default with { Master = 0.5f }); store.SaveGameplay(GameplaySettings.Default with { LockUI = true }); store.SaveChat(ChatSettings.Default with { HearTradeChat = false }); store.SaveCharacter("+Acdream", CharacterSettings.Default with { DefaultChatChannel = "Fellowship" }); Assert.Equal("2560x1440", store.LoadDisplay().Resolution); Assert.Equal(0.5f, store.LoadAudio().Master); Assert.True(store.LoadGameplay().LockUI); Assert.False(store.LoadChat().HearTradeChat); Assert.Equal("Fellowship", store.LoadCharacter("+Acdream").DefaultChatChannel); } }