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

namespace FishDice.Tests.LuckyDice
{
    public sealed class PoolFunnelRandomTests
    {
        [Test]
        public void Entry_resolution_skips_closed_commercial_pack_layer()
        {
            var store = new InMemoryRandomPoolStateStore();
            var service = TestService(
                new FixedRandomSource(0),
                store,
                new KeyedLayerAvailabilityPolicy("commercial_open", false));
            var pool = new RandomPoolDefinition(
                "main_pool",
                new[] { "commercial_pack", "normal_reward" },
                new[]
                {
                    Layer("commercial_pack", 100, "commercial_open", Result("pack_entry", 0, 1, 0, 1, PoolFunnelDecision.Stop())),
                    Layer("normal_reward", 10, null, Result("coins", 0, 1, 0, 1, PoolFunnelDecision.Stop()))
                });

            var result = service.Execute(pool, new PoolFunnelRequest("session-entry", "normal_roll"));

            Assert.That(result.Trace.SelectedEntryLayerId, Is.EqualTo("normal_reward"));
            Assert.That(result.Trace.UnavailableEntryLayerIds, Is.EqualTo(new[] { "commercial_pack" }));
            Assert.That(result.LastLayer.SelectedResult.ResultKey, Is.EqualTo("coins"));
            Assert.That(store.Load("main_pool", "commercial_pack").LuckValue, Is.EqualTo(0));
        }

        [Test]
        public void Commercial_pack_entry_can_enter_child_layer_and_child_uses_result_priority()
        {
            var service = TestService(new FixedRandomSource(0, 0), new InMemoryRandomPoolStateStore());
            var pool = new RandomPoolDefinition(
                "main_pool",
                new[] { "commercial_pack" },
                new[]
                {
                    Layer(
                        "commercial_pack",
                        100,
                        null,
                        Result("enter_pack_picker", 0, 1, 0, 0, PoolFunnelDecision.NextLayer("pack_picker"))),
                    Layer(
                        "pack_picker",
                        0,
                        null,
                        Result("standard_pack", 1, 999, 0, -3, PoolFunnelDecision.Stop()),
                        Result("premium_pack", 5, 1, 0, -7, PoolFunnelDecision.Stop()))
                });

            var result = service.Execute(pool, new PoolFunnelRequest("session-pack", "normal_roll"));

            Assert.That(result.Layers.Select(layer => layer.LayerId), Is.EqualTo(new[] { "commercial_pack", "pack_picker" }));
            Assert.That(result.LastLayer.SelectedResult.ResultKey, Is.EqualTo("premium_pack"));
            Assert.That(result.LastLayer.Weights.Select(weight => weight.ResultKey), Is.EqualTo(new[] { "standard_pack", "premium_pack" }));
        }

        [Test]
        public void Dynamic_weight_uses_luckValue_before_settlement()
        {
            var store = new InMemoryRandomPoolStateStore();
            store.Save(new RandomPoolState("main_pool", "jackpot_layer", luckValue: 5, rollCount: 5));
            var service = TestService(new FixedRandomSource(10), store);
            var pool = new RandomPoolDefinition(
                "main_pool",
                new[] { "jackpot_layer" },
                new[]
                {
                    Layer(
                        "jackpot_layer",
                        0,
                        null,
                        Result("small_reward", 0, 10, 0, 1, PoolFunnelDecision.Stop()),
                        Result("jackpot", 0, 1, 2, -5, PoolFunnelDecision.Stop()))
                });

            var result = service.Execute(pool, new PoolFunnelRequest("session-weight", "normal_roll"));

            Assert.That(result.LastLayer.Weights.Single(weight => weight.ResultKey == "jackpot").Weight, Is.EqualTo(11));
            Assert.That(result.LastLayer.RandomValue, Is.EqualTo(10));
            Assert.That(result.LastLayer.SelectedResult.ResultKey, Is.EqualTo("jackpot"));
            Assert.That(result.LastLayer.StateAfter.LuckValue, Is.EqualTo(0));
            Assert.That(result.LastLayer.StateAfter.RollCount, Is.EqualTo(6));
        }

        [Test]
        public void Different_pack_results_can_consume_different_luckValue_amounts()
        {
            var store = new InMemoryRandomPoolStateStore();
            store.Save(new RandomPoolState("main_pool", "pack_picker", luckValue: 20, rollCount: 2));
            var service = TestService(new FixedRandomSource(1), store);
            var pool = new RandomPoolDefinition(
                "main_pool",
                new[] { "pack_picker" },
                new[]
                {
                    Layer(
                        "pack_picker",
                        0,
                        null,
                        Result("standard_pack", 0, 1, 0, -3, PoolFunnelDecision.Stop()),
                        Result("premium_pack", 0, 1, 0, -7, PoolFunnelDecision.Stop()))
                });

            var result = service.Execute(pool, new PoolFunnelRequest("session-consume", "normal_roll"));

            Assert.That(result.LastLayer.SelectedResult.ResultKey, Is.EqualTo("premium_pack"));
            Assert.That(store.Load("main_pool", "pack_picker").LuckValue, Is.EqualTo(13));
            Assert.That(store.Load("main_pool", "pack_picker").RollCount, Is.EqualTo(3));
        }

