using System;
using System.IO;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Conformance;
///
/// P1 decisive evidence — the PRODUCTION-PATH membership conformance. Replays the
/// captured retail doorway golden through the REAL
/// (which builds the global sphere and SWEEPS it), then checks whether the swept
/// CellId matches retail's committed cell. This is the integration test the
/// P0 bare-FindCellList probe could NOT be: the probe fed the resting foot
/// origin with no sweep, so it always reported the cell the foot stood in. Here the
/// engine sweeps prev→curr, so the sphere crosses the doorway exactly as retail's did.
///
/// Diagnostic-first: prints MATCH/DIVERGE per segment so we read the real production
/// behaviour before pinning an assertion. Outcomes (notes
/// docs/research/2026-06-03-p0-conformance-apparatus-notes.md §CORRECTION): production
/// MATCHES → the P0 divergence was a probe artifact; DIVERGES → a real bug to design from.
///
/// Scope here = the INDOOR segments (vestibule 0170 ↔ room 0171), which need only the
/// building-cell cache. The outdoor-involving segments (0031↔0170) need the landcell +
/// building portal and are a follow-up.
///
public class ThresholdPortalCrossingReplayTests
{
private readonly ITestOutputHelper _out;
public ThresholdPortalCrossingReplayTests(ITestOutputHelper output) => _out = output;
// The doorway floor is flat at the captured pz across all transitions.
private const float FloorZ = 94.005f;
private const float SphereRadius = 0.4f;
private const float SphereHeight = 1.2f;
private const float StepUpHeight = 0.4f;
private const float StepDownHeight = 0.1f;
private static (PhysicsEngine, PhysicsDataCache) BuildBuildingEngine(DatCollection dats)
{
var cache = new PhysicsDataCache();
var engine = new PhysicsEngine { DataCache = cache };
for (uint low = 0x016Fu; low <= 0x0175u; low++)
ConformanceDats.LoadEnvCell(dats, cache, ConformanceDats.HoltburgLandblock | low);
// Stub landblock for FindObjCollisions context (flat far-below terrain; the
// indoor BSP path fires first, terrain is never consulted). Matches the
// CellarUpTrajectoryReplay harness pattern.
var heights = new byte[81];
var heightTable = new float[256];
for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
engine.AddLandblock(0xA9B40000u, new TerrainSurface(heights, heightTable),
Array.Empty(), Array.Empty(), 0f, 0f);
return (engine, cache);
}
private static PhysicsBody GroundedBodyAt(Vector3 pos, uint cellId) => new()
{
Position = pos,
Orientation = Quaternion.Identity,
ContactPlaneValid = true,
ContactPlane = new Plane(0f, 0f, 1f, -FloorZ),
ContactPlaneCellId = cellId,
WalkablePolygonValid = true,
WalkablePlane = new Plane(0f, 0f, 1f, -FloorZ),
WalkableVertices = new[]
{
new Vector3(150f, 5f, FloorZ), new Vector3(150f, 20f, FloorZ),
new Vector3(165f, 20f, FloorZ), new Vector3(165f, 5f, FloorZ),
},
WalkableUp = Vector3.UnitZ,
TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
};
private static uint Low(uint id) => id & 0xFFFFu;
///
/// Documents-the-bug (GREEN while acdream diverges; FAILS when P1 lands → rewrite to
/// per-segment Assert.Equal). The swept production path DIVERGES from retail on the
/// indoor doorway crossings: ResolveWithTransition completes the move
/// (restPos == target) but leaves CellId on the SOURCE cell — it never
/// advances curr_cell across the portal the way retail's change_cell golden
/// does. So the P0 finding is NOT a probe artifact: production membership genuinely lags.
/// P1 must port retail's swept-crossing curr_cell advance (how the sphere crossing
/// the doorway polygon / the leading sphere point promotes the neighbour to the membership
/// answer mid-sweep), then this flips to all-match.
///
[Fact]
public void ProductionPath_IndoorCrossings_DivergeFromRetail_PendingP1()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
var fixturePath = Path.Combine(ConformanceDats.FixturesDir, "find-cell-list-threshold.log");
if (!File.Exists(fixturePath)) { _out.WriteLine("SKIP: capture pending"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var (engine, _) = BuildBuildingEngine(dats);
var picks = RetailTrace.ParseAll(File.ReadAllLines(fixturePath));
int match = 0, total = 0;
for (int i = 0; i + 1 < picks.Count; i++)
{
uint fromCell = picks[i].PickedCellId;
uint toCell = picks[i + 1].PickedCellId;
if (Low(fromCell) < 0x100 || Low(toCell) < 0x100) continue; // indoor segments only
var body = GroundedBodyAt(picks[i].Position, fromCell);
var result = engine.ResolveWithTransition(
currentPos: picks[i].Position,
targetPos: picks[i + 1].Position,
cellId: fromCell,
sphereRadius: SphereRadius,
sphereHeight: SphereHeight,
stepUpHeight: StepUpHeight,
stepDownHeight: StepDownHeight,
isOnGround: true,
body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0);
bool ok = result.CellId == toCell;
if (ok) match++;
total++;
_out.WriteLine(
$"seg 0x{Low(fromCell):X4}->0x{Low(toCell):X4} " +
$"pos=({picks[i].Position.X:F2},{picks[i].Position.Y:F2})->({picks[i + 1].Position.X:F2},{picks[i + 1].Position.Y:F2}) " +
$"acdream=0x{Low(result.CellId):X4} restPos=({result.Position.X:F2},{result.Position.Y:F2},{result.Position.Z:F2}) " +
$"{(ok ? "MATCH" : "DIVERGE")}");
}
_out.WriteLine($"=== production-path indoor crossings: {match}/{total} match retail ===");
Assert.True(total > 0, "no indoor doorway segments found in the golden");
Assert.True(match < total,
$"acdream's swept ResolveWithTransition now reproduces {match}/{total} retail indoor " +
"doorway crossings. If match == total, P1's curr_cell swept-advance port landed -> " +
"rewrite this to Assert.Equal(toCell, result.CellId) per segment. (Retail truth = the golden.)");
}
}