#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(() => 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(() => { var _ = BattleManagerBase.IsForecast; }); Assert.Throws(() => 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(() => { var _ = BattleManagerBase.IsRandomDraw; }); Assert.Throws(() => 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(() => { 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(() => 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)); } }