using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using NUnit.Framework;
using NUnit.Framework.Internal;
using Robust.Client;
using Robust.Shared;
using Robust.Shared.Exceptions;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Utility;

namespace Robust.UnitTesting.Pool;

// This partial file contains logic related to recycling & disposing test pairs.
public partial class TestPair<TServer, TClient>
{
    private void ReportErrorLogs()
    {
        using (Assert.EnterMultipleScope())
        {
            foreach (var log in ServerLogHandler.FailingLogs)
                Assert.Fail(log);

            foreach (var log in ClientLogHandler.FailingLogs)
                Assert.Fail(log);
        }
    }

    private async Task OnDirtyDispose()
    {
        var usageTime = Watch.Elapsed;
        Watch.Restart();
        await TestOut.WriteLineAsync($"{nameof(DisposeAsync)}: Test gave back pair {Id} in {usageTime.TotalMilliseconds} ms");
        Kill();
        var disposeTime = Watch.Elapsed;
        await TestOut.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Id} in {disposeTime.TotalMilliseconds} ms");
        ReportErrorLogs();
        // Test pairs should only dirty dispose if they are failing. If they are not failing, this probably happened
        // because someone forgot to clean-return the pair.
        Assert.Warn("Test was dirty-disposed.");
    }

    /// <summary>
    /// This method gets called before any test pair gets returned to the pool.
    /// </summary>
    protected virtual async Task Cleanup()
    {
        if (TestMap != null)
        {
            await Server.WaitPost(() => Server.EntMan.DeleteEntity(TestMap.MapUid));
            TestMap = null;
        }
    }

    private async Task OnCleanDispose()
    {
        await Server.WaitIdleAsync();
        await Client.WaitIdleAsync();
        await Cleanup();
        await Server.Cleanup();
        await Client.Cleanup();
        await RevertModifiedCvars();

        var usageTime = Watch.Elapsed;
        Watch.Restart();
        await TestOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Id} for {usageTime.TotalMilliseconds} ms");
        // Let any last minute failures the test cause happen.
        await ReallyBeIdle();
        if (!Settings.Destructive)
        {
            if (Client.IsAlive == false)
                throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the client in pair {Id}:", Client.UnhandledException);

            if (Server.IsAlive == false)
                throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the server in pair {Id}:", Server.UnhandledException);
        }

        if (Settings.MustNotBeReused)
        {
            Kill();
            await ReallyBeIdle();
            await TestOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Clean disposed in {Watch.Elapsed.TotalMilliseconds} ms");
            return;
        }

        try
        {
            var sRuntimeLog = Server.Resolve<IRuntimeLog>();
            if (sRuntimeLog.ExceptionCount > 0)
                throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
            var cRuntimeLog = Client.Resolve<IRuntimeLog>();
            if (cRuntimeLog.ExceptionCount > 0)
                throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");

            ReportErrorLogs();
        }
        catch (Exception)
        {
            Kill();
            await ReallyBeIdle();
            await TestOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Dirty disposed in {Watch.Elapsed.TotalMilliseconds} ms");
            Assert.Warn("Test was dirty-disposed.");
            throw;
        }

        var returnTime = Watch.Elapsed;
        await TestOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
        State = PairState.Ready;
    }

    [HandlesResourceDisposal]
    public async ValueTask CleanReturnAsync()
    {
        if (State != PairState.InUse)
            throw new Exception($"{nameof(CleanReturnAsync)}: Unexpected state. Pair: {Id}. State: {State}.");

        await TestOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Id} started");
        State = PairState.CleanDisposed;
        try
        {
            await OnCleanDispose();
        }
        finally
        {
            DebugTools.Assert(State is PairState.Dead or PairState.Ready);
            ClearContext();
            Manager.Return(this);
        }
    }

    public async ValueTask DisposeAsync()
    {
        switch (State)
        {
            case PairState.Dead:
            case PairState.Ready:
                break;
            case PairState.InUse:
                await TestOut.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Id} started");
                try
                {
                    await OnDirtyDispose();
                }
                finally
                {
                    ClearContext();
                    Manager.Return(this);
                }
                break;
            default:
                throw new Exception($"{nameof(DisposeAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
        }
    }

    /// <summary>
    /// This method gets called when a previously used test pair is being retrieved from the pool.
    /// Note that in some instances this method may get skipped (See <see cref="PairSettings.CanFastRecycle"/>).
    /// </summary>
    public async Task RecycleInternal(PairSettings settings, TextWriter testOut)
    {
        Watch.Restart();
        await testOut.WriteLineAsync($"Recycling...");
        await RunTicksSync(1);

        // Disconnect the client if they are connected.
        if (Client.CNetMan.IsConnected)
        {
            await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Disconnecting client.");
            await Client.WaitPost(() => Client.CNetMan.ClientDisconnect("Test pooling cleanup disconnect"));
            await RunTicksSync(1);
        }

        await Recycle(settings, testOut);
        ClearModifiedCvars();

        // (possibly) reconnect the client
        if (settings.Connected)
        {
            await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Connecting client");
            await Client.Connect(Server);
        }

        Settings = default!;
        await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Idling");
        await ReallyBeIdle();
        await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Done recycling");
    }

    /// <summary>
    /// This method gets called when a previously used test pair is being retrieved from the pool.
    /// If the next settings are compatible with the previous settings, this step may get skipped (See <see cref="PairSettings.CanFastRecycle"/>).
    /// In general, this method should also call <see cref="ApplySettings"/>.
    /// </summary>
    protected virtual async Task Recycle(PairSettings next, TextWriter testOut)
    {
        //Apply Cvars
        await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Setting CVar ");
        await ApplySettings(next);
        await RunTicksSync(1);

        // flush server entities.
        await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Flushing server entities");
        await Server.WaitPost(() => Server.EntMan.FlushEntities());
        await RunTicksSync(1);
    }

    /// <summary>
    /// Apply settings to the test pair. This method is always called when a pair is fetched from the pool. There should
    /// be no need to apply settings that require a pair to be recycled, as in those cases the
    /// <see cref="PairSettings.CanFastRecycle"/> should have caused <see cref="Recycle"/> to be invoked, which should
    /// already have applied those settings.
    /// </summary>
    public async Task ApplySettings(PairSettings next)
    {
        await ApplySettings(Client, next);
        await ApplySettings(Server, next);
    }

    /// <inheritdoc cref="ApplySettings(PairSettings)"/>
    [MustCallBase]
    protected internal virtual async Task ApplySettings(IIntegrationInstance instance, PairSettings next)
    {
        if (instance.CfgMan.IsCVarRegistered(CVars.NetInterp.Name))
            await instance.WaitPost(() => instance.CfgMan.SetCVar(CVars.NetInterp, !next.DisableInterpolate));
    }

    /// <summary>
    /// Invoked after a test pair has been recycled to validate that the settings have been properly applied.
    /// </summary>
    [MustCallBase]
    public virtual void ValidateSettings(PairSettings settings)
    {
        var netMan = Client.Resolve<INetManager>();
        Assert.That(netMan.IsConnected, Is.EqualTo(settings.Connected));

        if (!settings.Connected)
            return;

        var baseClient = Client.Resolve<IBaseClient>();
        var cPlayer = Client.Resolve<ISharedPlayerManager>();
        var sPlayer = Server.Resolve<ISharedPlayerManager>();

        Assert.That(baseClient.RunLevel, Is.EqualTo(ClientRunLevel.InGame));
        Assert.That(sPlayer.Sessions.Length, Is.EqualTo(1));
        var session = sPlayer.Sessions.Single();
        Assert.That(cPlayer.LocalSession?.UserId, Is.EqualTo(session.UserId));
    }
}
