// 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.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play;

namespace osu.Game.Tests.Visual
{
    /// <summary>
    /// The goal of this abstract test class is to ensure that the process of exporting and re-importing of a replay does not affect its playback.
    /// Use <see cref="RunTest"/> to exercise that property.
    /// </summary>
    [HeadlessTest]
    [TestFixture]
    public abstract partial class ReplayStabilityTestScene : RateAdjustedBeatmapTestScene
    {
        private ReplayPlayer currentPlayer = null!;
        private readonly List<JudgementResult> results = new List<JudgementResult>();

        /// <summary>
        /// Runs <paramref name="replay"/> against the supplied <paramref name="beatmap"/>
        /// and checks that the judgement results recorded match <paramref name="expectedResults"/>.
        /// Then, encodes the <paramref name="replay"/>, decodes the result of encoding, runs the result of decoding against the supplied <paramref name="beatmap"/>,
        /// and checks that the judgement results recorded still match <paramref name="expectedResults"/>.
        /// </summary>
        protected void RunTest(IBeatmap beatmap, Replay replay, IEnumerable<HitResult> expectedResults)
        {
            Score originalScore = null!;
            Score decodedScore = null!;

            AddStep(@"create replay", () => originalScore = new Score
            {
                Replay = replay,
                ScoreInfo = new ScoreInfo()
            });

            AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(beatmap));
            AddStep(@"set ruleset", () => Ruleset.Value = beatmap.BeatmapInfo.Ruleset);
            AddStep(@"push player", () => pushNewPlayer(originalScore));

            AddUntilStep(@"wait until player is loaded", () => currentPlayer.IsCurrentScreen());
            skipIntroIfPresent();
            AddUntilStep(@"wait for completion", () => currentPlayer.GameplayState.HasCompleted);
            AddAssert(@"judgement results before encode are correct", () => results.Select(r => r.Type), () => Is.EquivalentTo(expectedResults));

            AddStep(@"exit player", () => currentPlayer.Exit());

            // The incoming beatmap is ruleset-typed in every usage, so the incoming hitobjects will be used as-is rather than being converted.
            // Because we'll be re-using the beatmap (thus also the hitobjects), we need to make sure the previous player has been fully disposed.
            AddUntilStep("player exited", () => !currentPlayer.IsCurrentScreen());
            AddStep("dispose player", () => currentPlayer.Dispose());

            AddStep(@"encode and decode score", () =>
            {
                var encoder = new LegacyScoreEncoder(originalScore, beatmap);

                using (var stream = new MemoryStream())
                {
                    encoder.Encode(stream, leaveOpen: true);
                    stream.Position = 0;
                    decodedScore = new TestScoreDecoder(Beatmap.Value).Parse(stream);
                }
            });

            AddStep(@"push player", () => pushNewPlayer(decodedScore));

            AddUntilStep(@"Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
            skipIntroIfPresent();
            AddUntilStep(@"Wait for completion", () => currentPlayer.GameplayState.HasCompleted);
            AddAssert(@"judgement results after encode are correct", () => results.Select(r => r.Type), () => Is.EquivalentTo(expectedResults));
        }

        private void pushNewPlayer(Score score)
        {
            var player = new ReplayPlayer(score);
            player.OnLoadComplete += _ =>
            {
                player.GameplayState.ScoreProcessor.NewJudgement += result =>
                {
                    if (currentPlayer == player)
                        results.Add(result);
                };
            };
            LoadScreen(currentPlayer = player);
            results.Clear();
        }

        private void skipIntroIfPresent() =>
            AddStep(@"skip intro if present", () =>
            {
                if (currentPlayer.ChildrenOfType<GameplayClockContainer>().Single().CurrentTime < 0)
                    currentPlayer.Seek(0);
            });

        private class TestScoreDecoder : LegacyScoreDecoder
        {
            private readonly WorkingBeatmap beatmap;

            public TestScoreDecoder(WorkingBeatmap beatmap)
            {
                this.beatmap = beatmap;
            }

            protected override Ruleset GetRuleset(int rulesetId) => beatmap.BeatmapInfo.Ruleset.CreateInstance();
            protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmap;
        }
    }
}
