#nullable enable
using SVSim.BattleEngine.Ambient;
using NUnit.Framework;
using System.Runtime.Serialization;
using System.Threading.Tasks;

namespace SVSim.BattleEngine.Tests;

[TestFixture, Parallelizable(ParallelScope.Self)]
public class BattleAmbientTests
{
    [Test]
    public void Current_IsNull_WhenNoScope()
    {
        Assert.That(BattleAmbient.Current, Is.Null);
    }

    [Test]
    public void Require_Throws_WhenNoScope()
    {
        Assert.Throws<System.InvalidOperationException>(() => BattleAmbient.Require());
    }

    [Test]
    public void Enter_SetsCurrent_RestoresOnDispose()
    {
        var ctx = new BattleAmbientContext { ViewerId = 42 };
        Assert.That(BattleAmbient.Current, Is.Null);
        using (var _ = BattleAmbient.Enter(ctx))
        {
            Assert.That(BattleAmbient.Current, Is.SameAs(ctx));
            Assert.That(BattleAmbient.Require().ViewerId, Is.EqualTo(42));
        }
        Assert.That(BattleAmbient.Current, Is.Null);
    }

    [Test]
    public void Enter_Nested_RestoresPriorOnDispose()
    {
        var outer = new BattleAmbientContext { ViewerId = 1 };
        var inner = new BattleAmbientContext { ViewerId = 2 };
        using (var _o = BattleAmbient.Enter(outer))
        {
            Assert.That(BattleAmbient.Current!.ViewerId, Is.EqualTo(1));
            using (var _i = BattleAmbient.Enter(inner))
            {
                Assert.That(BattleAmbient.Current!.ViewerId, Is.EqualTo(2));
            }
            Assert.That(BattleAmbient.Current!.ViewerId, Is.EqualTo(1));
        }
    }

    [Test]
    public async Task Enter_FlowsAcrossAwait()
    {
        var ctx = new BattleAmbientContext { ViewerId = 99 };
        using (var _ = BattleAmbient.Enter(ctx))
        {
            await Task.Yield();
            Assert.That(BattleAmbient.Current, Is.SameAs(ctx));
        }
    }

    [Test]
    public async Task Enter_IsolatedBetweenConcurrentTasks()
    {
        var ctxA = new BattleAmbientContext { ViewerId = 100 };
        var ctxB = new BattleAmbientContext { ViewerId = 200 };

        var taskA = Task.Run(async () => {
            using var _ = BattleAmbient.Enter(ctxA);
            await Task.Delay(20);
            return BattleAmbient.Current!.ViewerId;
        });
        var taskB = Task.Run(async () => {
            using var _ = BattleAmbient.Enter(ctxB);
            await Task.Delay(20);
            return BattleAmbient.Current!.ViewerId;
        });

        var results = await Task.WhenAll(taskA, taskB);
        Assert.That(results[0], Is.EqualTo(100));
        Assert.That(results[1], Is.EqualTo(200));
    }

    [Test]
    public void IsForecast_ReadsAmbient_WhenScopeActive()
    {
        var ctx = new BattleAmbientContext { IsForecast = false };
        using var _ = BattleAmbient.Enter(ctx);
        Assert.That(BattleManagerBase.IsForecast, Is.False);
        ctx.IsForecast = true;
        Assert.That(BattleManagerBase.IsForecast, Is.True);
    }

    [Test]
    public void IsForecast_WriteInsideScope_WritesAmbient_NotFallback()
    {
        var ctx = new BattleAmbientContext { IsForecast = false };
        using (var _ = BattleAmbient.Enter(ctx))
        {
            BattleManagerBase.IsForecast = true;
            Assert.That(ctx.IsForecast, Is.True);
        }
    }

    [Test]
    public void IsForecast_OutsideScope_GetAndSetThrow()
    {
        // Post-Task-8: fallback is gone. Both get and set go through BattleAmbient.Require(),
        // which throws when no scope is active. This is the forcing function — any unwrapped
        // engine code that touches IsForecast fails fast instead of silently writing a static.
        Assert.That(BattleAmbient.Current, Is.Null);
        Assert.Throws<System.InvalidOperationException>(() => { var _ = BattleManagerBase.IsForecast; });
        Assert.Throws<System.InvalidOperationException>(() => BattleManagerBase.IsForecast = true);
    }

    [Test]
    public void IsRandomDraw_OutsideScope_GetAndSetThrow_InsideScope_Roundtrips()
    {
        // Post-Task-8: get/set both Require() a scope. Inside a scope, writes land on the ctx.
        Assert.That(BattleAmbient.Current, Is.Null);
        Assert.Throws<System.InvalidOperationException>(() => { var _ = BattleManagerBase.IsRandomDraw; });
        Assert.Throws<System.InvalidOperationException>(() => BattleManagerBase.IsRandomDraw = true);

        var ctx = new BattleAmbientContext { IsRandomDraw = false };
        using (var _ = BattleAmbient.Enter(ctx))
        {
            Assert.That(BattleManagerBase.IsRandomDraw, Is.False);
            BattleManagerBase.IsRandomDraw = true;
            Assert.That(ctx.IsRandomDraw, Is.True);
        }

        // Scope disposed -> back to throwing on access.
        Assert.Throws<System.InvalidOperationException>(() => { var _ = BattleManagerBase.IsRandomDraw; });
    }

