using System.Collections.Generic;
using AcDream.Content;
using AcDream.Core.Content;
using AcDream.Core.Input;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
using DatReaderWriter.Lib.IO;
using Xunit;
namespace AcDream.Core.Tests.Input;
///
/// Campaign OP slice OP8: conformance.
/// Hermetic tests pin the merge/filter mechanism against a synthetic
/// (no DAT dependency — always runs);
/// pins the exact row
/// counts and spot-labels against the installed dats (skips cleanly when
/// unavailable, matching ConformanceDats.ResolveDatDir's pattern).
///
public sealed class RetailActionMapReaderTests
{
[Fact]
public void Read_FiltersNonUserBindableActions()
{
var actionMap = new ActionMap
{
StringTableId = 0x23000005u,
InputMaps = new Dictionary>
{
[4u] = new Dictionary
{
// Class 0 == not user-bindable — the reader must drop this row.
[0x1u] = new ActionMapValue { UserBinding = new UserBindingData { ActionClass = 0u } },
// Class 1 (Movement) — kept.
[0x29u] = new ActionMapValue
{
UserBinding = new UserBindingData
{
ActionClass = 1u,
ActionName = 0x111u,
ActionDescription = 0x222u,
},
},
},
},
ConflictingMaps = new Dictionary(),
};
var dats = new FakeDatObjectSource();
dats.Add(RetailActionMapIds.ActionMapId, actionMap);
RetailActionMapSnapshot? snapshot = RetailActionMapReader.Read(dats);
Assert.NotNull(snapshot);
RetailActionMapRow row = Assert.Single(snapshot!.Rows);
Assert.Equal(4u, row.InputMapId);
Assert.Equal(0x29u, row.ActionId);
Assert.Equal(RetailActionClass.Movement, row.ActionClass);
Assert.Equal(0x111u, row.LabelHash);
Assert.Equal(0x222u, row.TooltipHash);
}
[Fact]
public void Read_UnionMergesBothMasterMapsWithoutOrderDependency()
{
// The one real collision the live probe found: context 5 exists in BOTH
// master maps with completely disjoint action-id sets (2026-08-11 probe,
// see RetailActionMap.cs class doc unknown #4). Reproduce that shape here
// and assert the merge picks up defaults from BOTH sources for the SAME
// context, order-independent.
var actionMap = new ActionMap
{
InputMaps = new Dictionary>
{
[5u] = new Dictionary
{
[0x3Eu] = new ActionMapValue { UserBinding = new UserBindingData { ActionClass = 2u } },
[0x33u] = new ActionMapValue { UserBinding = new UserBindingData { ActionClass = 2u } },
},
},
ConflictingMaps = new Dictionary(),
};
var gameplayMap = new MasterInputMap
{
InputMaps = new Dictionary
{
[5u] = new CInputMap
{
Mappings = new List
{
new QualifiedControl
{
Key = new ControlSpecification { Key = (0xB5u << 16) | 0u, Modifier = 0u },
Activation = 3u,
Unknown = 0x3Eu,
},
},
},
},
};
var systemMap = new MasterInputMap
{
InputMaps = new Dictionary
{
[5u] = new CInputMap
{
Mappings = new List
{
new QualifiedControl
{
Key = new ControlSpecification { Key = (0x4Au << 16) | 0u, Modifier = 0u },
Activation = 3u,
Unknown = 0x33u,
},
},
},
},
};
var dats = new FakeDatObjectSource();
dats.Add(RetailActionMapIds.ActionMapId, actionMap);
dats.Add(RetailActionMapIds.GameplayMasterMapId, gameplayMap);
dats.Add(RetailActionMapIds.SystemMasterMapId, systemMap);
RetailActionMapSnapshot? snapshot = RetailActionMapReader.Read(dats);
Assert.NotNull(snapshot);
Assert.Equal(2, snapshot!.Rows.Count);
RetailActionMapRow rowFromGameplayMap = Assert.Single(snapshot.Rows, r => r.ActionId == 0x3Eu);
RetailKeyChord chord1 = Assert.Single(rowFromGameplayMap.DefaultBindings);
Assert.Equal(0xB5u, chord1.Scan);
RetailActionMapRow rowFromSystemMap = Assert.Single(snapshot.Rows, r => r.ActionId == 0x33u);
RetailKeyChord chord2 = Assert.Single(rowFromSystemMap.DefaultBindings);
Assert.Equal(0x4Au, chord2.Scan);
}
[Fact]
public void Read_MissingActionMap_ReturnsNull()
{
var dats = new FakeDatObjectSource();
Assert.Null(RetailActionMapReader.Read(dats));
}
[Fact]
public void Read_MissingMasterMaps_StillReturnsRowsWithEmptyDefaults()
{
var actionMap = new ActionMap
{
InputMaps = new Dictionary>
{
[4u] = new Dictionary
{
[0x29u] = new ActionMapValue { UserBinding = new UserBindingData { ActionClass = 1u } },
},
},
ConflictingMaps = new Dictionary(),
};
var dats = new FakeDatObjectSource();
dats.Add(RetailActionMapIds.ActionMapId, actionMap);
RetailActionMapSnapshot? snapshot = RetailActionMapReader.Read(dats);
Assert.NotNull(snapshot);
RetailActionMapRow row = Assert.Single(snapshot!.Rows);
Assert.Empty(row.DefaultBindings);
}
[Fact]
public void RetailInputMapHeaders_HasAllNineteenByteVerifiedEntries()
{
// docs/research/2026-08-10-keyboard-config-and-gameplay-tab.md §5.3.
Assert.Equal(19, RetailInputMapHeaders.NameByInputMapId.Count);
Assert.Equal("ID_InputMap_MovementCommands", RetailInputMapHeaders.NameByInputMapId[0x00000004u]);
Assert.Equal("ID_InputMap_CameraControls", RetailInputMapHeaders.NameByInputMapId[0x00000005u]);
Assert.Equal("ID_InputMap_Emotes", RetailInputMapHeaders.NameByInputMapId[0x10000006u]);
Assert.Equal("ID_InputMap_CharacterOptionCommands", RetailInputMapHeaders.NameByInputMapId[0x10000008u]);
Assert.Equal("ID_InputMap_ToggleChatEntry", RetailInputMapHeaders.NameByInputMapId[0x1000000Du]);
}
/// Minimal in-memory for hermetic tests —
/// no DAT file dependency.
private sealed class FakeDatObjectSource : IDatObjectSource
{
private readonly Dictionary _objects = new();
public void Add(uint fileId, T value) where T : IDBObj => _objects[fileId] = value!;
public T? Get(uint fileId) where T : IDBObj =>
_objects.TryGetValue(fileId, out object? value) && value is T typed ? typed : default;
public bool TryGet(uint fileId, out T value) where T : IDBObj
{
T? found = Get(fileId);
value = found!;
return found is not null;
}
}
}
/// Live-DAT conformance: pins the exact shape of the installed
/// client_portal.dat/client_local_English.dat ActionMap +
/// MasterInputMap objects. Skips cleanly when the dats are unavailable.
public sealed class RetailActionMapReader_LiveDatTests
{
[Fact]
public void Read_AgainstInstalledDats_MatchesPinnedShape()
{
string? datDir = Conformance.ConformanceDats.ResolveDatDir();
if (datDir is null) return; // CI / no local install — skip cleanly.
using var dats = new DatCollection(datDir);
var source = new DatCollectionAdapter(dats);
RetailActionMapSnapshot? snapshot = RetailActionMapReader.Read(source);
Assert.NotNull(snapshot);
// Pinned 2026-08-11 against the installed EoR dats — 306 user-bindable rows
// across the six non-zero ActionClass buckets (1,2,3,4,5,7 — never 0 or 6).
Assert.Equal(306, snapshot!.Rows.Count);
var byClass = new Dictionary();
foreach (RetailActionMapRow row in snapshot.Rows)
{
Assert.NotEqual(RetailActionClass.None, row.ActionClass);
byClass.TryGetValue(row.ActionClass, out int count);
byClass[row.ActionClass] = count + 1;
}
Assert.Equal(14, byClass[RetailActionClass.Movement]);
Assert.Equal(22, byClass[RetailActionClass.Camera]);
Assert.Equal(103, byClass[RetailActionClass.Ui]);
Assert.Equal(32, byClass[RetailActionClass.Combat]);
Assert.Equal(87, byClass[RetailActionClass.Emote]);
Assert.Equal(48, byClass[RetailActionClass.CharacterSettings]);
// Spot-pin Movement's "Move Forward" (action 0x29, InputMap 0x4): two
// default bindings (W + Up arrow), matching KeyBindings.RetailDefaults()'s
// MovementForward chords.
RetailActionMapRow moveForward = Assert.Single(
snapshot.Rows, r => r.InputMapId == 0x4u && r.ActionId == 0x29u);
Assert.Equal(2, moveForward.DefaultBindings.Count);
Assert.Contains(moveForward.DefaultBindings, c => c.Scan == 0x11u); // DIK_W
Assert.Contains(moveForward.DefaultBindings, c => c.Scan == 0xC8u); // DIK_UPARROW
}
}