using System.Collections.Generic;
using System.Reflection;
using Eagle0.Tutorial;
using NUnit.Framework;
using Shardok;
using UnityEngine;
using UnityEngine.UI;
using ClientTextProvider = eagle.ClientTextProvider;
using EagleHeroView = Net.Eagle0.Eagle.Views.HeroView;
using EagleProvinceView = Net.Eagle0.Eagle.Views.ProvinceView;

namespace eagle0.Tests {
    [TestFixture]
    public class TutorialDialogueContractTests {
        [Test]
        public void ServerDialogueConversionResolvesTextHeroAndFocus() {
            var textProvider = ClientTextProvider.Provider;
            textProvider.Clear();
            textProvider.HandleNewStreamingText("marek-name", "Old Marek", 0, true);
            textProvider.HandleNewStreamingText("opening-copy", "Hold the line.", 0, true);
            textProvider.HandleNewStreamingText("opening-instruction", "Click Fight!", 0, true);

            var dialogue = new Net.Eagle0.Eagle.Views.TutorialDialogue { Id = "tutorial_opening" };
            dialogue.Steps.Add(new Net.Eagle0.Eagle.Views.TutorialDialogueStep {
                SpeakerHeroId = 17,
                DialogueTextId = "opening-copy",
                InstructionTextId = "opening-instruction",
                Focus = new Net.Eagle0.Eagle.Views.TutorialFocus { ButtonId = "FightButton" }
            });
            var heroes = new Dictionary<int, EagleHeroView> {
                [17] =
                        new EagleHeroView {
                            Id = 17,
                            NameTextId = "marek-name",
                            ImagePath = "fixed/old_marek.png"
                        }
            };

            var converted = DialogueManager.TryCreateServerScript(
                    dialogue,
                    heroes,
                    new Dictionary<int, EagleProvinceView>(),
                    textProvider,
                    out var script);

            Assert.IsTrue(converted);
            Assert.AreEqual("tutorial_opening", script.id);
            Assert.AreEqual("Old Marek", script.steps[0].speakerName);
            Assert.AreEqual("fixed/old_marek.png", script.steps[0].speakerImagePath);
            Assert.AreEqual("Hold the line.", script.steps[0].dialogueText);
            Assert.AreEqual("Click Fight!", script.steps[0].instructionText);
            CollectionAssert.AreEqual(new[] { "FightButton" }, script.steps[0].highlightTargets);
            Assert.IsTrue(script.steps[0].persistHighlight);
            textProvider.Clear();
        }

        [Test]
        public void ServerDialogueConversionResolvesMultipleUiFocusTargets() {
            var textProvider = ClientTextProvider.Provider;
            textProvider.Clear();
            textProvider.HandleNewStreamingText("marek-name", "Old Marek", 0, true);
            textProvider.HandleNewStreamingText("arm-copy", "Arm the troops.", 0, true);

            var uiElements = new Net.Eagle0.Eagle.Views.TutorialUiElements();
            uiElements.ElementIds.Add("VisitTownButton");
            uiElements.ElementIds.Add("BattalionArmamentValues");
            uiElements.ElementIds.Add("InfrastructureField");
            var dialogue = new Net.Eagle0.Eagle.Views.TutorialDialogue {
                Id = "tutorial_attack_prepare_arm"
            };
            dialogue.Steps.Add(new Net.Eagle0.Eagle.Views.TutorialDialogueStep {
                SpeakerHeroId = 17,
                DialogueTextId = "arm-copy",
                Focus = new Net.Eagle0.Eagle.Views.TutorialFocus { UiElements = uiElements }
            });

            var converted = DialogueManager.TryCreateServerScript(
                    dialogue,
                    new Dictionary<int, EagleHeroView> {
                        [17] = new EagleHeroView { Id = 17, NameTextId = "marek-name" }
                    },
                    new Dictionary<int, EagleProvinceView>(),
                    textProvider,
                    out var script);

            Assert.IsTrue(converted);
            CollectionAssert.AreEqual(
                    new[] { "VisitTownButton", "BattalionArmamentValues", "InfrastructureField" },
                    script.steps[0].highlightTargets);
            Assert.IsTrue(script.steps[0].persistHighlight);
            textProvider.Clear();
        }