    [Test]
    public void GetIns_ReadsAmbient_WhenScopeActive()
    {
        var fakeMgr = (BattleManagerBase)System.Runtime.Serialization
            .FormatterServices.GetUninitializedObject(typeof(BattleManagerBase));
        var ctx = new BattleAmbientContext { Mgr = fakeMgr };
        using var _ = BattleAmbient.Enter(ctx);
        Assert.That(BattleManagerBase.GetIns(), Is.SameAs(fakeMgr));
    }

    [Test]
    public void GetIns_OutsideScope_ReturnsNull()
    {
        // Post-Task-8: fallback is gone. GetIns() reads Current?.Mgr (soft, kept null-tolerant so
        // engine call sites that pattern `GetIns()?.Foo ?? default` still compose). With no scope
        // active, Current is null, so GetIns() returns null.
        Assert.That(BattleAmbient.Current, Is.Null);
        Assert.That(BattleManagerBase.GetIns(), Is.Null);
    }

    [Test]
    public void ViewerId_ReadsAmbient_WhenScopeActive()
    {
        var ctx = new BattleAmbientContext { ViewerId = 12345 };
        using var _ = BattleAmbient.Enter(ctx);
        Assert.That(Cute.Certification.ViewerId, Is.EqualTo(12345));
    }

    [Test]
    public void RealTimeNetworkAgent_ReadsAmbient_WhenScopeActive()
    {
        var ctx = new BattleAmbientContext();
        using var _ = BattleAmbient.Enter(ctx);
        Assert.That(Wizard.ToolboxGame.RealTimeNetworkAgent, Is.Null);
        var agent = (RealTimeNetworkAgent)System.Runtime.Serialization
            .FormatterServices.GetUninitializedObject(typeof(RealTimeNetworkAgent));
        ctx.NetworkAgent = agent;
        Assert.That(Wizard.ToolboxGame.RealTimeNetworkAgent, Is.SameAs(agent));
    }

    [Test]
    public void SetRealTimeNetworkBattle_InsideScope_WritesAmbient()
    {
        var ctx = new BattleAmbientContext();
        using var _ = BattleAmbient.Enter(ctx);
        var agent = (RealTimeNetworkAgent)System.Runtime.Serialization
            .FormatterServices.GetUninitializedObject(typeof(RealTimeNetworkAgent));
        Wizard.ToolboxGame.SetRealTimeNetworkBattle(agent);
        Assert.That(ctx.NetworkAgent, Is.SameAs(agent));
    }

    [Test]
    public void BattleRecoveryInfo_ReadsAmbient_WhenScopeActive()
    {
        var info = (Wizard.BattleRecoveryInfo)System.Runtime.Serialization
            .FormatterServices.GetUninitializedObject(typeof(Wizard.BattleRecoveryInfo));
        var ctx = new BattleAmbientContext { RecoveryInfo = info };
        using var _ = BattleAmbient.Enter(ctx);
        Assert.That(Wizard.Data.BattleRecoveryInfo, Is.SameAs(info));
    }

    [Test]
    public void BattleRecoveryInfo_SetInsideScope_WritesAmbient()
    {
        var ctx = new BattleAmbientContext();
        using var _ = BattleAmbient.Enter(ctx);
        var info = (Wizard.BattleRecoveryInfo)System.Runtime.Serialization
            .FormatterServices.GetUninitializedObject(typeof(Wizard.BattleRecoveryInfo));
        Wizard.Data.BattleRecoveryInfo = info;
        Assert.That(ctx.RecoveryInfo, Is.SameAs(info));
    }

    [Test]
    public void GameMgr_GetIns_InsideScope_ReturnsScopeInstance()
    {
        var mgr = new GameMgr();
        var ctx = new BattleAmbientContext { GameMgr = mgr };
        using var _ = BattleAmbient.Enter(ctx);
        Assert.That(GameMgr.GetIns(), Is.SameAs(mgr));
    }

    [Test]
    public void GameMgr_GetIns_OutsideScope_Throws()
    {
        Assert.That(BattleAmbient.Current, Is.Null);
        Assert.Throws<System.InvalidOperationException>(() => GameMgr.GetIns());
    }

    [Test]
    public async Task GameMgr_GetIns_IsolatedBetweenConcurrentTasks()
    {
        var mgrA = new GameMgr();
        var mgrB = new GameMgr();

        var taskA = Task.Run(async () => {
            using var _ = BattleAmbient.Enter(new BattleAmbientContext { GameMgr = mgrA });
            await Task.Delay(20);
            return GameMgr.GetIns();
        });
        var taskB = Task.Run(async () => {
            using var _ = BattleAmbient.Enter(new BattleAmbientContext { GameMgr = mgrB });
            await Task.Delay(20);
            return GameMgr.GetIns();
        });
        var results = await Task.WhenAll(taskA, taskB);
        Assert.That(results[0], Is.SameAs(mgrA));
        Assert.That(results[1], Is.SameAs(mgrB));
    }
}
