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

namespace FishDice.Tests.LuckyDice
{
    public sealed class LuckyDiceSlice1Tests
    {
        [Test]
        public void DefaultConfig_keeps_normal_and_lucky_sets_separate()
        {
            var config = LuckyDiceDefaultConfig.Create();

            var normal = config.GetDiceSet(LuckyDiceKeys.NormalDiceSetId);
            var lucky = config.GetDiceSet(LuckyDiceKeys.LuckyDiceSetId);

            Assert.That(normal.SetType, Is.EqualTo(DiceSetType.Normal));
            Assert.That(normal.DiceCount, Is.EqualTo(2));
            Assert.That(normal.Faces.Select(face => face.Key), Is.EqualTo(new[] { "2", "3", "4", "5", "6", "clover" }));
            Assert.That(lucky.SetType, Is.EqualTo(DiceSetType.Lucky));
            Assert.That(lucky.DefaultResultSlotCount, Is.EqualTo(3));
            Assert.That(lucky.Faces.Select(face => face.Key), Is.EqualTo(new[] { "rocket", "thief" }));
        }

        [Test]
        public void Roll_uses_configured_dice_count_and_keeps_session_id()
        {
            var config = new DiceSetConfig(
                "normal_three",
                DiceSetType.Normal,
                diceCount: 3,
                faces: new[]
                {
                    DiceFace.NormalNumber(2),
                    DiceFace.NormalNumber(3),
                    DiceFace.NormalClover()
                },
                defaultResultSlotCount: null);
            var service = new DiceRollService(new FixedRandomSource(0, 1, 2));

            var result = service.Roll(config, "session-roll", "normal_roll");

            Assert.That(result.RollSessionId, Is.EqualTo("session-roll"));
            Assert.That(result.DiceCount, Is.EqualTo(3));
            Assert.That(result.Faces.Select(face => face.Key), Is.EqualTo(new[] { "2", "3", "clover" }));
            Assert.That(result.Multiplier, Is.EqualTo(1));
        }

        [Test]
        public void ComboFacts_normalize_faces_and_support_more_than_two_dice()
        {
            var result = DiceRollResult.Create(
                "session-facts",
                "normal_main",
                DiceSetType.Normal,
                new[] { DiceFace.NormalClover(), DiceFace.NormalNumber(2), DiceFace.NormalNumber(2) },
                multiplier: 1,
                triggerSource: "normal_roll");

            var facts = ComboFactBuilder.Build(result);

            Assert.That(facts.DiceCount, Is.EqualTo(3));
            Assert.That(facts.CloverCount, Is.EqualTo(1));
            Assert.That(facts.NumberSum, Is.EqualTo(4));
            Assert.That(facts.NumberValues, Is.EqualTo(new[] { 2, 2 }));
            Assert.That(facts.NormalizedKey, Is.EqualTo("2_2_clover"));
            Assert.That(facts.OrderedKey, Is.EqualTo("clover_2_2"));
            Assert.That(facts.FaceCounts["2"], Is.EqualTo(2));
        }

        [Test]
        public void ComboFacts_reject_lucky_dice_results()
        {
            var result = DiceRollResult.Create(
                "session-lucky",
                "lucky_dice",
                DiceSetType.Lucky,
                new[] { DiceFace.LuckyResult("rocket") },
                multiplier: 1,
                triggerSource: "normal_roll");

            Assert.Throws<InvalidOperationException>(() => ComboFactBuilder.Build(result));
        }

        [TestCase("clover_clover", "normal_clover_clover_lucky_dice", "trigger_lucky_dice")]
        [TestCase("2_clover", "normal_number_clover_bonus", "grant_reward,add_multiplier")]
        [TestCase("2_6", "normal_all_numbers_reward", "grant_reward")]
        public void RuleMatcher_uses_priority_and_matcher_type(string normalizedKey, string expectedRuleId, string expectedActionsCsv)
        {
            var facts = TestFacts.FromNormalizedKey(normalizedKey);
            var rules = LuckyDiceDefaultConfig.Create().ComboRules;

            var result = new ComboRuleMatcher().Match(facts, rules);

            Assert.That(result.Matches.Select(match => match.RuleId), Is.EqualTo(new[] { expectedRuleId }));
            Assert.That(
                result.Matches.Single().Actions.Select(action => action.ActionType),
                Is.EqualTo(expectedActionsCsv.Split(',')));
        }

