// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osuTK;
using osuTK.Input;

namespace osu.Game.Tests.Visual.Multiplayer
{
    public partial class TestSceneMatchStartControl : OsuManualInputManagerTestScene
    {
        private readonly Mock<MultiplayerClient> multiplayerClient = new Mock<MultiplayerClient>();
        private readonly Mock<OnlinePlayBeatmapAvailabilityTracker> availabilityTracker = new Mock<OnlinePlayBeatmapAvailabilityTracker>();

        private readonly Bindable<BeatmapAvailability> beatmapAvailability = new Bindable<BeatmapAvailability>();
        private readonly Bindable<Room> room = new Bindable<Room>();

        private MultiplayerRoom multiplayerRoom = null!;
        private MultiplayerRoomUser localUser = null!;
        private OngoingOperationTracker ongoingOperationTracker = null!;

        private PopoverContainer content = null!;
        private MatchStartControl control = null!;

        private OsuButton readyButton => control.ChildrenOfType<OsuButton>().Single();

        [BackgroundDependencyLoader]
        private void load()
        {
            Dependencies.CacheAs(multiplayerClient.Object);
            Dependencies.CacheAs(ongoingOperationTracker = new OngoingOperationTracker());
            Dependencies.CacheAs(availabilityTracker.Object);

            Children = new Drawable[]
            {
                ongoingOperationTracker,
                content = new PopoverContainer { RelativeSizeAxes = Axes.Both }
            };
        }

        [SetUpSteps]
        public void SetUpSteps()
        {
            AddStep("reset state", () =>
            {
                multiplayerClient.Reset();
                multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser);
                multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom);

                // By default, the local user is to be the host.
                multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser));

                // Assume all state changes are accepted by the server.
                multiplayerClient.Setup(m => m.ChangeState(It.IsAny<MultiplayerUserState>()))
                                 .Callback((MultiplayerUserState r) =>
                                 {
                                     Logger.Log($"Changing local user state from {localUser.State} to {r}");
                                     localUser.State = r;
                                     raiseRoomUpdated();
                                 });

                multiplayerClient.Setup(m => m.StartMatch())
                                 .Callback(() =>
                                 {
                                     multiplayerClient.Raise(m => m.LoadRequested -= null);

                                     // immediately "end" gameplay, as we don't care about that part of the process.
                                     changeUserState(localUser.UserID, MultiplayerUserState.Idle);
                                 });

                multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny<MatchUserRequest>()))
                                 .Callback((MatchUserRequest request) =>
                                 {
                                     switch (request)
                                     {
                                         case StartMatchCountdownRequest countdownStart:
                                             setRoomCountdown(countdownStart.Duration);
                                             break;

                                         case StopCountdownRequest:
                                             clearRoomCountdown();
                                             break;
                                     }
                                 });

                beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();

                availabilityTracker.Reset();
                availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability);

                PlaylistItem item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
                {
                    RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
                };

                room.Value = new Room
                {
                    Playlist = [item],
                    CurrentPlaylistItem = item
                };

                localUser = new MultiplayerRoomUser(API.LocalUser.Value.Id)
                {
                    User = API.LocalUser.Value
                };

                multiplayerRoom = new MultiplayerRoom(0)
                {
                    Playlist = { new MultiplayerPlaylistItem(item) },
                    Users = { localUser },
                    Host = localUser,
                };
            });

            AddStep("create control", () =>
            {
                content.Child = control = new MatchStartControl
                {
                    Anchor = Anchor.Centre,
                    Origin = Anchor.Centre,
                    Size = new Vector2(250, 50)
                };
            });
        }

        [Test]
        public void TestStartWithCountdown()
        {
            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);

            ClickButtonWhenEnabled<MultiplayerCountdownButton>();
            AddStep("click the first countdown button", () =>
            {
                var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
                InputManager.MoveMouseTo(popoverButton);
                InputManager.Click(MouseButton.Left);
            });

            AddStep("check request received", () =>
            {
                multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
                    req.Duration == TimeSpan.FromSeconds(10)
                )), Times.Once);
            });
        }

        [Test]
        public void TestCancelCountdown()
        {
            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);

            ClickButtonWhenEnabled<MultiplayerCountdownButton>();
            AddStep("click the first countdown button", () =>
            {
                var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
                InputManager.MoveMouseTo(popoverButton);
                InputManager.Click(MouseButton.Left);
            });

            AddStep("check request received", () =>
            {
                multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
                    req.Duration == TimeSpan.FromSeconds(10)
                )), Times.Once);
            });

            ClickButtonWhenEnabled<MultiplayerCountdownButton>();
            AddStep("click the cancel button", () =>
            {
                var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().Last();
                InputManager.MoveMouseTo(popoverButton);
                InputManager.Click(MouseButton.Left);
            });

            AddStep("check request received", () =>
            {
                multiplayerClient.Verify(m => m.SendMatchRequest(It.IsAny<StopCountdownRequest>()), Times.Once);
            });
        }

        [Test]
        public void TestReadyAndUnReadyDuringCountdown()
        {
            AddStep("add second user as host", () => addUser(new APIUser { Id = 2, Username = "Another user" }, true));

            AddStep("start countdown", () => setRoomCountdown(TimeSpan.FromMinutes(1)));

            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            checkLocalUserState(MultiplayerUserState.Ready);

            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            checkLocalUserState(MultiplayerUserState.Idle);
        }

        [Test]
        public void TestCountdownWhileSpectating()
        {
            AddStep("set spectating", () => changeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
            checkLocalUserState(MultiplayerUserState.Spectating);

            AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
            AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);

            AddStep("add second user", () => addUser(new APIUser { Id = 2, Username = "Another user" }));
            AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);

            AddStep("set second user ready", () => changeUserState(2, MultiplayerUserState.Ready));
            AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
        }

        [Test]
        public void TestBecomeHostDuringCountdownAndReady()
        {
            AddStep("add second user as host", () =>
            {
                addUser(new APIUser { Id = 2, Username = "Another user" }, true);
            });

            AddStep("start countdown", () => multiplayerClient.Object.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
            AddUntilStep("countdown started", () => multiplayerRoom.ActiveCountdowns.Any());

            AddStep("transfer host to local user", () => transferHost(localUser));
            AddUntilStep("local user is host", () => multiplayerRoom.Host?.Equals(multiplayerClient.Object.LocalUser) == true);

            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            checkLocalUserState(MultiplayerUserState.Ready);
            AddAssert("countdown still active", () => multiplayerRoom.ActiveCountdowns.Any());
        }

        [Test]
        public void TestCountdownButtonVisibilityWithAutoStart()
        {
            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            checkLocalUserState(MultiplayerUserState.Ready);
            AddUntilStep("countdown button visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);

            AddStep("enable auto start", () => changeRoomSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }));

            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            checkLocalUserState(MultiplayerUserState.Ready);
            AddUntilStep("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
        }

        [Test]
        public void TestClickingReadyButtonUnReadiesDuringAutoStart()
        {
            AddStep("enable auto start", () => changeRoomSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }));
            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            checkLocalUserState(MultiplayerUserState.Ready);

            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            checkLocalUserState(MultiplayerUserState.Idle);
        }

        [Test]
        public void TestDeletedBeatmapDisableReady()
        {
            AddUntilStep("ready button enabled", () => readyButton.Enabled.Value);

            AddStep("mark beatmap not available", () => beatmapAvailability.Value = BeatmapAvailability.NotDownloaded());
            AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);

            AddStep("mark beatmap available", () => beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable());
            AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
        }

        [Test]
        public void TestToggleStateWhenNotHost()
        {
            AddStep("add second user as host", () =>
            {
                addUser(new APIUser { Id = 2, Username = "Another user" }, true);
            });

            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            checkLocalUserState(MultiplayerUserState.Ready);

            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            checkLocalUserState(MultiplayerUserState.Idle);
        }

        [TestCase(true)]
        [TestCase(false)]
        public void TestToggleStateWhenHost(bool allReady)
        {
            if (!allReady)
                AddStep("add other user", () => addUser(new APIUser { Id = 2, Username = "Another user" }));

            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            checkLocalUserState(MultiplayerUserState.Ready);

            verifyGameplayStartFlow();
        }

        [Test]
        public void TestBecomeHostWhileReady()
        {
            AddStep("add host", () =>
            {
                addUser(new APIUser { Id = 2, Username = "Another user" }, true);
            });

            ClickButtonWhenEnabled<MultiplayerReadyButton>();

            AddStep("make local user host", () => transferHost(localUser));

            verifyGameplayStartFlow();
        }

        [Test]
        public void TestLoseHostWhileReady()
        {
            AddStep("setup", () =>
            {
                addUser(new APIUser { Id = 2, Username = "Another user" });
            });

            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            checkLocalUserState(MultiplayerUserState.Ready);

            AddStep("transfer host", () => transferHost(multiplayerRoom.Users[1]));

            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            checkLocalUserState(MultiplayerUserState.Idle);
            AddUntilStep("ready button enabled", () => readyButton.Enabled.Value);
        }

        [TestCase(true)]
        [TestCase(false)]
        public void TestManyUsersChangingState(bool isHost)
        {
            const int users = 10;

            AddStep("add many users", () =>
            {
                for (int i = 0; i < users; i++)
                    addUser(new APIUser { Id = i, Username = "Another user" }, !isHost && i == 2);
            });

            ClickButtonWhenEnabled<MultiplayerReadyButton>();

            AddRepeatStep("change user ready state", () =>
            {
                changeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
            }, 20);

            AddRepeatStep("ready all users", () =>
            {
                var nextUnready = multiplayerRoom.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
                if (nextUnready != null)
                    changeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
            }, users);
        }

        [Test]
        public void TestAbortMatch()
        {
            setUpMatchCallbacks();

            // Ready
            ClickButtonWhenEnabled<MultiplayerReadyButton>();

            // Start match
            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);

            // Abort
            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
        }

        private void setUpMatchCallbacks()
        {
            AddStep("setup client", () =>
            {
                multiplayerClient.Setup(m => m.StartMatch())
                                 .Callback(() =>
                                 {
                                     multiplayerClient.Raise(m => m.LoadRequested -= null);
                                     multiplayerClient.Object.Room!.State = MultiplayerRoomState.WaitingForLoad;
                                     raiseRoomUpdated();

                                     // The local user state doesn't really matter, so let's do the same as the base implementation for these tests.
                                     changeUserState(localUser.UserID, MultiplayerUserState.Idle);
                                 });

                multiplayerClient.Setup(m => m.AbortMatch())
                                 .Callback(() =>
                                 {
                                     multiplayerClient.Object.Room!.State = MultiplayerRoomState.Open;
                                     raiseRoomUpdated();
                                 });
            });
        }

        [Test]
        public void TestRefereeSpectating()
        {
            AddStep("set up referee", () =>
            {
                multiplayerClient.SetupGet(m => m.IsReferee).Returns(true);
                multiplayerClient.SetupGet(m => m.IsHost).Returns(false);
                multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee;
                raiseRoomUpdated();
            });

            const int users = 10;

            AddStep("add many users", () =>
            {
                for (int i = 0; i < users; i++)
                    addUser(new APIUser { Id = i, Username = "Another user" });
            });
            AddAssert("button disabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.False);

            AddStep("move to spectate", () => changeUserState(multiplayerClient.Object.LocalUser!.UserID, MultiplayerUserState.Spectating));

            AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready));
            AddAssert("button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.True);

            setUpMatchCallbacks();

            // start match
            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);

            // abort
            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
        }

        [Test]
        public void TestRefereeFlowWithoutCountdown()
        {
            AddStep("set up referee", () =>
            {
                multiplayerClient.SetupGet(m => m.IsReferee).Returns(true);
                multiplayerClient.SetupGet(m => m.IsHost).Returns(false);
                multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee;
                raiseRoomUpdated();
            });

            const int users = 10;

            AddStep("add many users", () =>
            {
                for (int i = 0; i < users; i++)
                    addUser(new APIUser { Id = i, Username = "Another user" });
            });
            AddAssert("button disabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.False);

            AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready));
            AddAssert("button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.True);

            setUpMatchCallbacks();

            // start match
            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);

            // abort
            ClickButtonWhenEnabled<MultiplayerReadyButton>();
            AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
        }

        [Test]
        public void TestRefereeFlowWithCountdown()
        {
            AddStep("set up referee", () =>
            {
                multiplayerClient.SetupGet(m => m.IsReferee).Returns(true);
                multiplayerClient.SetupGet(m => m.IsHost).Returns(false);
                multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee;
                raiseRoomUpdated();
            });

            const int users = 10;

            AddStep("add many users", () =>
            {
                for (int i = 0; i < users; i++)
                    addUser(new APIUser { Id = i, Username = "Another user" });
            });
            AddAssert("button disabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.False);

            AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready));
            AddAssert("button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.True);

            setUpMatchCallbacks();

            AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
            ClickButtonWhenEnabled<MultiplayerCountdownButton>();
            AddStep("click the first countdown button", () =>
            {
                var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
                InputManager.MoveMouseTo(popoverButton);
                InputManager.Click(MouseButton.Left);
            });

            AddStep("check request received", () =>
            {
                multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
                    req.Duration == TimeSpan.FromSeconds(10)
                )), Times.Once);
            });

            ClickButtonWhenEnabled<MultiplayerCountdownButton>();
            AddStep("click the cancel button", () =>
            {
                var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().Last();
                InputManager.MoveMouseTo(popoverButton);
                InputManager.Click(MouseButton.Left);
            });

            AddStep("check request received", () =>
            {
                multiplayerClient.Verify(m => m.SendMatchRequest(It.IsAny<StopCountdownRequest>()), Times.Once);
            });
        }

        private void verifyGameplayStartFlow()
        {
            checkLocalUserState(MultiplayerUserState.Ready);
            ClickButtonWhenEnabled<MultiplayerReadyButton>();

            AddStep("check start request received", () => multiplayerClient.Verify(m => m.StartMatch(), Times.Once));
        }

        private void checkLocalUserState(MultiplayerUserState state) =>
            AddUntilStep($"local user is {state}", () => localUser.State == state);

        private void setRoomCountdown(TimeSpan duration)
        {
            multiplayerRoom.ActiveCountdowns.Add(new MatchStartCountdown { TimeRemaining = duration });
            raiseRoomUpdated();
        }

        private void clearRoomCountdown()
        {
            multiplayerRoom.ActiveCountdowns.Clear();
            raiseRoomUpdated();
        }

        private void changeUserState(int userId, MultiplayerUserState newState)
        {
            multiplayerRoom.Users.Single(u => u.UserID == userId).State = newState;
            raiseRoomUpdated();
        }

        private void addUser(APIUser user, bool asHost = false)
        {
            var multiplayerRoomUser = new MultiplayerRoomUser(user.Id) { User = user };

            multiplayerRoom.Users.Add(multiplayerRoomUser);

            if (asHost)
                transferHost(multiplayerRoomUser);

            raiseRoomUpdated();
        }

        private void transferHost(MultiplayerRoomUser user)
        {
            multiplayerRoom.Host = user;
            raiseRoomUpdated();
        }

        private void changeRoomSettings(MultiplayerRoomSettings settings)
        {
            multiplayerRoom.Settings = settings;

            // Changing settings should reset all user ready statuses.
            foreach (var user in multiplayerRoom.Users)
            {
                if (user.State == MultiplayerUserState.Ready)
                    user.State = MultiplayerUserState.Idle;
            }

            raiseRoomUpdated();
        }

        private void raiseRoomUpdated() => multiplayerClient.Raise(m => m.RoomUpdated -= null);
    }
}
