// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace Microsoft.AspNetCore.Testing;

internal sealed class AspNetTestRunner : XunitTestRunner
{
    private readonly TestOutputHelper _testOutputHelper;
    private readonly bool _ownsTestOutputHelper;

    public AspNetTestRunner(
        ITest test,
        IMessageBus messageBus,
        Type testClass,
        object[] constructorArguments,
        MethodInfo testMethod,
        object[] testMethodArguments,
        string skipReason,
        IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
        ExceptionAggregator aggregator,
        CancellationTokenSource cancellationTokenSource)
        : base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource)
    {
        // Prioritize using ITestOutputHelper from constructor.
        if (ConstructorArguments != null)
        {
            foreach (var obj in ConstructorArguments)
            {
                _testOutputHelper = obj as TestOutputHelper;
                if (_testOutputHelper != null)
                {
                    break;
                }
            }
        }

        // No ITestOutputHelper in constructor so we'll create it ourselves.
        if (_testOutputHelper == null)
        {
            _testOutputHelper = new TestOutputHelper();
            _ownsTestOutputHelper = true;
        }
    }

    protected override async Task<Tuple<decimal, string>> InvokeTestAsync(ExceptionAggregator aggregator)
    {
        if (_ownsTestOutputHelper)
        {
            _testOutputHelper.Initialize(MessageBus, Test);
        }

        var retryAttribute = GetRetryAttribute(TestMethod);
        var result = retryAttribute is null
            ? await base.InvokeTestAsync(aggregator).ConfigureAwait(false)
            : await RunTestCaseWithRetryAsync(retryAttribute, aggregator).ConfigureAwait(false);

        if (_ownsTestOutputHelper)
        {
            // Update result with output if we created our own ITestOutputHelper.
            // The string returned from this method is what VS displays as the test output.
            result = new Tuple<decimal, string>(result.Item1, _testOutputHelper.Output);
            _testOutputHelper.Uninitialize();
        }

        return result;
    }

    private async Task<Tuple<decimal, string>> RunTestCaseWithRetryAsync(RetryAttribute retryAttribute, ExceptionAggregator aggregator)
    {
        var totalTimeTaken = 0m;
        List<string> messages = new();
        var numAttempts = Math.Max(1, retryAttribute.MaxRetries);

        for (var attempt = 1; attempt <= numAttempts; attempt++)
        {
            var result = await base.InvokeTestAsync(aggregator).ConfigureAwait(false);
            totalTimeTaken += result.Item1;
            messages.Add(result.Item2);

            if (!aggregator.HasExceptions)
            {
                break;
            }
            else if (attempt < numAttempts)
            {
                // We can't use the ITestOutputHelper here because there's no active test
                messages.Add($"[{TestCase.DisplayName}] Attempt {attempt} of {retryAttribute.MaxRetries} failed due to {aggregator.ToException()}");

                await Task.Delay(5000).ConfigureAwait(false);
                aggregator.Clear();
            }
        }

        return new(totalTimeTaken, string.Join(Environment.NewLine, messages));
    }

    protected override async Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator)
    {
        var repeatAttribute = GetRepeatAttribute(TestMethod);
        if (repeatAttribute == null)
        {
            return await InvokeTestMethodCoreAsync(aggregator).ConfigureAwait(false);
        }

        var repeatContext = new RepeatContext(repeatAttribute.RunCount);
        RepeatContext.Current = repeatContext;

        var timeTaken = 0.0M;
        for (repeatContext.CurrentIteration = 0; repeatContext.CurrentIteration < repeatContext.Limit; repeatContext.CurrentIteration++)
        {
            timeTaken = await InvokeTestMethodCoreAsync(aggregator).ConfigureAwait(false);
            if (aggregator.HasExceptions)
            {
                return timeTaken;
            }
        }

        return timeTaken;
    }

    private Task<decimal> InvokeTestMethodCoreAsync(ExceptionAggregator aggregator)
    {
        var invoker = new AspNetTestInvoker(Test, MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource, _testOutputHelper);
        return invoker.RunAsync();
    }

    private static RepeatAttribute GetRepeatAttribute(MethodInfo methodInfo)
    {
        var attributeCandidate = methodInfo.GetCustomAttribute<RepeatAttribute>();
        if (attributeCandidate != null)
        {
            return attributeCandidate;
        }

        attributeCandidate = methodInfo.DeclaringType.GetCustomAttribute<RepeatAttribute>();
        if (attributeCandidate != null)
        {
            return attributeCandidate;
        }

        return methodInfo.DeclaringType.Assembly.GetCustomAttribute<RepeatAttribute>();
    }

    private static RetryAttribute GetRetryAttribute(MethodInfo methodInfo)
    {
        var attributeCandidate = methodInfo.GetCustomAttribute<RetryAttribute>();
        if (attributeCandidate != null)
        {
            return attributeCandidate;
        }

        attributeCandidate = methodInfo.DeclaringType.GetCustomAttribute<RetryAttribute>();
        if (attributeCandidate != null)
        {
            return attributeCandidate;
        }

        return methodInfo.DeclaringType.Assembly.GetCustomAttribute<RetryAttribute>();
    }
}
