using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using Xunit;
namespace AcDream.Core.Tests.Physics.Motion;
///
/// R4-V2 — HandleMoveToPosition Phase 1 aux-turn steering
/// (00529d80, raw 307187-307280) per r4-moveto-decomp.md §6b: the
/// 20°/340° deadband, direction pick (diff ≥ 180 → TurnLeft else TurnRight),
/// no-redundant-reissue, and the "stop aux while animating" branch.
///
public sealed class MoveToManagerAuxTurnTests
{
/// Drives a manager into an active MoveToPosition (heading
/// already settled so BeginMoveForward runs on the first BeginNextNode),
/// then drains the interp's pending_motions queue via a synthetic
/// MotionDone callback — standing in for "the WalkForward/RunForward
/// animation-table dispatch cycle has started/completed" the way a real
/// AnimationSequencer would signal it. Without this, MotionsPending()
/// stays true forever (BeginMoveForward's own _DoMotion dispatch
/// enqueues a node that nothing else in this bare harness ever pops),
/// and HandleMoveToPosition's Phase 1 would perpetually take the
/// "animating, stop aux" branch — never exercising the deadband/turn
/// logic this test file targets.
private static MoveToManagerHarness ArmMoving(float initialHeading, Vector3 targetXY)
{
var h = new MoveToManagerHarness();
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
h.Heading = initialHeading;
var p = new MovementParameters { DistanceToObject = 0.6f, UseSpheres = false };
h.Manager.MoveToPosition(new Position(1u, targetXY, Quaternion.Identity), p);
h.DrainPendingMotions();
return h;
}
[Fact]
public void WithinDeadband_NoAuxTurnIssued()
{
// Target due east (heading 90); mover already facing 85 -> diff 5,
// inside [0,20] deadband.
var h = ArmMoving(initialHeading: 90f, targetXY: new Vector3(20f, 0f, 0f));
h.Heading = 85f; // simulate drift after the initial turn-to-face completed
h.Manager.HandleMoveToPosition();
Assert.Equal(0u, h.Manager.AuxCommand);
}
[Fact]
public void JustOutsideDeadband_Positive_IssuesTurnRight()
{
var h = ArmMoving(initialHeading: 90f, targetXY: new Vector3(20f, 0f, 0f));
h.Heading = 40f; // diff = 90-40 = 50 -> outside [0,20]∪[340,360)
h.Manager.HandleMoveToPosition();
Assert.Equal(MotionCommand.TurnRight, h.Manager.AuxCommand);
}
[Fact]
public void DiffAtOrAbove180_IssuesTurnLeft()
{
var h = ArmMoving(initialHeading: 90f, targetXY: new Vector3(20f, 0f, 0f));
h.Heading = 300f; // diff = 90-300 = -210 -> +360 = 150... need >=180 for TurnLeft; pick 260.
h.Heading = 260f; // diff = 90-260=-170 -> +360=190 (>=180) -> TurnLeft
h.Manager.HandleMoveToPosition();
Assert.Equal(MotionCommand.TurnLeft, h.Manager.AuxCommand);
}
[Fact]
public void DeadbandUpperBoundary_340_NoTurn()
{
var h = ArmMoving(initialHeading: 0f, targetXY: new Vector3(0f, 20f, 0f)); // target heading 0
h.Heading = 20f; // diff = 0-20=-20 -> +360=340 -> boundary INCLUSIVE (diff >= 340)
h.Manager.HandleMoveToPosition();
Assert.Equal(0u, h.Manager.AuxCommand);
}
[Fact]
public void DeadbandLowerBoundary_20_NoTurn()
{
var h = ArmMoving(initialHeading: 0f, targetXY: new Vector3(0f, 20f, 0f)); // target heading 0
h.Heading = -20f % 360f; // normalize below
h.Heading = 340f; // diff = 0-340 = -340 -> +360=20 -> boundary INCLUSIVE (diff <= 20)
h.Manager.HandleMoveToPosition();
Assert.Equal(0u, h.Manager.AuxCommand);
}
[Fact]
public void NoRedundantReissue_SameDirectionTwice_DoesNotRedispatch()
{
var h = ArmMoving(initialHeading: 90f, targetXY: new Vector3(20f, 0f, 0f));
h.Heading = 40f; // outside deadband -> TurnRight
h.Manager.HandleMoveToPosition();
uint firstAux = h.Manager.AuxCommand;
Assert.Equal(MotionCommand.TurnRight, firstAux);
// Drain the interp's pending_motions queue (the TurnRight dispatch
// just enqueued a node) so Phase 1's "not animating" branch runs
// again on the second tick — otherwise MotionsPending() would stay
// true and Phase 1 would take the "animating, stop aux" branch
// instead of exercising the redundant-reissue guard this test
// targets.
h.DrainPendingMotions();
int stopCallsBefore = h.StopCompletelyCalls; // unrelated counter, just for isolation
// Second tick, still outside deadband, same direction -> _DoMotion
// should NOT be re-issued (turn != AuxCommand test fails since
// AuxCommand already equals TurnRight) — assert AuxCommand is
// unchanged (still TurnRight) as the observable proxy.
h.Manager.HandleMoveToPosition();
Assert.Equal(MotionCommand.TurnRight, h.Manager.AuxCommand);
Assert.Equal(stopCallsBefore, h.StopCompletelyCalls);
}
[Fact]
public void AnimatingMotionsPending_StopsAuxTurn_DoesNotStartNew()
{
var h = ArmMoving(initialHeading: 90f, targetXY: new Vector3(20f, 0f, 0f));
h.Heading = 40f;
h.Manager.HandleMoveToPosition();
Assert.Equal(MotionCommand.TurnRight, h.Manager.AuxCommand);
// Simulate an animation-table motion still pending by queueing a
// node onto the REAL MotionInterpreter's pending_motions.
h.Interp.AddToQueue(0, MotionCommand.WalkForward, 0);
Assert.True(h.Interp.MotionsPending());
h.Manager.HandleMoveToPosition();
Assert.Equal(0u, h.Manager.AuxCommand);
}
}