using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using eagle;
using eagle.Notifications;
using Grpc.Core;
using Net.Eagle0.Eagle.Api;
using Net.Eagle0.Eagle.Views;
using NUnit.Framework;
using ActionResultType = Net.Eagle0.Eagle.Common.ActionResultType;

namespace eagle0.Tests {
    public class GameModelUpdaterTests {
        [Test]
        public void DisposeUnsubscribesAndReleasesCallbacks() {
            const long gameId = 7123;
            var connection =
                    new PersistentClientConnection(null, new Metadata(), CancellationToken.None);
            var updater = new GameModelUpdater(gameId, null, connection) {
                UpdateAction =
                        _ => {},
                ActionResultAction =
                        _ => {},
                NoteRecipient = (Notification _) => {},
                HasPendingCommandWithToken =
                        _ => true
            };
            var subscribers = (IDictionary) typeof(PersistentClientConnection)
                                      .GetField(
                                              "_subscribers",
                                              BindingFlags.Instance | BindingFlags.NonPublic)
                                      .GetValue(connection);
            subscribers.Add(gameId, updater);

            updater.Dispose();

            Assert.IsFalse(subscribers.Contains(gameId));
            Assert.IsNull(updater.UpdateAction);
            Assert.IsNull(updater.ActionResultAction);
            Assert.IsNull(updater.NoteRecipient);
            Assert.IsNull(updater.HasPendingCommandWithToken);
            Assert.DoesNotThrow(updater.Dispose);
        }

        [Test]
        public void EndAftermathDeliversDeferredProvinceConqueredNotification() {
            const long gameId = 7128;
            const int observingFactionId = 3;
            const int conqueringFactionId = 1;
            const int defendingFactionId = 5;
            const int conqueringHeroId = 10;
            const int defendingHeroId = 50;
            const int provinceId = 35;
            var notifications = new List<Notification>();
            var updater = new GameModelUpdater(gameId, observingFactionId, null) {
                NoteRecipient = notifications.Add,
                UpdateAction =
                        _ => {}
            };
            var startingState = new GameStateView { GameId = gameId };
            startingState.Factions.Add(
                    conqueringFactionId,
                    new FactionView {
                        Id = conqueringFactionId,
                        FactionHeadId = conqueringHeroId,
                        Name = "The Eagle"
                    });
            startingState.Factions.Add(
                    defendingFactionId,
                    new FactionView {
                        Id = defendingFactionId,
                        FactionHeadId = defendingHeroId,
                        Name = "Tumala's Defenders"
                    });
            startingState.Heroes.Add(conqueringHeroId, new HeroView { Id = conqueringHeroId });
            startingState.Heroes.Add(defendingHeroId, new HeroView { Id = defendingHeroId });
            startingState.Provinces.Add(
                    provinceId,
                    new ProvinceView {
                        Id = provinceId,
                        Name = "Tumala",
                        RulingFactionId = conqueringFactionId
                    });
            updater.ReceiveGameUpdate(new GameUpdate {
                GameId = gameId,
                StartingState = startingState,
                ActionResultResponse = new ActionResultResponse()
            });

            var endAftermath = new ActionResultView { Type = ActionResultType.EndAftermathPhase };
            endAftermath.Notifications.Add(new Net.Eagle0.Eagle.Common.Notification {
                Details =
                        new Net.Eagle0.Eagle.Common.NotificationDetails {
                            ProvinceConqueredDetails =
                                    new Net.Eagle0.Eagle.Common.ProvinceConqueredDetails {
                                        ProvinceId = provinceId,
                                        ConqueringFactionId = conqueringFactionId,
                                        DefendingFactionId = defendingFactionId
                                    }
                        }
            });
            var response = new ActionResultResponse { UnfilteredResultCountAfter = 1 };
            response.ActionResultViews.Add(endAftermath);

            updater.ReceiveGameUpdate(
                    new GameUpdate { GameId = gameId, ActionResultResponse = response });

            Assert.AreEqual(1, notifications.Count);
            Assert.AreEqual("Province Conquered", notifications[0].Title);
            Assert.AreEqual(
                    "The Eagle has conquered Tumala from Tumala's Defenders.",
                    notifications[0].Text);
            CollectionAssert.AreEqual(new[] { provinceId }, notifications[0].ProvinceIds);
        }

