using System;
using NUnit.Framework;
using SVSim.BattleEngine.Rng;
using Wizard;
using Wizard.Battle;

namespace SVSim.BattleEngine.Tests
{
    [TestFixture]
    public class RngSeamTests
    {
        private TestBattleScope _scope;

        [SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
        [TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }

        // RandomSourceBridge.Range must mirror the engine's exact roll arithmetic:
        // BattleManagerBase.StableRandom does `(int)Math.Floor((double)val * unit)`.
        [Test]
        public void Bridge_Range_mirrors_engine_floor_arithmetic()
        {
            Assert.That(RandomSourceBridge.Range(7, 0.0), Is.EqualTo(0));   // floor(7*0)   = 0
            Assert.That(RandomSourceBridge.Range(7, 0.999), Is.EqualTo(6)); // floor(6.993) = 6 (never == val)
            Assert.That(RandomSourceBridge.Range(3, 0.5), Is.EqualTo(1));   // floor(1.5)   = 1 (middle of 3)
            Assert.That(RandomSourceBridge.Range(1, 0.5), Is.EqualTo(0));   // floor(0.5)   = 0
        }

        // SeededRandomSource(seed) must reproduce the engine's own generators EXACTLY: BattleManagerBase
        // seeds both _stableRandom and _stableRandomOnlySelf as `new System.Random(RandomSeed)`
        // (BattleManagerBase.cs:721-722). NextUnit() == synced.NextDouble(); NextSelf(max) == self.Next(max).
        [Test]
        public void SeededSource_reproduces_two_System_Random_streams()
        {
            const int seed = 12345;
            var src = new SeededRandomSource(seed);

            var refSynced = new System.Random(seed); // mirrors _stableRandom
            var refSelf   = new System.Random(seed); // mirrors _stableRandomOnlySelf (separate stream)

            for (int i = 0; i < 8; i++)
                Assert.That(src.NextUnit(), Is.EqualTo(refSynced.NextDouble()), $"NextUnit drift at {i}");
            for (int i = 0; i < 8; i++)
                Assert.That(src.NextSelf(100), Is.EqualTo(refSelf.Next(100)), $"NextSelf drift at {i}");
        }

        // ScriptedRandomSource feeds a known sequence (the oracle's control + the Phase-3 replay seam).
        // It MUST throw on overrun, not wrap: an unexpected extra roll should fail loudly so a test
        // surfaces a miscount of engine RNG calls rather than silently reusing a value.
        [Test]
        public void ScriptedSource_returns_sequence_then_throws_on_overrun()
        {
            var src = new ScriptedRandomSource(new[] { 0.1, 0.5 }, new[] { 3 });

            Assert.That(src.NextUnit(), Is.EqualTo(0.1));
            Assert.That(src.NextUnit(), Is.EqualTo(0.5));
            Assert.That(() => src.NextUnit(), Throws.InvalidOperationException, "should throw on unit overrun");

            Assert.That(src.NextSelf(99), Is.EqualTo(3));
            Assert.That(() => src.NextSelf(99), Throws.InvalidOperationException, "should throw on self overrun");
        }

        // The decoupling (F2): the override must roll REAL values even though IsForecast == true (which
        // forces the un-overridden engine methods to return 0). A ScriptedRandomSource proves the value
        // came from the injected source, not the engine's zeroing.
        [Test]
        public void Override_rolls_real_values_under_IsForecast()
        {
            BattleManagerBase.IsForecast = true; // would zero the un-overridden engine RNG

            // 3 units; with RandomSourceBridge.Range(val, unit) = floor(val*unit):
            //   StableRandom(7) with 0.5  -> floor(3.5) = 3
            //   StableRandomDouble()      -> 0.25
            //   StableRandomOnlySelf(10)  -> scripted self pick 4
            var src = new ScriptedRandomSource(new[] { 0.5, 0.25 }, new[] { 4 });
            var mgr = new HeadlessBattleMgr(new HeadlessContentsCreator(), src);
            _scope.Ctx.Mgr = mgr;

            Assert.That(mgr.StableRandom(7), Is.EqualTo(3), "StableRandom did not use the injected source");
            Assert.That(mgr.randomResult, Is.EqualTo(0.5), "StableRandom must set randomResult to the rolled unit");
            Assert.That(mgr.StableRandomDouble(), Is.EqualTo(0.25), "StableRandomDouble did not use the injected source");
            Assert.That(mgr.randomResult, Is.EqualTo(0.25), "StableRandomDouble must set randomResult");
            Assert.That(mgr.StableRandomOnlySelf(10), Is.EqualTo(4), "StableRandomOnlySelf did not use the injected source");
        }

        // Parity: with the DEFAULT (seeded) source, HeadlessBattleMgr.StableRandom must equal what the
        // verbatim engine would compute — floor(val * new System.Random(seed).NextDouble()) — pinning the
        // re-authored RandomSourceBridge arithmetic to the engine's own formula+generator. (The default
        // source seeds from HeadlessContentsCreator.RandomSeed == 12345.)
        [Test]
        public void Default_source_matches_engine_generator_and_formula()
        {
            BattleManagerBase.IsForecast = true;

            var mgr = new HeadlessBattleMgr(new HeadlessContentsCreator()); // default SeededRandomSource(12345)
            _scope.Ctx.Mgr = mgr;
            var reference = new System.Random(12345);

            for (int i = 0; i < 10; i++)
            {
                int expected = (int)System.Math.Floor(7 * reference.NextDouble());
                Assert.That(mgr.StableRandom(7), Is.EqualTo(expected), $"parity drift at roll {i}");
            }
        }
    }
}