        [Test]
        public void ServerDialogueConversionWaitsForCompletedText() {
            var textProvider = ClientTextProvider.Provider;
            textProvider.Clear();
            textProvider.HandleNewStreamingText("marek-name", "Old Marek", 0, true);
            textProvider.HandleNewStreamingText("opening-copy", "Partial", 0, false);

            var dialogue = new Net.Eagle0.Eagle.Views.TutorialDialogue { Id = "tutorial_opening" };
            dialogue.Steps.Add(new Net.Eagle0.Eagle.Views.TutorialDialogueStep {
                SpeakerHeroId = 17,
                DialogueTextId = "opening-copy",
                Focus = new Net.Eagle0.Eagle.Views.TutorialFocus { ProvinceId = 14 }
            });
            var heroes = new Dictionary<int, EagleHeroView> {
                [17] = new EagleHeroView { Id = 17, NameTextId = "marek-name" }
            };
            var provinces = new Dictionary<int, EagleProvinceView> {
                [14] = new EagleProvinceView { Id = 14, Name = "Onmaa" }
            };

            var converted = DialogueManager.TryCreateServerScript(
                    dialogue,
                    heroes,
                    provinces,
                    textProvider,
                    out var script);

            Assert.IsFalse(converted);
            Assert.IsNull(script);

            textProvider.HandleNewStreamingText("opening-copy", "Ready", 0, true);
            converted = DialogueManager.TryCreateServerScript(
                    dialogue,
                    heroes,
                    provinces,
                    textProvider,
                    out script);

            Assert.IsTrue(converted);
            Assert.AreEqual("Ready", script.steps[0].dialogueText);
            Assert.AreEqual("Onmaa", script.steps[0].highlightProvince);
            textProvider.Clear();
        }

        [Test]
        public void TacticalServerDialogueConversionResolvesReplacementAndUnitFocus() {
            var textProvider = ClientTextProvider.Provider;
            textProvider.Clear();
            textProvider.HandleNewStreamingText("marek-name", "Old Marek", 0, true);
            textProvider.HandleNewStreamingText("captured-name", "Ikhaan Tarn", 0, true);
            textProvider.HandleNewStreamingText("capture-copy", "We captured {heroName}.", 0, true);
            textProvider
                    .HandleNewStreamingText("capture-instruction", "Guard {heroName}.", 0, true);

            var dialogue = new Net.Eagle0.Eagle.Views.TutorialDialogue {
                Id = "tutorial_enemy_hero_captured"
            };
            var step = new Net.Eagle0.Eagle.Views.TutorialDialogueStep {
                SpeakerHeroId = 17,
                DialogueTextId = "capture-copy",
                InstructionTextId = "capture-instruction",
                Focus = new Net.Eagle0.Eagle.Views.TutorialFocus { UnitId = 314 }
            };
            step.TextReplacements.Add(new Net.Eagle0.Eagle.Views.TutorialTextReplacement {
                Token = "{heroName}",
                HeroId = 29
            });
            dialogue.Steps.Add(step);
            var heroes = new Dictionary<int, EagleHeroView> {
                [17] = new EagleHeroView { Id = 17, NameTextId = "marek-name" },
                [29] = new EagleHeroView { Id = 29, NameTextId = "captured-name" }
            };

            var converted = DialogueManager.TryCreateServerScript(
                    dialogue,
                    heroes,
                    new Dictionary<int, EagleProvinceView>(),
                    textProvider,
                    out var script);

            Assert.IsTrue(converted);
            Assert.AreEqual("We captured Ikhaan Tarn.", script.steps[0].dialogueText);
            Assert.AreEqual("Guard Ikhaan Tarn.", script.steps[0].instructionText);
            Assert.AreEqual(314, script.steps[0].highlightUnit.unitId);
            textProvider.Clear();
        }

        [Test]
        public void ServerDialogueConversionResolvesProvinceReplacement() {
            var textProvider = ClientTextProvider.Provider;
            textProvider.Clear();
            textProvider.HandleNewStreamingText("marek-name", "Old Marek", 0, true);
            textProvider.HandleNewStreamingText(
                    "ready-copy",
                    "A hero in {provinceName} is ready to join.",
                    0,
                    true);
            textProvider.HandleNewStreamingText(
                    "ready-instruction",
                    "Visit Town in {provinceName}.",
                    0,
                    true);

            var dialogue =
                    new Net.Eagle0.Eagle.Views.TutorialDialogue { Id = "tutorial_ready_to_join" };
            var step = new Net.Eagle0.Eagle.Views.TutorialDialogueStep {
                SpeakerHeroId = 17,
                DialogueTextId = "ready-copy",
                InstructionTextId = "ready-instruction"
            };
            step.TextReplacements.Add(new Net.Eagle0.Eagle.Views.TutorialTextReplacement {
                Token = "{provinceName}",
                ProvinceId = 14
            });
            dialogue.Steps.Add(step);
            var heroes = new Dictionary<int, EagleHeroView> {
                [17] = new EagleHeroView { Id = 17, NameTextId = "marek-name" }
            };
            var provinces = new Dictionary<int, EagleProvinceView> {
                [14] = new EagleProvinceView { Id = 14, Name = "Onmaa" }
            };

            var converted = DialogueManager.TryCreateServerScript(
                    dialogue,
                    heroes,
                    provinces,
                    textProvider,
                    out var script);

            Assert.IsTrue(converted);
            Assert.AreEqual("A hero in Onmaa is ready to join.", script.steps[0].dialogueText);
            Assert.AreEqual("Visit Town in Onmaa.", script.steps[0].instructionText);
            textProvider.Clear();
        }

