using System.Linq;
using FishDice.LuckyDice;
using NUnit.Framework;

namespace FishDice.Tests.LuckyDice
{
    public sealed class LuckyDiceCoreFlowTests
    {
        [Test]
        public void RollFlowService_runs_normal_reward_chain_from_one_entry()
        {
            var flow = TestFlow(0, 1);

            var result = flow.Execute(new RollFlowRequest("session-flow-normal"));

            Assert.That(result.RollSessionId, Is.EqualTo("session-flow-normal"));
            Assert.That(result.ComboFacts.NormalizedKey, Is.EqualTo("2_3"));
            Assert.That(result.MatchedRules.Select(rule => rule.RuleId), Is.EqualTo(new[] { "normal_all_numbers_reward" }));
            Assert.That(result.Effects.OfType<RewardEffect>().Count(), Is.EqualTo(1));
            Assert.That(result.Effects.OfType<LuckyDiceRequestedEffect>().Any(), Is.False);
            Assert.That(result.LuckyDiceResult, Is.Null);
            Assert.That(result.Trace.ActionResults.Select(action => action.ActionType), Is.EqualTo(new[] { "grant_reward" }));
        }

        [Test]
        public void RollFlowService_resolves_lucky_dice_to_target_mode_from_one_entry()
        {
            var flow = TestFlow(5, 5, 0);

            var result = flow.Execute(new RollFlowRequest("session-flow-lucky"));

            Assert.That(result.ComboFacts.NormalizedKey, Is.EqualTo("clover_clover"));
            Assert.That(result.Effects.OfType<LuckyDiceRequestedEffect>().Count(), Is.EqualTo(1));
            Assert.That(result.LuckyDiceResult.Selected.ResultKey, Is.EqualTo("rocket"));
            Assert.That(result.LuckyDiceResult.Selected, Is.TypeOf<LuckyDiceSelectedResult>());
            Assert.That(typeof(LuckyDiceSelectedResult).GetProperty("Weight"), Is.Null);
            Assert.That(result.LuckyDiceResult.SlotResultKeys, Is.EqualTo(new[] { "rocket", "rocket", "rocket" }));
            Assert.That(result.TargetMode.TargetKey, Is.EqualTo("slap_down"));
            Assert.That(result.TargetMode.ModeKey, Is.EqualTo("slap_down_normal"));
            Assert.That(result.Effects.OfType<EnterTargetModeEffect>().Single().ModeKey, Is.EqualTo("slap_down_normal"));
            Assert.That(result.Trace.LuckyDiceInitialCandidateCount, Is.EqualTo(2));
            Assert.That(result.Trace.LuckyDiceFilteredCandidateCount, Is.EqualTo(2));
            Assert.That(result.Trace.LuckyDiceSelectedResultKey, Is.EqualTo("rocket"));
            Assert.That(result.Trace.ResolvedModeKey, Is.EqualTo("slap_down_normal"));
        }

        [Test]
        public void RollFlowService_uses_injected_random_for_lucky_weighted_pick()
        {
            var flow = TestFlow(5, 5, 1);

            var result = flow.Execute(new RollFlowRequest("session-flow-thief"));

            Assert.That(result.LuckyDiceResult.Selected.ResultKey, Is.EqualTo("thief"));
            Assert.That(result.TargetMode.TargetKey, Is.EqualTo("treasure_heist"));
            Assert.That(result.TargetMode.ModeKey, Is.EqualTo("treasure_heist_normal"));
            Assert.That(result.Trace.LuckyDiceRandomValue, Is.EqualTo(1));
        }

        [Test]
        public void ConfigValidator_reports_unknown_matcher_and_action()
        {
            var rules = new[]
            {
                new ComboRuleDefinition
                {
                    RuleId = "bad_rule",
                    Priority = 1,
                    MatcherType = "missing_matcher",
                    Actions = new[]
                    {
                        new RollActionDefinition { ActionType = "missing_action" }
                    }
                }
            };

            var result = LuckyDiceConfigValidator.Validate(
                rules,
                ValidCandidateDefinitions(),
                ValidTargetModeDefinitions(),
                ComboMatcherRegistry.CreateDefaultMatchers(),
                RollActionExecutorRegistry.CreateDefaultExecutors());

            Assert.That(result.IsValid, Is.False);
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("UnknownMatcher"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("UnknownAction"));
        }

