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

using System.ComponentModel;

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// A collection of helper classes to test various conditions within
/// unit tests. If the condition being tested is not met, an exception
/// is thrown.
/// </summary>

#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
#pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads

public sealed partial class Assert
{
    // Keep the compiler-facing handler overloads in sync with the related handlers in Assert.IsNull.cs.

    /// <summary>
    /// Provides an interpolated string handler used by <c>Assert.IsTrue</c> overloads
    /// that only allocates and formats the message when the assertion is failing.
    /// </summary>
    /// <remarks>
    /// This type is intended to be used by the compiler; users should not reference it directly.
    /// </remarks>
    [StackTraceHidden]
    [InterpolatedStringHandler]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [GenerateAssertInterpolatedStringAppendMethods]
    public readonly partial struct AssertIsTrueInterpolatedStringHandler
    {
        private readonly StringBuilder? _builder;
        private readonly bool? _condition;

        /// <summary>
        /// Initializes a new instance of the <see cref="AssertIsTrueInterpolatedStringHandler"/> struct.
        /// </summary>
        /// <param name="literalLength">The number of constant characters in the interpolated string.</param>
        /// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
        /// <param name="condition">The condition value being asserted; the message is only computed when the assertion fails.</param>
        /// <param name="shouldAppend">When this method returns, indicates whether the interpolated string should be evaluated.</param>
        public AssertIsTrueInterpolatedStringHandler(int literalLength, int formattedCount, bool? condition, out bool shouldAppend)
        {
            _condition = condition;
            shouldAppend = IsTrueFailing(condition);
            if (shouldAppend)
            {
                _builder = new StringBuilder(literalLength + formattedCount);
            }
        }

        internal void ComputeAssertion(string conditionExpression)
        {
            if (_builder is not null)
            {
                ReportAssertIsTrueFailed(_condition, _builder.ToString(), conditionExpression);
            }
        }
    }

    /// <summary>
    /// Provides an interpolated string handler used by <c>Assert.IsFalse</c> overloads
    /// that only allocates and formats the message when the assertion is failing.
    /// </summary>
    /// <remarks>
    /// This type is intended to be used by the compiler; users should not reference it directly.
    /// </remarks>
    [StackTraceHidden]
    [InterpolatedStringHandler]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [GenerateAssertInterpolatedStringAppendMethods]
    public readonly partial struct AssertIsFalseInterpolatedStringHandler
    {
        private readonly StringBuilder? _builder;
        private readonly bool? _condition;

        /// <summary>
        /// Initializes a new instance of the <see cref="AssertIsFalseInterpolatedStringHandler"/> struct.
        /// </summary>
        /// <param name="literalLength">The number of constant characters in the interpolated string.</param>
        /// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
        /// <param name="condition">The condition value being asserted; the message is only computed when the assertion fails.</param>
        /// <param name="shouldAppend">When this method returns, indicates whether the interpolated string should be evaluated.</param>
        public AssertIsFalseInterpolatedStringHandler(int literalLength, int formattedCount, bool? condition, out bool shouldAppend)
        {
            _condition = condition;
            shouldAppend = IsFalseFailing(condition);
            if (shouldAppend)
            {
                _builder = new StringBuilder(literalLength + formattedCount);
            }
        }

        internal void ComputeAssertion(string conditionExpression)
        {
            if (_builder is not null)
            {
                ReportAssertIsFalseFailed(_condition, _builder.ToString(), conditionExpression);
            }
        }
    }

    /// <inheritdoc cref="IsTrue(bool?, string, string)"/>
#pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578
    public static void IsTrue([DoesNotReturnIf(false)] bool? condition, [InterpolatedStringHandlerArgument(nameof(condition))] ref AssertIsTrueInterpolatedStringHandler message, [CallerArgumentExpression(nameof(condition))] string conditionExpression = "")
#pragma warning restore IDE0060 // Remove unused parameter
    {
        TelemetryCollector.TrackAssertionCall("Assert.IsTrue");
        message.ComputeAssertion(conditionExpression);
    }