        [Test]
        public void Selected_result_actions_are_dispatched_into_layer_result()
        {
            var service = TestService(new FixedRandomSource(0), new InMemoryRandomPoolStateStore());
            var pool = new RandomPoolDefinition(
                "main_pool",
                new[] { "action_layer" },
                new[]
                {
                    Layer(
                        "action_layer",
                        0,
                        null,
                        new RandomResultDefinition(
                            "show_pack",
                            priority: 0,
                            baseWeight: 1,
                            luckValueWeightFactor: 0,
                            luckValueDelta: 0,
                            decision: PoolFunnelDecision.Stop(),
                            actions: new[] { new RollActionSpec("show_commercial_pack") }))
                });

            var result = service.Execute(pool, new PoolFunnelRequest("session-actions", "normal_roll"));

            Assert.That(result.LastLayer.ActionResults.Select(action => action.ActionType), Is.EqualTo(new[] { "show_commercial_pack" }));
            Assert.That(result.LastLayer.ActionResults.Single().Outcome, Is.EqualTo("PoolFunnelActionQueued"));
        }

        [Test]
        public void Local_state_is_isolated_by_poolId_and_layerId()
        {
            var store = new InMemoryRandomPoolStateStore();
            store.Save(new RandomPoolState("pool_a", "shared_layer", luckValue: 4, rollCount: 1));
            store.Save(new RandomPoolState("pool_b", "shared_layer", luckValue: 99, rollCount: 9));
            var service = TestService(new FixedRandomSource(0), store);
            var pool = new RandomPoolDefinition(
                "pool_a",
                new[] { "shared_layer" },
                new[]
                {
                    Layer("shared_layer", 0, null, Result("reward", 0, 1, 0, 2, PoolFunnelDecision.Stop()))
                });

            service.Execute(pool, new PoolFunnelRequest("session-state", "normal_roll"));

            Assert.That(store.Load("pool_a", "shared_layer").LuckValue, Is.EqualTo(6));
            Assert.That(store.Load("pool_a", "shared_layer").RollCount, Is.EqualTo(2));
            Assert.That(store.Load("pool_b", "shared_layer").LuckValue, Is.EqualTo(99));
            Assert.That(store.Load("pool_b", "shared_layer").RollCount, Is.EqualTo(9));
        }

        private static PoolFunnelRandomService TestService(
            IRandomSource random,
            IRandomPoolStateStore store,
            ILayerAvailabilityPolicy availability = null)
        {
            return new PoolFunnelRandomService(
                random,
                store,
                availability ?? new AlwaysAvailableLayerPolicy(),
                new LinearLuckValueWeightCalculator(),
                new NoOpPoolFunnelActionDispatcher(),
                new DeltaPoolSettlementStrategy(),
                new ConfigFunnelDecisionStrategy());
        }

        private static RandomLayerDefinition Layer(
            string layerId,
            int priority,
            string availabilityKey,
            params RandomResultDefinition[] results)
        {
            return new RandomLayerDefinition(layerId, priority, availabilityKey, results);
        }

        private static RandomResultDefinition Result(
            string resultKey,
            int priority,
            int baseWeight,
            int luckValueWeightFactor,
            int luckValueDelta,
            PoolFunnelDecision decision)
        {
            return new RandomResultDefinition(
                resultKey,
                priority,
                baseWeight,
                luckValueWeightFactor,
                luckValueDelta,
                decision);
        }

        private sealed class KeyedLayerAvailabilityPolicy : ILayerAvailabilityPolicy
        {
            private readonly Dictionary<string, bool> _availability;

            public KeyedLayerAvailabilityPolicy(string key, bool isAvailable)
            {
                _availability = new Dictionary<string, bool> { { key, isAvailable } };
            }

            public bool IsLayerAvailable(RandomLayerDefinition layer, PoolFunnelContext context)
            {
                if (string.IsNullOrEmpty(layer.AvailabilityKey))
                {
                    return true;
                }

                bool isAvailable;
                return _availability.TryGetValue(layer.AvailabilityKey, out isAvailable) && isAvailable;
            }
        }

        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;
            }
        }
    }
}
