// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// An abstract attribute that controls retrying a test method if it failed. It's up to the derived classes to
/// define how the retry is done.
/// </summary>
/// <remarks>
/// When applied to a test class, the attribute is used as a default for every test method declared on the class.
/// A retry attribute placed directly on a test method takes precedence over a class-level one.
/// The attribute is not inherited: applying it to a base test class does not apply it to derived test classes.
/// <para>
/// The runner only invokes <see cref="ExecuteAsync"/> after the initial (non-retry) run produced an
/// unacceptable result, i.e. at least one of the produced <see cref="TestResult"/> entries had a
/// <see cref="UnitTestOutcome.Failed"/> or <see cref="UnitTestOutcome.Timeout"/> outcome. It does not itself
/// re-run the test on subsequent attempts: once <see cref="ExecuteAsync"/> is entered, the derived
/// implementation owns the retry loop and decides when to stop. Derived types may use
/// <c>IsAcceptableResultForRetry</c> (as <see cref="RetryAttribute"/> does) or implement any other policy.
/// </para>
/// </remarks>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public abstract class RetryBaseAttribute : Attribute
{
    /// <summary>
    /// Retries the test method. The details of how retry is done is left to the derived classes.
    /// Note that a first run of the method was already executed and failed before this method is called.
    /// </summary>
    /// <param name="retryContext">An object to encapsulate the state needed for retry execution.</param>
    /// <returns>
    /// Returns a <see cref="RetryResult"/> object that contains the results of all attempts. The last added
    /// element determines the test outcome; under Microsoft.Testing.Platform the earlier attempts are also
    /// reported, tagged as superseded, so tooling can surface the retry (attempt count, flaky detection,
    /// per-attempt error messages). The VSTest host receives only the final result.
    /// </returns>
    [Experimental("MSTESTEXP", UrlFormat = "https://aka.ms/mstest/diagnostics#{0}")]
    protected internal abstract Task<RetryResult> ExecuteAsync(RetryContext retryContext);

    internal static bool IsAcceptableResultForRetry(TestResult[] results)
    {
        foreach (TestResult result in results)
        {
            UnitTestOutcome outcome = result.Outcome;

            // The attempt is retried when any single result is Failed or Timeout. Any other outcome (including
            // Inconclusive) is acceptable on its own, so an attempt is treated as acceptable (retry stops) only
            // when none of its results is Failed or Timeout.
            if (outcome is UnitTestOutcome.Failed or UnitTestOutcome.Timeout)
            {
                return false;
            }
        }

        return true;
    }
}
