using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.App.Tests.UI.Layout; using AcDream.Core.Net.Messages; namespace AcDream.App.Tests.UI; public sealed class GameplayConfirmationControllerTests { [Fact] public void SkillRequestAppendsContinueAndCloseNoticeSendsServerTuple() { var root = new UiRoot { Width = 800f, Height = 600f }; ImportedLayout? shown = null; var factory = new RetailDialogFactory(root, _ => shown = FixtureLoader.LoadConfirmationDialog()); var responses = new List<(uint Type, uint Context, bool Accepted)>(); using var controller = new GameplayConfirmationController( factory, (type, context, accepted) => responses.Add((type, context, accepted))); Assert.True(controller.HandleRequest( new GameEvents.CharacterConfirmationRequest(2u, 42u, "Raise this skill?"))); Assert.Equal( "Raise this skill? Continue?", string.Join(" ", Assert.IsType(shown!.FindElement( RetailConfirmationDialogView.MessageElementId)).LinesProvider().Select(static line => line.Text))); Assert.IsType(shown.FindElement( RetailConfirmationDialogView.AcceptButtonId)).OnClick!(); Assert.Equal([(2u, 42u, true)], responses); Assert.Equal(0u, controller.ActiveDialogContext); } /// /// MUST-FIX 2 (FA4 fix round, 2026-08-12): a fellowship invite /// (ConfirmationType.Fellowship, type 4) reaches this generic /// controller and opens a dialog exactly like any other type — no /// client-side interceptor exists anymore (D6's correction: retail's /// client reads neither option bit on the invite path; /// RetailUiRuntime.HandleConfirmationRequest now routes every /// type, including 4, straight here). Type 4 is NOT in the /// 2/3/5/6 " Continue?"-suffix set, so with no composer injected the /// message renders verbatim — matching /// Handle_Character__ConfirmationRequest @0x005640A0's case-4 arm. /// (Production now injects the StringTable template composer — see /// ; /// this test remains the null-composer fallback contract.) /// [Fact] public void FellowshipInviteRequest_Type4_OpensDialog_MessageVerbatim_AndSendsAcceptOnClose() { var root = new UiRoot { Width = 800f, Height = 600f }; ImportedLayout? shown = null; var factory = new RetailDialogFactory(root, _ => shown = FixtureLoader.LoadConfirmationDialog()); var responses = new List<(uint Type, uint Context, bool Accepted)>(); using var controller = new GameplayConfirmationController( factory, (type, context, accepted) => responses.Add((type, context, accepted))); Assert.True(controller.HandleRequest( new GameEvents.CharacterConfirmationRequest(4u, 7u, "Alice invites you to join their fellowship."))); Assert.Equal( "Alice invites you to join their fellowship.", string.Join(" ", Assert.IsType(shown!.FindElement( RetailConfirmationDialogView.MessageElementId)).LinesProvider().Select(static line => line.Text))); Assert.IsType(shown.FindElement( RetailConfirmationDialogView.AcceptButtonId)).OnClick!(); Assert.Equal([(4u, 7u, true)], responses); Assert.Equal(0u, controller.ActiveDialogContext); } /// /// 2026-08-13 social gate round 2 (the AD-85 narrowing): with an /// injected composer, types 1/4 render the StringTable-composed /// sentence instead of ACE's bare name; the 2/3/5/6 " Continue?" family /// never consults the composer; and a null compose result falls back to /// the bare wire message. /// [Fact] public void InjectedComposerWrapsTypes1And4_AndNeverTouchesContinueFamily() { var root = new UiRoot { Width = 800f, Height = 600f }; ImportedLayout? shown = null; var factory = new RetailDialogFactory(root, _ => shown = FixtureLoader.LoadConfirmationDialog()); var composed = new List(); using var controller = new GameplayConfirmationController( factory, (_, _, _) => { }, (type, bareName) => { composed.Add(type); return type == 4u ? bareName + " has invited you to join their fellowship. Do you accept?" : null; }); Assert.True(controller.HandleRequest( new GameEvents.CharacterConfirmationRequest(4u, 7u, "Alice"))); Assert.Equal( "Alice has invited you to join their fellowship. Do you accept?", string.Join(" ", Assert.IsType(shown!.FindElement( RetailConfirmationDialogView.MessageElementId)).LinesProvider() .Select(static line => line.Text))); Assert.IsType(shown.FindElement( RetailConfirmationDialogView.AcceptButtonId)).OnClick!(); // Null compose result (type 1 here) → the bare wire message. Assert.True(controller.HandleRequest( new GameEvents.CharacterConfirmationRequest(1u, 8u, "Bob"))); Assert.Equal( "Bob", string.Join(" ", Assert.IsType(shown!.FindElement( RetailConfirmationDialogView.MessageElementId)).LinesProvider() .Select(static line => line.Text))); Assert.IsType(shown.FindElement( RetailConfirmationDialogView.AcceptButtonId)).OnClick!(); // The " Continue?" family bypasses the composer entirely. Assert.True(controller.HandleRequest( new GameEvents.CharacterConfirmationRequest(2u, 9u, "Raise this skill?"))); Assert.Equal( "Raise this skill? Continue?", string.Join(" ", Assert.IsType(shown!.FindElement( RetailConfirmationDialogView.MessageElementId)).LinesProvider() .Select(static line => line.Text))); Assert.Equal([4u, 1u], composed); } /// /// Campaign FA slice FA5, item 3: verifies the allegiance-swear /// confirmation (ConfirmationType.AllegianceSwear, type 1 — /// RecvNotice_SwearAllegianceRequest -> /// Handle_Character__ConfirmationRequest @0x005640A0's case 1) /// reaches this SAME generic controller, mirroring the type-4 test /// above exactly — no allegiance-specific intercept exists (there /// never was one to remove; D6's fellowship correction did not touch /// type 1 at all, but FA5's own contract calls for this explicit check /// since SocialAllegiancePageController is the new panel that /// makes this path reachable). Type 1 is NOT in the 2/3/5/6 " Continue?" /// suffix set, so with no composer injected the message renders verbatim /// — ACE's own type-1 message is the target's BARE name (lane C §6.4: /// Player_Allegiance.cs:91/ConfirmationManager.cs:38). /// (Production now injects the StringTable template composer that wraps /// the name into retail's full sentence — the 2026-08-13 AD-85 /// narrowing; this test remains the null-composer fallback contract.) /// [Fact] public void AllegianceSwearRequest_Type1_OpensDialog_MessageVerbatim_AndSendsAcceptOnClose() { var root = new UiRoot { Width = 800f, Height = 600f }; ImportedLayout? shown = null; var factory = new RetailDialogFactory(root, _ => shown = FixtureLoader.LoadConfirmationDialog()); var responses = new List<(uint Type, uint Context, bool Accepted)>(); using var controller = new GameplayConfirmationController( factory, (type, context, accepted) => responses.Add((type, context, accepted))); Assert.True(controller.HandleRequest( new GameEvents.CharacterConfirmationRequest(1u, 13u, "Bob"))); Assert.Equal( "Bob", string.Join(" ", Assert.IsType(shown!.FindElement( RetailConfirmationDialogView.MessageElementId)).LinesProvider().Select(static line => line.Text))); Assert.IsType(shown.FindElement( RetailConfirmationDialogView.AcceptButtonId)).OnClick!(); Assert.Equal([(1u, 13u, true)], responses); Assert.Equal(0u, controller.ActiveDialogContext); } [Fact] public void MatchingConfirmationDoneClosesDialogAndUnmatchedTupleDoesNothing() { var root = new UiRoot { Width = 800f, Height = 600f }; var factory = new RetailDialogFactory(root, _ => FixtureLoader.LoadConfirmationDialog()); var responses = new List<(uint Type, uint Context, bool Accepted)>(); using var controller = new GameplayConfirmationController( factory, (type, context, accepted) => responses.Add((type, context, accepted))); controller.HandleRequest(new GameEvents.CharacterConfirmationRequest(7u, 99u, "Proceed?")); Assert.False(controller.HandleDone( new GameEvents.CharacterConfirmationDone(7u, 100u))); Assert.True(factory.IsOpen); Assert.True(controller.HandleDone( new GameEvents.CharacterConfirmationDone(7u, 99u))); Assert.False(factory.IsOpen); Assert.Equal([(7u, 99u, false)], responses); } [Fact] public void FactoryReset_CompletesResponseBeforeSessionTupleIsForgotten() { var root = new UiRoot { Width = 800f, Height = 600f }; var factory = new RetailDialogFactory(root, _ => FixtureLoader.LoadConfirmationDialog()); var responses = new List<(uint Type, uint Context, bool Accepted)>(); using var controller = new GameplayConfirmationController( factory, (type, context, accepted) => responses.Add((type, context, accepted))); controller.HandleRequest( new GameEvents.CharacterConfirmationRequest(7u, 99u, "Proceed?")); factory.Reset(); controller.ResetSession(); Assert.Equal(0u, controller.ActiveDialogContext); Assert.Equal([(7u, 99u, false)], responses); Assert.False(factory.IsOpen); } }