using System.Collections.Generic;
using System.Linq;
using AcDream.Content;
using AcDream.Core.Input;
using AcDream.UI.Abstractions.Input;
using DatReaderWriter;
using Xunit;
namespace AcDream.Core.Tests.Input;
///
/// Campaign OP slice OP8: pins 's agreement
/// with — for every
/// this slice's table resolves, the UNION of DAT default bindings across every DAT
/// row mapped to that action must equal KeyBindings.RetailDefaults()'s chord
/// set for it. Per the slice contract: "investigate + report any disagreement rather
/// than silently preferring one." Skips cleanly when the installed dats are
/// unavailable (CI), matching every other live-DAT conformance test in this project.
///
///
/// Two real, byte-verified disagreements survive after the mechanism fixes
/// (2026-08-11 investigation, updated at the M2 rework — none are bugs in this
/// slice's table; both are PRE-EXISTING
/// gaps/design choices this slice does not touch, listed in
/// with citations). A THIRD
/// disagreement — ten CameraAlternateControls (InputMap 0x6) actions — was RETIRED
/// at the M2 rework: no longer maps InputMap
/// 0x6 to any at all (the aliasing that produced two
/// independent rows fighting over one live target — M2, 2026-08-11 review), so this
/// test never sees a ctx-0x6 row and the ctx-0x5-only union now matches
/// RetailDefaults() exactly for all twelve Camera actions with no allowlist
/// entry needed:
///
///
/// - MovementWalkMode. The DAT's raw QualifiedControl.Modifier
/// for the Shift-key binding is 0 (the key itself IS Shift — there is no separate
/// "modifier" to report when the primary key and the modifier are the same physical
/// key). RetailDefaults() deliberately encodes Modifiers=Shift anyway —
/// its own comment (K-fix1, 2026-04-26) explains the OS echoes
/// CurrentModifiers=Shift alongside a Shift key-DOWN event, so the chord must
/// carry the flag to match at dispatch time. Not a disagreement to fix; a raw-DAT
/// artifact this slice's reader faithfully reproduces.
/// - Quickslot 1-9's Ctrl+N chord (and its SelectQuickSlot_1-9
/// counterpart). The DAT's own default
/// master map binds Ctrl+1..9 to the SAME action id as bare 1..9 ("Quickslot N" —
/// UseQuickSlot_N), NOT to the separate "Select Quickslot N" action id
/// (SelectQuickSlot_N, DAT action ids 0x1000004E-56) — those carry NO
/// default binding at all in the shipped DAT. RetailDefaults()'s own comment
/// (citing gmToolbarUI::ListenToGlobalMessage @0x004BE4E0) asserts retail's
/// CLIENT reinterprets Ctrl+N contextually as Select — a runtime behavior this raw
/// keymap-default probe cannot see (it reads bound ACTIONS, not the dispatch
/// function's own modifier branching). Both readings are independently retail-
/// sourced; reconciling them needs the decompiled dispatch function, out of scope
/// here. Reported, not silently resolved either way.
///
///
public sealed class RetailActionIdentityRoundTripTests
{
/// Actions with a citation-backed, pre-existing reason their DAT-union
/// default set legitimately differs from
/// — see class doc. Every other mapped action must match exactly.
private static readonly HashSet KnownRetailDefaultsDisagreements = new()
{
InputAction.MovementWalkMode,
InputAction.UseQuickSlot_1,
InputAction.UseQuickSlot_2,
InputAction.UseQuickSlot_3,
InputAction.UseQuickSlot_4,
InputAction.UseQuickSlot_5,
InputAction.UseQuickSlot_6,
InputAction.UseQuickSlot_7,
InputAction.UseQuickSlot_8,
InputAction.UseQuickSlot_9,
// Same Use-vs-Select ambiguity as the bare-numeral block above: the DAT's
// own "Select Quickslot N" action ids carry NO default binding at all —
// RetailDefaults()'s Ctrl+N->Select mapping rests on the decompiled
// dispatch function's runtime modifier check, not the raw keymap default.
InputAction.SelectQuickSlot_1,
InputAction.SelectQuickSlot_2,
InputAction.SelectQuickSlot_3,
InputAction.SelectQuickSlot_4,
InputAction.SelectQuickSlot_5,
InputAction.SelectQuickSlot_6,
InputAction.SelectQuickSlot_7,
InputAction.SelectQuickSlot_8,
InputAction.SelectQuickSlot_9,
};
[Fact]
public void MappedActions_DatUnionDefaultBindings_MatchRetailDefaults()
{
string? datDir = Conformance.ConformanceDats.ResolveDatDir();
if (datDir is null) return;
using var dats = new DatCollection(datDir);
var source = new DatCollectionAdapter(dats);
RetailActionMapSnapshot? snapshot = RetailActionMapReader.Read(source);
Assert.NotNull(snapshot);
KeyBindings retailDefaults = KeyBindings.RetailDefaults();
// Aggregate DAT default chords by resolved InputAction — a single action can
// be reached by more than one DAT row (e.g. the Camera/CameraAlternate pair).
var datChordsByAction = new Dictionary>();
var unresolvedScanCodes = new List();
foreach (RetailActionMapRow row in snapshot!.Rows)
{
if (!RetailActionIdentityTable.TryResolve(row.InputMapId, row.ActionId, out InputAction action))
continue;
if (!datChordsByAction.TryGetValue(action, out HashSet? set))
datChordsByAction[action] = set = new HashSet();
foreach (RetailKeyChord raw in row.DefaultBindings)
{
Silk.NET.Input.Key? key = RetailScanCodeMap.ToSilkKey(raw.Scan, raw.Device);
if (key is null)
{
unresolvedScanCodes.Add(
$"{action}: DAT default scan=0x{raw.Scan:X2} dev={raw.Device} has no "
+ "RetailScanCodeMap entry");
continue;
}
set.Add(new KeyChord(key.Value, RetailScanCodeMap.ToModifierMask(raw.Modifier), (byte)raw.Device));
}
}
Assert.True(datChordsByAction.Count > 100,
$"expected >100 mapped actions, got {datChordsByAction.Count}");
Assert.Empty(unresolvedScanCodes);
var mismatches = new List();
foreach ((InputAction action, HashSet datChords) in datChordsByAction)
{
if (KnownRetailDefaultsDisagreements.Contains(action)) continue;
var acdreamChords = retailDefaults.ForAction(action).Select(b => b.Chord).ToHashSet();
if (!datChords.SetEquals(acdreamChords))
{
mismatches.Add(
$"{action}: DAT union=[{string.Join(",", datChords)}] vs "
+ $"RetailDefaults()=[{string.Join(",", acdreamChords)}]");
}
}
Assert.True(mismatches.Count == 0,
$"{mismatches.Count} unexpected DAT-vs-RetailDefaults() disagreements "
+ "(not in the documented KnownRetailDefaultsDisagreements allowlist):\n"
+ string.Join("\n", mismatches));
}
}