using System.IO; using System.Linq; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; namespace AcDream.UI.Abstractions.Tests.Input; /// /// K.1c: / /// round-trip + version migration. /// Pins schema-stable behavior so future K.1d / Phase L panel-binding /// edits don't accidentally break existing user keymap files. /// public class KeyBindingsJsonTests { private static string TempFile(string suffix = ".json") { var f = Path.Combine(Path.GetTempPath(), $"acdream_kb_test_{System.Guid.NewGuid():N}{suffix}"); return f; } [Fact] public void LoadOrDefault_missing_file_returns_RetailDefaults() { var path = TempFile(); // No file written. var loaded = KeyBindings.LoadOrDefault(path); var defaults = KeyBindings.RetailDefaults(); Assert.Equal(defaults.All.Count, loaded.All.Count); } [Fact] public void Roundtrip_preserves_every_binding() { var path = TempFile(); try { var original = KeyBindings.RetailDefaults(); original.SaveToFile(path); var loaded = KeyBindings.LoadOrDefault(path); Assert.Equal(original.All.Count, loaded.All.Count); // Every binding from the original must round-trip with structural // equality (chord + action + activation). foreach (var b in original.All) { Assert.Contains(loaded.All, x => x.Chord == b.Chord && x.Action == b.Action && x.Activation == b.Activation); } } finally { if (File.Exists(path)) File.Delete(path); } } [Fact] public void LoadOrDefault_corrupt_file_returns_RetailDefaults() { var path = TempFile(); try { File.WriteAllText(path, "{ this is not valid JSON :: garbage"); var loaded = KeyBindings.LoadOrDefault(path); var defaults = KeyBindings.RetailDefaults(); Assert.Equal(defaults.All.Count, loaded.All.Count); } finally { if (File.Exists(path)) File.Delete(path); } } [Fact] public void LoadOrDefault_corrupt_file_does_NOT_overwrite_user_file() { var path = TempFile(); try { const string corrupt = "{ this is not valid JSON :: garbage"; File.WriteAllText(path, corrupt); _ = KeyBindings.LoadOrDefault(path); // The corrupt file must remain untouched on disk so the user // can recover or hand-edit. We never silently blow it away. Assert.Equal(corrupt, File.ReadAllText(path)); } finally { if (File.Exists(path)) File.Delete(path); } } [Fact] public void Custom_binding_for_one_action_keeps_defaults_for_others() { var path = TempFile(); try { // User customizes ONE action — replace MovementForward with Q. var custom = new KeyBindings(); custom.Add(new(new KeyChord(Key.Q, ModifierMask.None), InputAction.MovementForward)); custom.SaveToFile(path); var loaded = KeyBindings.LoadOrDefault(path); // Custom: MovementForward → Q (only — the user's file // overrode all defaults for that action). var fwd = loaded.ForAction(InputAction.MovementForward).ToList(); Assert.Single(fwd); Assert.Equal(new KeyChord(Key.Q, ModifierMask.None), fwd[0].Chord); // Default-merged: MovementBackup still has its retail bindings // because the user file didn't customize it. var back = loaded.ForAction(InputAction.MovementBackup).ToList(); Assert.Contains(back, x => x.Chord == new KeyChord(Key.X, ModifierMask.None)); } finally { if (File.Exists(path)) File.Delete(path); } } [Fact] public void LoadOrDefault_handles_version_zero_legacy_file() { // A pretend "old schema" file with version=0 + just one action. // Should still parse — unknown fields are ignored, missing // actions get default-merged. var path = TempFile(); try { const string legacyJson = """ { "version": 0, "actions": { "MovementForward": [ { "key": "W", "mod": "None" } ] } } """; File.WriteAllText(path, legacyJson); var loaded = KeyBindings.LoadOrDefault(path); // The custom binding survives. var fwd = loaded.ForAction(InputAction.MovementForward).ToList(); Assert.Single(fwd); // Default-merged: the user file didn't define MovementBackup // so it gets the retail default chords. var back = loaded.ForAction(InputAction.MovementBackup).ToList(); Assert.NotEmpty(back); } finally { if (File.Exists(path)) File.Delete(path); } } [Fact] public void LoadOrDefault_preserves_Hold_activation() { var path = TempFile(); try { var original = new KeyBindings(); original.Add(new( new KeyChord(Key.ShiftLeft, ModifierMask.None), InputAction.MovementWalkMode, ActivationType.Hold)); original.SaveToFile(path); var loaded = KeyBindings.LoadOrDefault(path); var binds = loaded.ForAction(InputAction.MovementWalkMode).ToList(); // The user binding is preserved with Hold activation. Assert.Contains(binds, x => x.Chord.Key == Key.ShiftLeft && x.Activation == ActivationType.Hold); } finally { if (File.Exists(path)) File.Delete(path); } } [Fact] public void LoadOrDefault_unknown_action_name_skipped_silently() { var path = TempFile(); try { const string json = """ { "version": 1, "actions": { "NotARealActionName": [ { "key": "W", "mod": "None" } ], "MovementForward": [ { "key": "Q", "mod": "None" } ] } } """; File.WriteAllText(path, json); var loaded = KeyBindings.LoadOrDefault(path); // The known action loaded; unknown action skipped without // throwing. var fwd = loaded.ForAction(InputAction.MovementForward).ToList(); Assert.Contains(fwd, x => x.Chord.Key == Key.Q); } finally { if (File.Exists(path)) File.Delete(path); } } [Fact] public void DefaultPath_lives_under_LocalAppData_acdream() { var path = KeyBindings.DefaultPath(); Assert.Contains("acdream", path); Assert.EndsWith("keybinds.json", path); } }