        [Test]
        public void ResultBearingUpdateDefensivelyReplacesCommandsWhenTokenIsUnchanged() {
            const long gameId = 7124;
            const long token = 17;
            const int provinceId = 34;
            var models = new List<IGameModel>();
            var updater = new GameModelUpdater(gameId, null, null) { UpdateAction = models.Add };

            updater.ReceiveGameUpdate(new GameUpdate {
                GameId = gameId,
                StartingState = new GameStateView { GameId = gameId },
                ActionResultResponse =
                        new ActionResultResponse {
                            AvailableCommands = AvailableCommandsWith(
                                    token,
                                    provinceId,
                                    new AvailableCommand {
                                        ReturnCommand =
                                                new ReturnAvailableCommand {
                                                    ActingProvinceId = provinceId
                                                }
                                    })
                        }
            });

            var questFulfilled = new ActionResultResponse {
                UnfilteredResultCountAfter = 1,
                AvailableCommands = AvailableCommandsWith(
                        token,
                        provinceId,
                        new AvailableCommand {
                            RecruitHeroesCommand =
                                    new RecruitHeroesAvailableCommand {
                                        ActingProvinceId = provinceId
                                    }
                        })
            };
            questFulfilled.ActionResultViews.Add(
                    new ActionResultView { Type = ActionResultType.QuestFulfilled });
            updater.ReceiveGameUpdate(
                    new GameUpdate { GameId = gameId, ActionResultResponse = questFulfilled });

            Assert.AreEqual(2, models.Count);
            Assert.AreEqual(token, models[1].CommandToken);
            Assert.Greater(models[1].StrategicUpdateRevision, models[0].StrategicUpdateRevision);
            var command = models[1].AvailableCommandsForProvince(provinceId).Single();
            Assert.AreEqual(
                    AvailableCommand.SealedValueOneofCase.RecruitHeroesCommand,
                    command.SealedValueCase);
        }

        [Test]
        public void RemovedBattleRetainsModelForQueuedFinalShardokResult() {
            const long gameId = 7125;
            const string shardokGameId = "finished-battle";
            const int fleeingUnitId = 41;
            IGameModel currentModel = null;
            var updater = new GameModelUpdater(gameId, null, null) {
                UpdateAction = model => currentModel = model
            };
            updater.ReceiveGameUpdate(new GameUpdate {
                GameId = gameId,
                StartingState = new GameStateView { GameId = gameId },
                ActionResultResponse = new ActionResultResponse()
            });

            var shardokModel = new ShardokGameModel(
                    gameId,
                    shardokGameId,
                    0,
                    null,
                    null,
                    "Test Province",
                    1,
                    new List<ShardokGameModel.PlayerWithHostility>(),
                    new Dictionary<int, string>(),
                    new Dictionary<int, string>(),
                    new Dictionary<int, string>());
            shardokModel.UnitsById[fleeingUnitId] = new UnitViewWithName(
                    new Net.Eagle0.Shardok.Api.UnitView { UnitId = fleeingUnitId },
                    null,
                    null,
                    null);
            currentModel.ShardokGameModels[shardokGameId] = shardokModel;

            var battleRemovedDiff = new GameStateViewDiff();
            battleRemovedDiff.RemovedBattleIds.Add(shardokGameId);
            var battleRemovedResponse = new ActionResultResponse { UnfilteredResultCountAfter = 1 };
            battleRemovedResponse.ActionResultViews.Add(new ActionResultView {
                Type = ActionResultType.QuestFulfilled,
                GameStateDiff = battleRemovedDiff
            });
            updater.ReceiveGameUpdate(new GameUpdate {
                GameId = gameId,
                ActionResultResponse = battleRemovedResponse
            });

            Assert.IsTrue(shardokModel.CanExitBattle);
            Assert.AreSame(shardokModel, currentModel.ShardokGameModels[shardokGameId]);

            var finalDiff = new Net.Eagle0.Shardok.Api.GameStateViewDiff();
            finalDiff.RemovedUnits.Add(fleeingUnitId);
            var finalShardokResponse =
                    new ShardokActionResultResponse.Types.SingleShardokGameResultResponse {
                        ShardokGameId = shardokGameId,
                        NewResultViewCount = 1,
                        AvailableCommands = new Net.Eagle0.Shardok.Api.AvailableCommands()
                    };
            finalShardokResponse.ActionResultViews.Add(new Net.Eagle0.Shardok.Api.ActionResultView {
                Type = Net.Eagle0.Shardok.Common.ActionType.Fled,
                Actor = fleeingUnitId,
                GameStateViewDiff = finalDiff
            });
            updater.ReceiveGameUpdate(new GameUpdate {
                GameId = gameId,
                ShardokActionResultResponse =
                        new ShardokActionResultResponse {
                            ShardokGameResponses = { finalShardokResponse }
                        }
            });

            Assert.AreEqual(1, shardokModel.History.Count);
            Assert.IsFalse(shardokModel.UnitsById.ContainsKey(fleeingUnitId));
        }

        private static AvailableCommands
        AvailableCommandsWith(long token, int provinceId, AvailableCommand command) {
            var result = new AvailableCommands { Token = token, SuggestedProvinceId = provinceId };
            result.CommandsByProvince.Add(
                    provinceId,
                    new OneProvinceAvailableCommands {
                        ProvinceId = provinceId,
                        Commands = { command }
                    });
            return result;
        }
    }
}
