using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Combat;
using Theriapolis.Core.Rules.Stats;
using Theriapolis.Core.Util;
using Xunit;

namespace Theriapolis.Tests.Combat;

/// <summary>
/// Phase 5 plan §5: same (worldSeed, encounterId, rollSequence) → identical
/// dice outcomes across runs. Save/load can resume mid-combat by re-creating
/// the encounter and replaying through its rollCount.
/// </summary>
public sealed class DamageDeterminismTests
{
    private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));

    [Fact]
    public void EncounterSeed_IsXorOfWorldSeedRngCombatAndEncounterId()
    {
        var enc = new Encounter(worldSeed: 0xABCDUL, encounterId: 0x1234UL, MakeOne());
        Assert.Equal(0xABCDUL ^ Theriapolis.Core.C.RNG_COMBAT ^ 0x1234UL, enc.EncounterSeed);
    }

    [Fact]
    public void SameInputs_SameDiceSequence()
    {
        var a = new Encounter(0xCAFEUL, 1, MakeOne());
        var b = new Encounter(0xCAFEUL, 1, MakeOne());
        for (int i = 0; i < 100; i++)
            Assert.Equal(a.RollD20(), b.RollD20());
    }

    [Fact]
    public void DifferentEncounterIds_DivergeImmediately()
    {
        var a = new Encounter(0xCAFEUL, 1, MakeOne());
        var b = new Encounter(0xCAFEUL, 2, MakeOne());
        bool anyDifferent = false;
        for (int i = 0; i < 20; i++)
            if (a.RollD20() != b.RollD20()) { anyDifferent = true; break; }
        Assert.True(anyDifferent, "Different encounter ids should produce different dice streams.");
    }

    [Fact]
    public void ResumeRolls_SkipsForwardThroughDiceStream()
    {
        var a = new Encounter(0xCAFEUL, 1, MakeOne());
        var b = new Encounter(0xCAFEUL, 1, MakeOne());

        // Burn some rolls on `a` and capture the next 5.
        for (int i = 0; i < 10; i++) a.RollD20();
        int rollCountSnapshot = a.RollCount;  // includes initiative rolls consumed by the ctor
        int[] expected = new int[5];
        for (int i = 0; i < 5; i++) expected[i] = a.RollD20();

        // Resume `b` to the same total rollcount and capture the same window.
        b.ResumeRolls(rollCountSnapshot);
        int[] actual = new int[5];
        for (int i = 0; i < 5; i++) actual[i] = b.RollD20();

        Assert.Equal(expected, actual);
        Assert.Equal(rollCountSnapshot + 5, b.RollCount);
    }

    [Fact]
    public void Resolver_FullScenario_IsDeterministicAcrossRuns()
    {
        // Run the same scripted scenario twice and expect identical logs.
        var log1 = RunScriptedScenario(seed: 0xABCDEFUL);
        var log2 = RunScriptedScenario(seed: 0xABCDEFUL);
        Assert.Equal(log1, log2);
    }

    [Fact]
    public void Resolver_DifferentSeeds_ProduceDifferentLogs()
    {
        var log1 = RunScriptedScenario(seed: 1UL);
        var log2 = RunScriptedScenario(seed: 2UL);
        Assert.NotEqual(log1, log2);
    }

    private List<Combatant> MakeOne() => new()
    {
        Combatant.FromNpcTemplate(_content.Npcs.Templates.First(t => t.Id == "brigand_footpad"), id: 1, new Vec2(0, 0)),
    };

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

        for (int round = 0; round < 10 && !enc.IsOver; round++)
        {
            for (int t = 0; t < enc.Participants.Count && !enc.IsOver; t++)
            {
                var actor = enc.CurrentActor;
                if (actor.IsAlive && !actor.IsDown)
                {
                    var target = actor.Id == hero.Id ? foe : hero;
                    if (target.IsAlive && !target.IsDown)
                        Resolver.AttemptAttack(enc, actor, target, actor.AttackOptions[0]);
                }
                enc.EndTurn();
            }
        }

        var sb = new System.Text.StringBuilder();
        foreach (var entry in enc.Log)
            sb.AppendLine($"R{entry.Round} T{entry.Turn} [{entry.Type}] {entry.Message}");
        return sb.ToString();
    }
}