        [Test]
        public void ServerDialogueConversionResolvesFactionReplacement() {
            var textProvider = ClientTextProvider.Provider;
            textProvider.Clear();
            textProvider.HandleNewStreamingText("marek-name", "Old Marek", 0, true);
            textProvider.HandleNewStreamingText(
                    "border-copy",
                    "We've earned support on the border with {factionName}.",
                    0,
                    true);

            var dialogue = new Net.Eagle0.Eagle.Views.TutorialDialogue {
                Id = "tutorial_attack_prepare_organize"
            };
            var step = new Net.Eagle0.Eagle.Views.TutorialDialogueStep {
                SpeakerHeroId = 17,
                DialogueTextId = "border-copy"
            };
            step.TextReplacements.Add(new Net.Eagle0.Eagle.Views.TutorialTextReplacement {
                Token = "{factionName}",
                FactionId = 5
            });
            dialogue.Steps.Add(step);
            var heroes = new Dictionary<int, EagleHeroView> {
                [17] = new EagleHeroView { Id = 17, NameTextId = "marek-name" }
            };

            var converted = DialogueManager.TryCreateServerScript(
                    dialogue,
                    heroes,
                    new Dictionary<int, EagleProvinceView>(),
                    textProvider,
                    out var script,
                    factionId => factionId == 5 ? "The Border League" : null);

            Assert.IsTrue(converted);
            Assert.AreEqual(
                    "We've earned support on the border with The Border League.",
                    script.steps[0].dialogueText);
            textProvider.Clear();
        }

        [Test]
        public void TacticalServerDialogueWaitsForBattleUi() {
            var gameObject = new GameObject("DialogueManager test");
            var textProvider = ClientTextProvider.Provider;
            try {
                textProvider.Clear();
                textProvider.HandleNewStreamingText("marek-name", "Old Marek", 0, true);
                textProvider.HandleNewStreamingText("placement-copy", "Place the unit.", 0, true);
                var manager = gameObject.AddComponent<DialogueManager>();
                var updater = new eagle.GameModelUpdater(987654321, null, null);
                var model = GetPrivateField<eagle.IGameModel>(updater, "_currentModel");
                model.Heroes[17] = new EagleHeroView { Id = 17, NameTextId = "marek-name" };
                manager.OnEagleModelUpdated(model);

                var dialogue =
                        new Net.Eagle0.Eagle.Views.TutorialDialogue { Id = "tutorial_placement" };
                dialogue.Steps.Add(new Net.Eagle0.Eagle.Views.TutorialDialogueStep {
                    SpeakerHeroId = 17,
                    DialogueTextId = "placement-copy"
                });

                manager.TriggerTacticalServerDialogue(dialogue);

                Assert.IsFalse(manager.IsDialogueActive);
                Assert.IsTrue(manager.HasReceivedServerDialogue("tutorial_placement"));
                Assert.AreEqual(
                        1,
                        GetPrivateField<Queue<Net.Eagle0.Eagle.Views.TutorialDialogue>>(
                                manager,
                                "_pendingTacticalServerDialogues")
                                .Count);
            } finally {
                textProvider.Clear();
                Object.DestroyImmediate(gameObject);
            }
        }

