using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Persistence;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Rules.Combat;
using Theriapolis.Core.Util;
using Xunit;

namespace Theriapolis.Tests.Persistence;

/// <summary>
/// Phase 5 plan §5: same (worldSeed, encounterId) → identical dice stream;
/// save mid-encounter, load, continue, byte-identical outcome.
///
/// We exercise the codec layer + Encounter.ResumeRolls — the live game's
/// CombatHUD wires these together but the determinism contract belongs to
/// the Core layer and tests independently of MonoGame.
/// </summary>
public sealed class MidCombatSaveRoundTripTests
{
    private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));

    [Fact]
    public void EncounterState_RoundTripsThroughSaveCodec()
    {
        var enc = MakeEncounter(0xCAFEUL);
        // Burn 5 d20s + 1 attack (mutates participants).
        for (int i = 0; i < 5; i++) enc.RollD20();
        var attacker = enc.Participants[0];
        var target   = enc.Participants[1];
        Resolver.AttemptAttack(enc, attacker, target, attacker.AttackOptions[0]);

        var snapshot = SnapshotEncounter(enc);
        var header = new SaveHeader { Version = C.SAVE_SCHEMA_VERSION };
        var body   = new SaveBody { ActiveEncounter = snapshot };
        body.PlayerCharacter = new PlayerCharacterState();  // make body well-formed for codec

        var bytes = SaveCodec.Serialize(header, body);
        var (_, body2) = SaveCodec.Deserialize(bytes);
        Assert.NotNull(body2.ActiveEncounter);
        Assert.Equal(snapshot.EncounterId,      body2.ActiveEncounter!.EncounterId);
        Assert.Equal(snapshot.RollCount,        body2.ActiveEncounter.RollCount);
        Assert.Equal(snapshot.RoundNumber,      body2.ActiveEncounter.RoundNumber);
        Assert.Equal(snapshot.Combatants.Length, body2.ActiveEncounter.Combatants.Length);
        for (int i = 0; i < snapshot.Combatants.Length; i++)
        {
            Assert.Equal(snapshot.Combatants[i].Id,        body2.ActiveEncounter.Combatants[i].Id);
            Assert.Equal(snapshot.Combatants[i].CurrentHp, body2.ActiveEncounter.Combatants[i].CurrentHp);
            Assert.Equal(snapshot.Combatants[i].PositionX, body2.ActiveEncounter.Combatants[i].PositionX);
        }
    }

    [Fact]
    public void Encounter_ResumedAtRollCount_ProducesIdenticalDownstreamLog()
    {
        // Setup: build two identical encounters from the same seed.
        var encA = MakeEncounter(0xDEADUL);
        var encB = MakeEncounter(0xDEADUL);
        Assert.Equal(encA.EncounterSeed, encB.EncounterSeed);

        // Run encA for several attacks, then snapshot its rollcount.
        var atkA = encA.Participants[0];
        var defA = encA.Participants[1];
        for (int i = 0; i < 3; i++)
            Resolver.AttemptAttack(encA, atkA, defA, atkA.AttackOptions[0]);
        int snapshotRollCount = encA.RollCount;
        int snapshotHp        = defA.CurrentHp;

        // Resume encB at the same point — defB starts at full HP, copy the
        // mutated HP from defA so they're at the same state.
        encB.ResumeRolls(snapshotRollCount);
        encB.Participants[1].CurrentHp = snapshotHp;

        // Continue both encounters in lockstep and compare each roll outcome.
        var atkB = encB.Participants[0];
        var defB = encB.Participants[1];
        for (int i = 0; i < 5; i++)
        {
            int hpBeforeA = defA.CurrentHp;
            int hpBeforeB = defB.CurrentHp;
            var resA = Resolver.AttemptAttack(encA, atkA, defA, atkA.AttackOptions[0]);
            var resB = Resolver.AttemptAttack(encB, atkB, defB, atkB.AttackOptions[0]);
            Assert.Equal(resA.D20Roll,      resB.D20Roll);
            Assert.Equal(resA.Hit,          resB.Hit);
            Assert.Equal(resA.DamageRolled, resB.DamageRolled);
            Assert.Equal(defA.CurrentHp,    defB.CurrentHp);
        }
    }

    [Fact]
    public void NpcRoster_RoundTripsThroughSaveCodec()
    {
        var roster = new NpcRosterState();
        roster.ChunkDeltas.Add(new NpcChunkDelta
        {
            ChunkX = 5, ChunkY = -3,
            KilledSpawnIndices = new[] { 1, 2, 7 },
        });
        roster.ChunkDeltas.Add(new NpcChunkDelta
        {
            ChunkX = 0, ChunkY = 0,
            KilledSpawnIndices = new[] { 0 },
        });

        var header = new SaveHeader { Version = C.SAVE_SCHEMA_VERSION };
        var body   = new SaveBody { NpcRoster = roster };
        body.PlayerCharacter = new PlayerCharacterState();
        var bytes = SaveCodec.Serialize(header, body);
        var (_, body2) = SaveCodec.Deserialize(bytes);

        Assert.Equal(2, body2.NpcRoster.ChunkDeltas.Count);
        Assert.Equal(5, body2.NpcRoster.ChunkDeltas[0].ChunkX);
        Assert.Equal(-3, body2.NpcRoster.ChunkDeltas[0].ChunkY);
        Assert.Equal(new[] { 1, 2, 7 }, body2.NpcRoster.ChunkDeltas[0].KilledSpawnIndices);
        Assert.Equal(new[] { 0 },       body2.NpcRoster.ChunkDeltas[1].KilledSpawnIndices);
    }

    private Encounter MakeEncounter(ulong worldSeed)
    {
        var brigand = _content.Npcs.Templates.First(t => t.Id == "brigand_footpad")
                          with { DefaultAllegiance = "player" };
        var wolf    = _content.Npcs.Templates.First(t => t.Id == "wolf");
        var hero = Combatant.FromNpcTemplate(brigand, id: 1, new Vec2(0, 0));
        var foe  = Combatant.FromNpcTemplate(wolf,    id: 2, new Vec2(1, 0));
        return new Encounter(worldSeed, encounterId: 7, new[] { hero, foe });
    }

    /// <summary>
    /// Build an EncounterState from a live encounter — mirrors what
    /// <see cref="Game.Screens.CombatHUDScreen.SnapshotForSave"/> does in
    /// the game side, but inlined here so this test stays Core-only.
    /// </summary>
    private static EncounterState SnapshotEncounter(Encounter enc)
    {
        var snaps = new CombatantSnapshot[enc.Participants.Count];
        for (int i = 0; i < enc.Participants.Count; i++)
        {
            var c = enc.Participants[i];
            snaps[i] = new CombatantSnapshot
            {
                Id            = c.Id,
                Name          = c.Name,
                IsPlayer      = c.SourceCharacter is not null,
                NpcTemplateId = c.SourceTemplate?.Id ?? "",
                CurrentHp     = c.CurrentHp,
                PositionX     = c.Position.X,
                PositionY     = c.Position.Y,
                Conditions    = c.Conditions.Select(x => (byte)x).ToArray(),
            };
        }
        var initOrder = new int[enc.InitiativeOrder.Count];
        for (int i = 0; i < initOrder.Length; i++) initOrder[i] = enc.InitiativeOrder[i];
        return new EncounterState
        {
            EncounterId      = enc.EncounterId,
            RollCount        = enc.RollCount,
            CurrentTurnIndex = enc.CurrentTurnIndex,
            RoundNumber      = enc.RoundNumber,
            InitiativeOrder  = initOrder,
            Combatants       = snaps,
        };
    }
}