        [Test]
        public void ComboRuleDefinitionLoader_converts_flat_rule_rows_to_runtime_rules()
        {
            var definitions = new[]
            {
                new ComboRuleDefinition
                {
                    RuleId = "test_clover_bonus",
                    Priority = 25,
                    MatcherType = LuckyDiceKeys.ContainsFaceMatcher,
                    FaceKey = LuckyDiceKeys.CloverFaceKey,
                    RequiredCount = 1,
                    Actions = new[]
                    {
                        new RollActionDefinition { ActionType = LuckyDiceKeys.GrantRewardAction },
                        new RollActionDefinition
                        {
                            ActionType = LuckyDiceKeys.AddMultiplierAction,
                            Params = new[]
                            {
                                new ActionParamDefinition { Key = "reason", Value = "test_bonus" }
                            }
                        }
                    }
                }
            };

            var rules = ComboRuleDefinitionLoader.Load(definitions);

            Assert.That(rules.Count, Is.EqualTo(1));
            Assert.That(rules[0].RuleId, Is.EqualTo("test_clover_bonus"));
            Assert.That(rules[0].Matcher.MatcherType, Is.EqualTo(LuckyDiceKeys.ContainsFaceMatcher));
            Assert.That(rules[0].Matcher.FaceKey, Is.EqualTo(LuckyDiceKeys.CloverFaceKey));
            Assert.That(rules[0].Matcher.RequiredCount, Is.EqualTo(1));
            Assert.That(rules[0].Actions.Select(action => action.ActionType), Is.EqualTo(new[] { "grant_reward", "add_multiplier" }));
            Assert.That(rules[0].Actions[1].Params["reason"], Is.EqualTo("test_bonus"));
        }

        [Test]
        public void ActionDispatcher_dispatches_by_action_type_and_records_unregistered_actions()
        {
            var trace = new RollTrace("session-actions");
            var reward = new RecordingActionExecutor("grant_reward");
            var lucky = new RecordingActionExecutor("trigger_lucky_dice");
            var dispatcher = new RollActionDispatcher(new IRollActionExecutor[] { reward, lucky });
            var context = new RollActionContext("session-actions", "2_3", trace);

            var results = dispatcher.Execute(
                context,
                new[]
                {
                    new RollActionSpec("grant_reward"),
                    new RollActionSpec("missing_action"),
                    new RollActionSpec("trigger_lucky_dice")
                });

            Assert.That(reward.Calls, Is.EqualTo(1));
            Assert.That(lucky.Calls, Is.EqualTo(1));
            Assert.That(results.Select(result => result.ActionType), Is.EqualTo(new[] { "grant_reward", "missing_action", "trigger_lucky_dice" }));
            Assert.That(results.Single(result => result.ActionType == "missing_action").Success, Is.False);
            Assert.That(trace.ExecutedActions, Is.EqualTo(new[] { "grant_reward", "missing_action", "trigger_lucky_dice" }));
        }

        [TestCase("2_3", "normal_all_numbers_reward", "grant_reward", "Reward")]
        [TestCase("2_clover", "normal_number_clover_bonus", "grant_reward,add_multiplier", "Multiplier")]
        [TestCase("clover_clover", "normal_clover_clover_lucky_dice", "trigger_lucky_dice", "LuckyDiceRequested")]
        public void Slice1_chain_matches_rules_dispatches_actions_and_keeps_trace(
            string normalizedKey,
            string expectedRuleId,
            string expectedActionsCsv,
            string expectedOutcome)
        {
            var config = LuckyDiceDefaultConfig.Create();
            var trace = new RollTrace("session-chain");
            var facts = TestFacts.FromNormalizedKey(normalizedKey, "session-chain");
            trace.RecordComboFacts(facts);

            var match = new ComboRuleMatcher().Match(facts, config.ComboRules);
            trace.RecordRuleMatch(match);

            var dispatcher = new RollActionDispatcher(
                new IRollActionExecutor[]
                {
                    new GrantRewardExecutor(),
                    new AddMultiplierExecutor(),
                    new TriggerLuckyDiceExecutor()
                });
            var context = new RollActionContext(trace.RollSessionId, facts.NormalizedKey, trace);
            var actionResults = dispatcher.Execute(context, match.Matches.Single().Actions);

            Assert.That(trace.RollSessionId, Is.EqualTo("session-chain"));
            Assert.That(trace.ComboNormalizedKey, Is.EqualTo(normalizedKey));
            Assert.That(trace.MatchedRuleId, Is.EqualTo(expectedRuleId));
            Assert.That(actionResults.Select(result => result.ActionType), Is.EqualTo(expectedActionsCsv.Split(',')));
            Assert.That(trace.FinalOutcome, Is.EqualTo(expectedOutcome));
        }

        private sealed class FixedRandomSource : IRandomSource
        {
            private readonly Queue<int> _values;

            public FixedRandomSource(params int[] values)
            {
                _values = new 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 RecordingActionExecutor : IRollActionExecutor
        {
            public RecordingActionExecutor(string actionType)
            {
                ActionType = actionType;
            }

            public string ActionType { get; }

            public int Calls { get; private set; }

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

        private static class TestFacts
        {
            public static ComboFacts FromNormalizedKey(string normalizedKey, string rollSessionId = "session-rules")
            {
                var faces = normalizedKey.Split('_')
                    .Select(key => key == LuckyDiceKeys.CloverFaceKey
                        ? DiceFace.NormalClover()
                        : DiceFace.NormalNumber(int.Parse(key)))
                    .ToArray();

                return ComboFactBuilder.Build(DiceRollResult.Create(
                    rollSessionId,
                    LuckyDiceKeys.NormalDiceSetId,
                    DiceSetType.Normal,
                    faces,
                    multiplier: 1,
                    triggerSource: LuckyDiceKeys.NormalRollTriggerSource));
            }
        }
    }
}
