// 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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Tests.Visual;
using osu.Game.Updater;

namespace osu.Game.Tests.NonVisual
{
    [HeadlessTest]
    public partial class TestSceneUpdateManager : OsuTestScene
    {
        [Cached(typeof(INotificationOverlay))]
        private readonly INotificationOverlay notifications = new TestNotificationOverlay();

        private TestUpdateManager manager = null!;
        private OsuConfigManager config = null!;

        [SetUpSteps]
        public void SetupSteps()
        {
            AddStep("add manager", () =>
            {
                config = new OsuConfigManager(LocalStorage);
                config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer);

                Child = new DependencyProvidingContainer
                {
                    CachedDependencies = [(typeof(OsuConfigManager), config)],
                    Child = manager = new TestUpdateManager()
                };
            });

            // Updates should be checked when the object is loaded for the first time.
            AddUntilStep("check pending", () => manager.IsPending);
            AddStep("complete check", () => manager.Complete());
            AddUntilStep("1 check completed", () => manager.Completions, () => Is.EqualTo(1));
            AddUntilStep("no check pending", () => !manager.IsPending);
        }

        [TearDownSteps]
        public void TeardownSteps()
        {
            // Importantly, this immediately saves the config, which cancels any pending background save.
            AddStep("dispose config manager", () => config.Dispose());
        }

        /// <summary>
        /// Updates should be checked when the release stream is changed.
        /// </summary>
        [Test]
        public void TestReleaseStreamChanged()
        {
            AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Tachyon));

            AddUntilStep("check pending", () => manager.IsPending);
            AddStep("complete check", () => manager.Complete());
            AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2));
            AddUntilStep("no check pending", () => !manager.IsPending);

            AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer));

            AddUntilStep("check pending", () => manager.IsPending);
            AddStep("complete check", () => manager.Complete());
            AddUntilStep("3 checks completed", () => manager.Completions, () => Is.EqualTo(3));
            AddUntilStep("no check pending", () => !manager.IsPending);
        }

        /// <summary>
        /// Changing the release stream should start a new invocation and cancel the existing one.
        /// </summary>
        [Test]
        public void TestNewInvocationOnReleaseStreamChanged()
        {
            AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Tachyon));
            AddUntilStep("check pending", () => manager.IsPending);
            AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer));
            AddUntilStep("3 invocations", () => manager.Invocations, () => Is.EqualTo(3));

            AddStep("complete check", () => manager.Complete());
            AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2));
            AddUntilStep("no check pending", () => !manager.IsPending);
        }

        /// <summary>
        /// Updates should be checked when the user requests them to.
        /// </summary>
        [Test]
        public void TestUserRequest()
        {
            AddStep("request check", () => manager.CheckForUpdate());

            AddUntilStep("check pending", () => manager.IsPending);
            AddStep("complete check", () => manager.Complete());
            AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2));
            AddUntilStep("no check pending", () => !manager.IsPending);

            AddStep("request check", () => manager.CheckForUpdate());

            AddUntilStep("check pending", () => manager.IsPending);
            AddStep("complete check", () => manager.Complete());
            AddUntilStep("3 checks completed", () => manager.Completions, () => Is.EqualTo(3));
            AddUntilStep("no check pending", () => !manager.IsPending);
        }

        /// <summary>
        /// User requests should start a new invocation and cancel the existing one.
        /// </summary>
        [Test]
        public void TestUserRequestOverridesExistingCheck()
        {
            // This part covering double user input is not really possible because the settings button is disabled during the check,
            // but it's kept here for sanity in-case the update manager is used as a standalone object elsewhere.

            AddStep("request check", () => manager.CheckForUpdate());
            AddUntilStep("check pending", () => manager.IsPending);
            AddStep("request check", () => manager.CheckForUpdate());
            AddUntilStep("3 invocations", () => manager.Invocations, () => Is.EqualTo(3));

            AddStep("complete check", () => manager.Complete());
            AddUntilStep("2 checks completed", () => manager.Completions, () => Is.EqualTo(2));
            AddUntilStep("no check pending", () => !manager.IsPending);

            // This next part tests for the user requesting an update during a background check, and is possible to occur in practice.

            AddStep("change release stream", () => config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Tachyon));
            AddUntilStep("check pending", () => manager.IsPending);
            AddStep("request check", () => manager.CheckForUpdate());
            AddUntilStep("5 invocations", () => manager.Invocations, () => Is.EqualTo(5));

            AddStep("complete check", () => manager.Complete());
            AddUntilStep("3 checks completed", () => manager.Completions, () => Is.EqualTo(3));
            AddUntilStep("no check pending", () => !manager.IsPending);
        }

        [Test]
        public void TestFixedReleaseStreamWrittenToConfig()
        {
            AddStep("add manager", () =>
            {
                config = new OsuConfigManager(LocalStorage);
                config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer);

                Child = new DependencyProvidingContainer
                {
                    CachedDependencies = [(typeof(OsuConfigManager), config)],
                    Child = manager = new TestUpdateManager(ReleaseStream.Tachyon)
                };
            });

            AddAssert("release stream set to tachyon", () => config.Get<ReleaseStream>(OsuSetting.ReleaseStream), () => Is.EqualTo(ReleaseStream.Tachyon));
        }

        private partial class TestUpdateManager : UpdateManager
        {
            public override ReleaseStream? FixedReleaseStream { get; }

            public bool IsPending { get; private set; }
            public int Invocations { get; private set; }
            public int Completions { get; private set; }

            private TaskCompletionSource<bool>? pendingCheck;

            public TestUpdateManager(ReleaseStream? fixedReleaseStream = null)
            {
                FixedReleaseStream = fixedReleaseStream;
            }

            protected override async Task<bool> PerformUpdateCheck(CancellationToken cancellationToken)
            {
                Invocations++;

                var check = pendingCheck = new TaskCompletionSource<bool>();
                IsPending = true;

                try
                {
                    bool result = await check.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
                    Completions++;
                    return result;
                }
                finally
                {
                    IsPending = false;
                }
            }

            public void Complete()
            {
                pendingCheck?.SetResult(true);
            }
        }

        private partial class TestNotificationOverlay : INotificationOverlay
        {
            public void Post(Notification notification)
            {
            }

            public void Hide()
            {
            }

            public IBindable<int> UnreadCount { get; } = new Bindable<int>();

            public IEnumerable<Notification> AllNotifications { get; } = Enumerable.Empty<Notification>();
        }
    }
}