        [Test]
        public void ChangingGamesClearsThePreviousGameModelBeforeDialogueResolution() {
            var gameObject = new GameObject("DialogueManager test");
            try {
                var manager = gameObject.AddComponent<DialogueManager>();
                var panelObject = new GameObject("Dialogue panel", typeof(RectTransform));
                panelObject.transform.SetParent(gameObject.transform);
                manager.panelController = panelObject.AddComponent<DialoguePanelController>();
                var continueObject = new GameObject(
                        "Continue",
                        typeof(RectTransform),
                        typeof(UnityEngine.UI.Button));
                continueObject.transform.SetParent(panelObject.transform);
                manager.panelController.continueButton =
                        continueObject.GetComponent<UnityEngine.UI.Button>();
                var skipObject = new GameObject(
                        "Skip",
                        typeof(RectTransform),
                        typeof(UnityEngine.UI.Button));
                skipObject.transform.SetParent(panelObject.transform);
                manager.panelController.skipButton =
                        skipObject.GetComponent<UnityEngine.UI.Button>();
                var updater = new eagle.GameModelUpdater(123, null, null);
                var previousModel = GetPrivateField<eagle.IGameModel>(updater, "_currentModel");

                manager.UseProgressForGame(previousModel.GameId);
                manager.OnEagleModelUpdated(previousModel);
                Assert.AreSame(
                        previousModel,
                        GetPrivateField<eagle.IGameModel>(manager, "_eagleModel"));

                manager.UseProgressForGame(gameId: 456);

                Assert.IsNull(GetPrivateField<eagle.IGameModel>(manager, "_eagleModel"));
            } finally { Object.DestroyImmediate(gameObject); }
        }

        [Test]
        public void ResetBattleDialoguesAllowsFreshServerCopies() {
            var gameObject = new GameObject("DialogueManager test");
            try {
                var manager = gameObject.AddComponent<DialogueManager>();
                manager.TriggerTacticalServerDialogue(
                        new Net.Eagle0.Eagle.Views.TutorialDialogue { Id = "tutorial_placement" });

                manager.ResetCompletedScriptsById(new[] { "tutorial_placement" });

                Assert.IsFalse(manager.HasReceivedServerDialogue("tutorial_placement"));
                Assert.AreEqual(
                        0,
                        GetPrivateField<Queue<Net.Eagle0.Eagle.Views.TutorialDialogue>>(
                                manager,
                                "_pendingTacticalServerDialogues")
                                .Count);
            } finally { Object.DestroyImmediate(gameObject); }
        }

        [Test]
        public void InactiveTacticalQueueDoesNotBlockStrategicServerDialogue() {
            var gameObject = new GameObject("DialogueManager test");
            try {
                var manager = gameObject.AddComponent<DialogueManager>();
                manager.TriggerTacticalServerDialogue(
                        new Net.Eagle0.Eagle.Views.TutorialDialogue { Id = "tutorial_placement" });
                manager.TriggerServerDialogue(
                        new Net.Eagle0.Eagle.Views.TutorialDialogue { Id = "tutorial_opening" });

                var playable = InvokePrivate<Queue<Net.Eagle0.Eagle.Views.TutorialDialogue>>(
                        manager,
                        "PendingServerDialogueQueue");
                Assert.AreEqual("tutorial_opening", playable.Peek().Id);
            } finally { Object.DestroyImmediate(gameObject); }
        }

        [Test]
        public void StrategicServerDialogueWaitsUntilBattleExit() {
            var managerObject = new GameObject("DialogueManager test");
            var shardokObject = new GameObject("ShardokGameController test");
            try {
                var manager = managerObject.AddComponent<DialogueManager>();
                var shardokController = shardokObject.AddComponent<ShardokGameController>();
                SetShardokModel(shardokController, CreateShardokModel());
                manager.SetShardokController(shardokController);
                manager.TriggerServerDialogue(new Net.Eagle0.Eagle.Views.TutorialDialogue {
                    Id = "post_battle_aftermath"
                });

                Assert.IsNull(InvokePrivate<Queue<Net.Eagle0.Eagle.Views.TutorialDialogue>>(
                        manager,
                        "PendingServerDialogueQueue"));

                SetShardokModel(shardokController, null);

                var playable = InvokePrivate<Queue<Net.Eagle0.Eagle.Views.TutorialDialogue>>(
                        manager,
                        "PendingServerDialogueQueue");
                Assert.AreEqual("post_battle_aftermath", playable.Peek().Id);
            } finally {
                Object.DestroyImmediate(managerObject);
                Object.DestroyImmediate(shardokObject);
            }
        }

        [Test]
        public void CompletedServerDialogueIsRemovedFromPendingQueue() {
            var gameObject = new GameObject("DialogueManager test");
            try {
                var manager = gameObject.AddComponent<DialogueManager>();
                GetPrivateField<HashSet<string>>(manager, "_completedScriptIds")
                        .Add("post_battle_aftermath");

                manager.TriggerServerDialogue(new Net.Eagle0.Eagle.Views.TutorialDialogue {
                    Id = "post_battle_aftermath"
                });

                Assert.AreEqual(
                        0,
                        GetPrivateField<Queue<Net.Eagle0.Eagle.Views.TutorialDialogue>>(
                                manager,
                                "_pendingServerDialogues")
                                .Count);
                Assert.IsFalse(manager.IsDialogueActive);
            } finally { Object.DestroyImmediate(gameObject); }
        }