        [Test]
        public void ConfigValidator_reports_null_child_items()
        {
            var rules = new ComboRuleDefinition[]
            {
                null,
                new ComboRuleDefinition
                {
                    RuleId = "rule_with_null_action",
                    Priority = 1,
                    MatcherType = LuckyDiceKeys.AllNumbersMatcher,
                    Actions = new RollActionDefinition[] { null }
                }
            };
            var candidates = new LuckyDiceCandidateDefinition[]
            {
                null,
                new LuckyDiceCandidateDefinition
                {
                    ResultKey = LuckyDiceKeys.RocketResultKey,
                    TargetKey = "slap_down",
                    ResultSlotCount = 3,
                    Weight = 1,
                    Enabled = true,
                    SourceFilter = new[] { LuckyDiceKeys.NormalRollTriggerSource },
                    IsDefault = true
                }
            };
            var targetModes = new TargetModeDefinition[]
            {
                null,
                new TargetModeDefinition
                {
                    ResultKey = LuckyDiceKeys.RocketResultKey,
                    TargetKey = "slap_down",
                    ModeKey = "slap_down_normal"
                }
            };

            var result = LuckyDiceConfigValidator.Validate(
                rules,
                candidates,
                targetModes,
                ComboMatcherRegistry.CreateDefaultMatchers(),
                RollActionExecutorRegistry.CreateDefaultExecutors());

            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullRule"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullAction"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullCandidate"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullTargetMode"));
        }

        [Test]
        public void ConfigValidator_reports_bad_extension_registry_shape()
        {
            var result = LuckyDiceConfigValidator.Validate(
                new ComboRuleDefinition[0],
                ValidCandidateDefinitions(),
                ValidTargetModeDefinitions(),
                new IComboMatcher[]
                {
                    null,
                    new TypedMatcher(""),
                    new TypedMatcher("duplicate_matcher"),
                    new TypedMatcher("duplicate_matcher")
                },
                new IRollActionExecutor[]
                {
                    null,
                    new TypedActionExecutor(""),
                    new TypedActionExecutor("duplicate_action"),
                    new TypedActionExecutor("duplicate_action")
                });

            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullMatcher"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyMatcherType"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("DuplicateMatcherType"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullActionExecutor"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyActionExecutorType"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("DuplicateActionExecutorType"));
        }

        [Test]
        public void ConfigValidator_reports_bad_action_params()
        {
            var rules = new[]
            {
                new ComboRuleDefinition
                {
                    RuleId = "rule_with_bad_params",
                    Priority = 1,
                    MatcherType = LuckyDiceKeys.AllNumbersMatcher,
                    Actions = new[]
                    {
                        new RollActionDefinition
                        {
                            ActionType = LuckyDiceKeys.GrantRewardAction,
                            Params = new ActionParamDefinition[]
                            {
                                null,
                                new ActionParamDefinition { Key = "", Value = "empty" },
                                new ActionParamDefinition { Key = "duplicate", Value = "first" },
                                new ActionParamDefinition { Key = "duplicate", Value = "second" }
                            }
                        }
                    }
                }
            };

            var result = LuckyDiceConfigValidator.Validate(
                rules,
                ValidCandidateDefinitions(),
                ValidTargetModeDefinitions(),
                ComboMatcherRegistry.CreateDefaultMatchers(),
                RollActionExecutorRegistry.CreateDefaultExecutors());

            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullActionParam"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyActionParamKey"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("DuplicateActionParamKey"));
        }

        [Test]
        public void RuntimeFactory_rejects_bad_registry_before_runtime_dictionary_build()
        {
            var context = new LuckyDiceConfigValidationContext(
                new IComboMatcher[]
                {
                    null,
                    new TypedMatcher("duplicate_matcher"),
                    new TypedMatcher("duplicate_matcher")
                },
                new IRollActionExecutor[]
                {
                    null,
                    new TypedActionExecutor("duplicate_action"),
                    new TypedActionExecutor("duplicate_action")
                });
            var factory = new LuckyDiceRuntimeFactory(context, new FixedRandomSource());

            var exception = Assert.Throws<InvalidLuckyDiceConfigException>(() =>
                factory.CreateFlow(
                    new ComboRuleDefinition[0],
                    ValidCandidateDefinitions(),
                    ValidTargetModeDefinitions()));

            Assert.That(exception.ValidationResult.Errors.Select(error => error.Code), Does.Contain("NullMatcher"));
            Assert.That(exception.ValidationResult.Errors.Select(error => error.Code), Does.Contain("DuplicateMatcherType"));
            Assert.That(exception.ValidationResult.Errors.Select(error => error.Code), Does.Contain("NullActionExecutor"));
            Assert.That(exception.ValidationResult.Errors.Select(error => error.Code), Does.Contain("DuplicateActionExecutorType"));
        }

        [Test]
        public void DefaultConfig_rejects_bad_action_params_before_loader_dictionary_build()
        {
            var rules = new[]
            {
                new ComboRuleDefinition
                {
                    RuleId = "rule_with_duplicate_param",
                    Priority = 1,
                    MatcherType = LuckyDiceKeys.AllNumbersMatcher,
                    Actions = new[]
                    {
                        new RollActionDefinition
                        {
                            ActionType = LuckyDiceKeys.GrantRewardAction,
                            Params = new[]
                            {
                                new ActionParamDefinition { Key = "duplicate", Value = "first" },
                                new ActionParamDefinition { Key = "duplicate", Value = "second" }
                            }
                        }
                    }
                }
            };

            var exception = Assert.Throws<InvalidLuckyDiceConfigException>(() =>
                LuckyDiceDefaultConfig.CreateValidated(
                    rules,
                    ValidCandidateDefinitions(),
                    ValidTargetModeDefinitions()));

            Assert.That(exception.ValidationResult.Errors.Select(error => error.Code), Does.Contain("DuplicateActionParamKey"));
        }

        [Test]
        public void LuckyDice_fallback_does_not_bypass_progress_or_source_filters()
        {
            var service = new LuckyDiceFlowService(new FixedRandomSource());
            var candidates = new[]
            {
                new LuckyDiceCandidateConfig(
                    "locked_default",
                    "locked_target",
                    resultSlotCount: 3,
                    weight: 0,
                    enabled: true,
                    minLevel: 10,
                    sourceFilter: new[] { "other_source" },
                    isDefault: true)
            };
            var request = new RollFlowRequest(
                "session-filter",
                LuckyDiceKeys.NormalDiceSetId,
                LuckyDiceKeys.NormalRollTriggerSource,
                playerLevel: 0,
                isTutorial: false);

            var result = service.Select(candidates, request, LuckyDiceDefaultConfig.Create().GetDiceSet(LuckyDiceKeys.LuckyDiceSetId));

            Assert.That(result.Selected, Is.Null);
            Assert.That(result.FallbackType, Is.EqualTo("NormalRewardFallback"));
        }

        [Test]
        public void TargetModeResolver_returns_failure_reason_when_mapping_is_missing()
        {
            var resolver = new TargetModeResolver();
            var request = new RollFlowRequest(
                "session-mode-missing",
                LuckyDiceKeys.NormalDiceSetId,
                LuckyDiceKeys.NormalRollTriggerSource,
                playerLevel: 0,
                isTutorial: false);
            var candidates = new[]
            {
                new LuckyDiceCandidateConfig(
                    "unknown_result",
                    "unknown_target",
                    resultSlotCount: 3,
                    weight: 1,
                    enabled: true,
                    minLevel: 0,
                    sourceFilter: null,
                    isDefault: true)
            };
            var selection = new LuckyDiceFlowService(new FixedRandomSource(0))
                .Select(candidates, request, LuckyDiceDefaultConfig.Create().GetDiceSet(LuckyDiceKeys.LuckyDiceSetId));

            var result = resolver.Resolve(selection.Selected, request, 1, LuckyDiceDefaultConfig.Create().TargetModeDefinitions);

            Assert.That(result.Success, Is.False);
            Assert.That(result.FailureReason, Is.EqualTo("MissingTargetMode"));
            Assert.That(result.FallbackMode, Is.Null);
        }

        [Test]
        public void ConfigValidator_reports_bad_candidate_shape()
        {
            var candidates = new[]
            {
                new LuckyDiceCandidateDefinition { ResultKey = "", TargetKey = "target", ResultSlotCount = 0, Weight = -1, Enabled = false, IsDefault = true },
                new LuckyDiceCandidateDefinition { ResultKey = "", TargetKey = "target", ResultSlotCount = 3, Weight = 1, Enabled = true }
            };

            var result = LuckyDiceConfigValidator.Validate(
                new ComboRuleDefinition[0],
                candidates,
                ValidTargetModeDefinitions(),
                ComboMatcherRegistry.CreateDefaultMatchers(),
                RollActionExecutorRegistry.CreateDefaultExecutors());

            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyCandidateResultKey"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("DuplicateCandidate"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NegativeCandidateWeight"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("InvalidResultSlotCount"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("DisabledDefaultCandidate"));
        }

        [Test]
        public void ConfigValidator_reports_bad_target_mode_shape()
        {
            var targetModes = new[]
            {
                new TargetModeDefinition { ResultKey = "", TargetKey = "", ModeKey = "" },
                new TargetModeDefinition { ResultKey = "", TargetKey = "", ModeKey = "duplicate" }
            };

            var result = LuckyDiceConfigValidator.Validate(
                new ComboRuleDefinition[0],
                ValidCandidateDefinitions(),
                targetModes,
                ComboMatcherRegistry.CreateDefaultMatchers(),
                RollActionExecutorRegistry.CreateDefaultExecutors());

            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyTargetModeResultKey"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyTargetModeTargetKey"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyTargetModeModeKey"));
            Assert.That(result.Errors.Select(error => error.Code), Does.Contain("DuplicateTargetMode"));
        }

        [Test]
        public void DefaultConfig_exposes_immutable_runtime_candidate_config()
        {
            var config = LuckyDiceDefaultConfig.Create();
            var candidate = config.LuckyDiceCandidates[0];

            Assert.That(candidate, Is.TypeOf<LuckyDiceCandidateConfig>());
            Assert.That(typeof(LuckyDiceCandidateConfig).GetProperty("Weight").CanWrite, Is.False);
            Assert.That(typeof(LuckyDiceCandidateConfig).GetProperty("Enabled").CanWrite, Is.False);
            Assert.That(candidate.SourceFilter, Is.Not.TypeOf<string[]>());
        }

        [Test]
        public void DefaultConfig_create_validated_copies_definition_values()
        {
            var candidates = ValidCandidateDefinitions();
            var targetModes = ValidTargetModeDefinitions();
            var config = LuckyDiceDefaultConfig.CreateValidated(
                new ComboRuleDefinition[0],
                candidates,
                targetModes);

            candidates[0].Weight = 999;
            candidates[0].SourceFilter[0] = "mutated_source";
            targetModes[0].ModeKey = "mutated_mode";

            Assert.That(config.LuckyDiceCandidates[0].Weight, Is.EqualTo(1));
            Assert.That(config.LuckyDiceCandidates[0].SourceFilter[0], Is.EqualTo(LuckyDiceKeys.NormalRollTriggerSource));
            Assert.That(config.TargetModeDefinitions[0].ModeKey, Is.EqualTo("slap_down_normal"));
        }

        [Test]
        public void DefaultConfig_create_validated_rejects_bad_config()
        {
            var exception = Assert.Throws<InvalidLuckyDiceConfigException>(() =>
                LuckyDiceDefaultConfig.CreateValidated(
                    new ComboRuleDefinition[0],
                    new[]
                    {
                        new LuckyDiceCandidateDefinition
                        {
                            ResultKey = "bad",
                            TargetKey = "target",
                            Weight = -1,
                            Enabled = true,
                            IsDefault = true
                        }
                    },
                    new[]
                    {
                        new TargetModeDefinition { ResultKey = "bad", TargetKey = "target", ModeKey = "mode" }
                    }));

            Assert.That(exception.ValidationResult.Errors.Select(error => error.Code), Does.Contain("NegativeCandidateWeight"));
        }

        [Test]
        public void DefaultConfig_create_validated_uses_supplied_validation_context()
        {
            var config = LuckyDiceDefaultConfig.CreateValidated(
                new[]
                {
                    new ComboRuleDefinition
                    {
                        RuleId = "custom_rule",
                        Priority = 1,
                        MatcherType = "custom_matcher",
                        Actions = new[] { new RollActionDefinition { ActionType = "custom_action" } }
                    }
                },
                ValidCandidateDefinitions(),
                ValidTargetModeDefinitions(),
                new LuckyDiceConfigValidationContext(
                    new[] { new CustomMatcher() },
                    new[] { new CustomActionExecutor() }));

            Assert.That(config.ComboRules.Single().Matcher.MatcherType, Is.EqualTo("custom_matcher"));
            Assert.That(config.ComboRules.Single().Actions.Single().ActionType, Is.EqualTo("custom_action"));
        }

        [Test]
        public void RuntimeFactory_uses_same_context_for_validation_and_runtime_dispatch()
        {
            var context = new LuckyDiceConfigValidationContext(
                new[] { new CustomMatcher() },
                new[] { new CustomActionExecutor() });
            var factory = new LuckyDiceRuntimeFactory(context, new FixedRandomSource(0, 1));

            var flow = factory.CreateFlow(
                new[]
                {
                    new ComboRuleDefinition
                    {
                        RuleId = "custom_rule",
                        Priority = 1,
                        MatcherType = "custom_matcher",
                        Actions = new[] { new RollActionDefinition { ActionType = "custom_action" } }
                    }
                },
                ValidCandidateDefinitions(),
                ValidTargetModeDefinitions());

            var result = flow.Execute(new RollFlowRequest("session-custom-flow"));

            Assert.That(result.MatchedRules.Select(rule => rule.RuleId), Is.EqualTo(new[] { "custom_rule" }));
            Assert.That(result.ActionResults.Single().ActionType, Is.EqualTo("custom_action"));
            Assert.That(result.ActionResults.Single().Outcome, Is.EqualTo("CustomAction"));
        }

        [Test]
        public void DefaultConfig_rejects_unknown_dice_set_with_clear_exception()
        {
            var config = LuckyDiceDefaultConfig.Create();

            var exception = Assert.Throws<InvalidDiceSetException>(() => config.GetDiceSet("missing_set"));

            Assert.That(exception.DiceSetId, Is.EqualTo("missing_set"));
        }

        [Test]
        public void RollFlowService_rejects_invalid_requests()
        {
            var flow = TestFlow(0, 1);

            Assert.Throws<System.ArgumentNullException>(() => flow.Execute(null));
            Assert.Throws<System.ArgumentException>(() => flow.Execute(new RollFlowRequest("session-invalid", "", LuckyDiceKeys.NormalRollTriggerSource, 0, false)));
            Assert.Throws<System.ArgumentException>(() => flow.Execute(new RollFlowRequest("session-invalid", LuckyDiceKeys.NormalDiceSetId, "", 0, false)));
            Assert.Throws<System.ArgumentException>(() => flow.Execute(new RollFlowRequest("session-invalid", LuckyDiceKeys.NormalDiceSetId, null, 0, false)));
            Assert.Throws<InvalidDiceSetException>(() => flow.Execute(new RollFlowRequest("session-invalid", "missing_set", LuckyDiceKeys.NormalRollTriggerSource, 0, false)));
        }

        [Test]
        public void Runtime_asmdef_disallows_engine_references()
        {
            var asmdef = System.IO.File.ReadAllText("FishDice/Assets/FishDice/Runtime/FishDice.Runtime.asmdef");

            Assert.That(asmdef, Does.Contain("\"noEngineReferences\": true"));
        }

        private static LuckyDiceRollFlowService TestFlow(params int[] randomValues)
        {
            var random = new FixedRandomSource(randomValues);

            return LuckyDiceRuntimeFactory.CreateDefault(random).CreateDefaultFlow();
        }

        private static LuckyDiceCandidateDefinition[] ValidCandidateDefinitions()
        {
            return new[]
            {
                new LuckyDiceCandidateDefinition
                {
                    ResultKey = LuckyDiceKeys.RocketResultKey,
                    TargetKey = "slap_down",
                    ResultSlotCount = 3,
                    Weight = 1,
                    Enabled = true,
                    SourceFilter = new[] { LuckyDiceKeys.NormalRollTriggerSource },
                    IsDefault = true
                }
            };
        }

        private static TargetModeDefinition[] ValidTargetModeDefinitions()
        {
            return new[]
            {
                new TargetModeDefinition
                {
                    ResultKey = LuckyDiceKeys.RocketResultKey,
                    TargetKey = "slap_down",
                    ModeKey = "slap_down_normal"
                }
            };
        }

        private sealed class FixedRandomSource : IRandomSource
        {
            private readonly System.Collections.Generic.Queue<int> _values;

            public FixedRandomSource(params int[] values)
            {
                _values = new System.Collections.Generic.Queue<int>(values);
            }

            public int Next(int minInclusive, int maxExclusive)
            {
                var value = _values.Dequeue();
                Assert.That(value, Is.InRange(minInclusive, maxExclusive - 1));
                return value;
            }
        }

        private sealed class CustomMatcher : IComboMatcher
        {
            public string MatcherType => "custom_matcher";

            public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec)
            {
                return true;
            }
        }

        private sealed class CustomActionExecutor : IRollActionExecutor
        {
            public string ActionType => "custom_action";

            public RollActionResult Execute(RollActionContext context, RollActionSpec action)
            {
                return RollActionResult.Succeeded(ActionType, "CustomAction");
            }
        }

        private sealed class TypedMatcher : IComboMatcher
        {
            public TypedMatcher(string matcherType)
            {
                MatcherType = matcherType;
            }

            public string MatcherType { get; }

            public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec)
            {
                return true;
            }
        }

        private sealed class TypedActionExecutor : IRollActionExecutor
        {
            public TypedActionExecutor(string actionType)
            {
                ActionType = actionType;
            }

            public string ActionType { get; }

            public RollActionResult Execute(RollActionContext context, RollActionSpec action)
            {
                return RollActionResult.Succeeded(ActionType, "TypedAction");
            }
        }
    }
}