    /// <summary>
    /// Tests whether the specified condition is true and throws an exception
    /// if the condition is false.
    /// </summary>
    /// <param name="condition">
    /// The condition the test expects to be true.
    /// </param>
    /// <param name="message">
    /// The message to include in the exception when <paramref name="condition"/>
    /// is false. The message is shown in test results.
    /// </param>
    /// <param name="conditionExpression">
    /// The syntactic expression of condition as given by the compiler via caller argument expression.
    /// Users shouldn't pass a value for this parameter.
    /// </param>
    /// <exception cref="AssertFailedException">
    /// Thrown if <paramref name="condition"/> is false.
    /// </exception>
    public static void IsTrue([DoesNotReturnIf(false)] bool? condition, string? message = "", [CallerArgumentExpression(nameof(condition))] string conditionExpression = "")
    {
        TelemetryCollector.TrackAssertionCall("Assert.IsTrue");

        if (IsTrueFailing(condition))
        {
            ReportAssertIsTrueFailed(condition, message, conditionExpression);
        }
    }

    private static bool IsTrueFailing(bool? condition)
        => condition is false or null;

    [DoesNotReturn]
    private static void ReportAssertIsTrueFailed(bool? condition, string? message, string conditionExpression)
    {
        string actualValue = AssertionValueRenderer.RenderValue(condition);
        EvidenceBlock evidence = EvidenceBlock.Create()
            .AddLine("actual:", actualValue);

        StructuredAssertionMessage structured = new(FrameworkMessages.IsTrueFailedSummary);
        structured.WithUserMessage(message);
        structured.WithEvidence(evidence);
        structured.WithExpectedAndActual(AssertionValueRenderer.RenderValue(true), actualValue);
        structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsTrue", conditionExpression, nameof(condition)));

        ReportAssertFailed(structured);
    }

    /// <inheritdoc cref="IsFalse(bool?, string, string)" />
#pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578
    public static void IsFalse([DoesNotReturnIf(true)] bool? condition, [InterpolatedStringHandlerArgument(nameof(condition))] ref AssertIsFalseInterpolatedStringHandler message, [CallerArgumentExpression(nameof(condition))] string conditionExpression = "")
#pragma warning restore IDE0060 // Remove unused parameter
    {
        TelemetryCollector.TrackAssertionCall("Assert.IsFalse");
        message.ComputeAssertion(conditionExpression);
    }

    /// <summary>
    /// Tests whether the specified condition is false and throws an exception
    /// if the condition is true.
    /// </summary>
    /// <param name="condition">
    /// The condition the test expects to be false.
    /// </param>
    /// <param name="message">
    /// The message to include in the exception when <paramref name="condition"/>
    /// is true. The message is shown in test results.
    /// </param>
    /// <param name="conditionExpression">
    /// The syntactic expression of condition as given by the compiler via caller argument expression.
    /// Users shouldn't pass a value for this parameter.
    /// </param>
    /// <exception cref="AssertFailedException">
    /// Thrown if <paramref name="condition"/> is true.
    /// </exception>
    public static void IsFalse([DoesNotReturnIf(true)] bool? condition, string? message = "", [CallerArgumentExpression(nameof(condition))] string conditionExpression = "")
    {
        TelemetryCollector.TrackAssertionCall("Assert.IsFalse");

        if (IsFalseFailing(condition))
        {
            ReportAssertIsFalseFailed(condition, message, conditionExpression);
        }
    }

    private static bool IsFalseFailing(bool? condition)
        => condition is true or null;

    [DoesNotReturn]
    private static void ReportAssertIsFalseFailed(bool? condition, string? message, string conditionExpression)
    {
        string actualValue = AssertionValueRenderer.RenderValue(condition);
        EvidenceBlock evidence = EvidenceBlock.Create()
            .AddLine("actual:", actualValue);

        StructuredAssertionMessage structured = new(FrameworkMessages.IsFalseFailedSummary);
        structured.WithUserMessage(message);
        structured.WithEvidence(evidence);
        structured.WithExpectedAndActual(AssertionValueRenderer.RenderValue(false), actualValue);
        structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsFalse", conditionExpression, nameof(condition)));

        ReportAssertFailed(structured);
    }
}