        [Test]
        public void FinalUiHighlightsPersistUntilPlayerActs() {
            var managerObject = new GameObject("DialogueManager test");
            var panelObject = new GameObject("DialoguePanel test", typeof(RectTransform));
            var targetObject = new GameObject("LoyaltyLabel test", typeof(RectTransform));
            var secondTargetObject =
                    new GameObject("InfrastructureField test", typeof(RectTransform));
            try {
                var manager = managerObject.AddComponent<DialogueManager>();
                var panel = panelObject.AddComponent<DialoguePanelController>();
                panel.continueButton = new GameObject("Continue").AddComponent<Button>();
                panel.continueButton.transform.SetParent(panelObject.transform);
                panel.skipButton = new GameObject("Skip").AddComponent<Button>();
                panel.skipButton.transform.SetParent(panelObject.transform);
                manager.panelController = panel;
                SetPrivateField(
                        manager,
                        "_activeScript",
                        new DialogueScript {
                            id = "tutorial_loyalty_warning",
                            steps = new List<DialogueStep> { new DialogueStep {
                                highlightTargets =
                                        new List<string> { "LoyaltyLabel", "InfrastructureField" },
                                persistHighlight = true
                            } }
                        });

                InvokePrivate(
                        manager,
                        "HighlightRects",
                        new List<RectTransform> {
                            targetObject.GetComponent<RectTransform>(),
                            secondTargetObject.GetComponent<RectTransform>()
                        });
                Assert.AreEqual(
                        2,
                        GetPrivateField<List<GameObject>>(manager, "_activeHighlights").Count);

                manager.EndDialogueWithoutCompletion();

                Assert.AreEqual(
                        2,
                        GetPrivateField<List<GameObject>>(manager, "_activeHighlights").Count);

                manager.OnPlayerAction();

                Assert.IsEmpty(GetPrivateField<List<GameObject>>(manager, "_activeHighlights"));
                Assert.IsEmpty(GetPrivateField<List<Image>>(manager, "_highlightEdges"));
            } finally {
                Object.DestroyImmediate(managerObject);
                Object.DestroyImmediate(panelObject);
                Object.DestroyImmediate(targetObject);
                Object.DestroyImmediate(secondTargetObject);
            }
        }

        private static T GetPrivateField<T>(object target, string fieldName) {
            var field = target.GetType().GetField(
                    fieldName,
                    BindingFlags.Instance | BindingFlags.NonPublic);
            Assert.NotNull(field, $"Missing private field '{fieldName}'");
            return (T)field.GetValue(target);
        }

        private static void SetPrivateField<T>(object target, string fieldName, T value) {
            var field = target.GetType().GetField(
                    fieldName,
                    BindingFlags.Instance | BindingFlags.NonPublic);
            Assert.NotNull(field, $"Missing private field '{fieldName}'");
            field.SetValue(target, value);
        }

        private static T InvokePrivate<T>(object target, string methodName) {
            var method = target.GetType().GetMethod(
                    methodName,
                    BindingFlags.Instance | BindingFlags.NonPublic);
            Assert.NotNull(method, $"Missing private method '{methodName}'");
            return (T)method.Invoke(target, null);
        }

        private static void InvokePrivate(object target, string methodName, params object[] args) {
            var method = target.GetType().GetMethod(
                    methodName,
                    BindingFlags.Instance | BindingFlags.NonPublic);
            Assert.NotNull(method, $"Missing private method '{methodName}'");
            method.Invoke(target, args);
        }

        private static ShardokGameModel CreateShardokModel() {
            return new ShardokGameModel(
                    987654321,
                    "battle",
                    1,
                    null,
                    null,
                    "Test",
                    1,
                    new List<ShardokGameModel.PlayerWithHostility>(),
                    new Dictionary<int, string>(),
                    new Dictionary<int, string>(),
                    new Dictionary<int, string>());
        }

        private static void SetShardokModel(
                ShardokGameController controller,
                ShardokGameModel model) {
            var property = typeof(ShardokGameController)
                                   .GetProperty(
                                           nameof(ShardokGameController.Model),
                                           BindingFlags.Instance | BindingFlags.Public);
            Assert.NotNull(property);
            property.SetValue(controller, model);
        }
    }
}
