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

namespace Theriapolis.Tests.Combat;

public sealed class DamageRollTests
{
    [Theory]
    [InlineData("1d6",     1, 6, 0)]
    [InlineData("2d8+2",   2, 8, 2)]
    [InlineData("1d4-1",   1, 4, -1)]
    [InlineData("3d6",     3, 6, 0)]
    [InlineData("d8",      1, 8, 0)]
    [InlineData(" 1 d 6 + 1", 1, 6, 1)]
    public void Parse_ProducesExpectedShape(string expr, int n, int sides, int mod)
    {
        var d = DamageRoll.Parse(expr, DamageType.Slashing);
        Assert.Equal(n,     d.DiceCount);
        Assert.Equal(sides, d.DiceSides);
        Assert.Equal(mod,   d.FlatMod);
    }

    [Fact]
    public void Parse_PureFlatNumber_HasNoDice()
    {
        var d = DamageRoll.Parse("5", DamageType.Bludgeoning);
        Assert.Equal(0, d.DiceCount);
        Assert.Equal(0, d.DiceSides);
        Assert.Equal(5, d.FlatMod);
    }

    [Fact]
    public void Parse_BadExpressionThrows()
    {
        Assert.Throws<System.FormatException>(() => DamageRoll.Parse("1d", DamageType.Slashing));
        Assert.Throws<System.ArgumentException>(() => DamageRoll.Parse("", DamageType.Slashing));
    }

    [Fact]
    public void Roll_Range_StaysWithinMinAndMax()
    {
        var rng = new SeededRng(0xCAFEUL);
        var d   = DamageRoll.Parse("2d6+3", DamageType.Slashing);
        for (int i = 0; i < 1000; i++)
        {
            int v = d.Roll(sides => (int)(rng.NextUInt64() % (ulong)sides) + 1);
            Assert.InRange(v, d.Min(), d.Max());
        }
    }

    [Fact]
    public void Roll_Crit_DoublesDiceButNotFlatMod()
    {
        var roller = new FixedRoller(new[] { 1 });  // every die rolls 1
        var d = DamageRoll.Parse("2d6+3", DamageType.Slashing);
        // Normal: 2 dice ⇒ 2*1 + 3 = 5
        Assert.Equal(5, d.Roll(roller.Next));
        // Crit:   4 dice ⇒ 4*1 + 3 = 7  (NOT 4*1 + 6 = 10 — flat mod doesn't double)
        roller.Reset();
        Assert.Equal(7, d.Roll(roller.Next, isCrit: true));
    }

    [Fact]
    public void Roll_NeverNegative()
    {
        var d = DamageRoll.Parse("1d4-10", DamageType.Slashing);
        for (int i = 0; i < 50; i++)
        {
            // Force the die to roll 1 (the worst case): result would be 1-10 = -9, clamped to 0.
            int v = d.Roll(_ => 1);
            Assert.True(v >= 0);
        }
    }

    private sealed class FixedRoller
    {
        private readonly int[] _values;
        private int _idx;
        public FixedRoller(int[] values) { _values = values; }
        public int Next(int _) { int v = _values[_idx % _values.Length]; _idx++; return v; }
        public void Reset() => _idx = 0;
    }
}
