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

using AwesomeAssertions;

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;

using Moq;

using TestFramework.ForTestingMSTest;

namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.Discovery;

public class TestMethodValidatorTests : TestContainer
{
    private readonly TestMethodValidator _testMethodValidator;
    private readonly Mock<ReflectHelper> _mockReflectHelper;
    private readonly List<string> _warnings;

    private readonly Mock<MethodInfo> _mockMethodInfo;
    private readonly Type _type;

    public TestMethodValidatorTests()
    {
        _mockReflectHelper = new Mock<ReflectHelper>(MockBehavior.Strict);
        _testMethodValidator = new TestMethodValidator(_mockReflectHelper.Object, discoverInternals: false);
        _warnings = [];

        _mockMethodInfo = new Mock<MethodInfo>(MockBehavior.Strict);
        _type = typeof(TestMethodValidatorTests);
    }

    public void IsValidTestMethodShouldReturnFalseForMethodsWithoutATestMethodAttributeOrItsDerivedAttributes()
    {
        var declaringType = new Mock<Type>(MockBehavior.Strict);
        _mockMethodInfo.Setup(x => x.DeclaringType).Returns(declaringType.Object);

        _mockReflectHelper.Setup(
            rh => rh.IsAttributeDefined<TestMethodAttribute>(It.IsAny<MemberInfo>())).Returns(false);
        _testMethodValidator.IsValidTestMethod(_mockMethodInfo.Object, _type, _warnings).Should().BeFalse();
    }

    public void IsValidTestMethodShouldReturnTrueForGenericTestMethodDefinitions()
    {
        SetupTestMethod();

        _mockReflectHelper
            .Setup(x => x.GetFirstAttributeOrDefault<AsyncStateMachineAttribute>(_mockMethodInfo.Object))
            .Returns(default(AsyncStateMachineAttribute?));

        _mockMethodInfo.Setup(mi => mi.IsGenericMethodDefinition).Returns(true);
        _mockMethodInfo.Setup(mi => mi.DeclaringType!.FullName).Returns("DummyTestClass");
        _mockMethodInfo.Setup(mi => mi.Name).Returns("DummyTestMethod");
        _mockMethodInfo.Setup(mi => mi.Attributes).Returns(MethodAttributes.Public);
        _mockMethodInfo.Setup(mi => mi.ReturnType).Returns(typeof(void));
        _mockMethodInfo.Setup(mi => mi.GetParameters()).Returns([]);

        _testMethodValidator.IsValidTestMethod(_mockMethodInfo.Object, _type, _warnings).Should().BeTrue();
        _warnings.Count.Should().Be(0);
    }

    public void IsValidTestMethodShouldReturnFalseForNonPublicMethods()
    {
        SetupTestMethod();
        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "InternalTestMethod",
            BindingFlags.Instance | BindingFlags.NonPublic)!;

        _testMethodValidator.IsValidTestMethod(methodInfo, typeof(DummyTestClass), _warnings).Should().BeFalse();
    }

    public void IsValidTestMethodShouldReturnFalseForAbstractMethods()
    {
        SetupTestMethod();
        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "AbstractTestMethod",
            BindingFlags.Instance | BindingFlags.Public)!;

        _testMethodValidator.IsValidTestMethod(methodInfo, typeof(DummyTestClass), _warnings).Should().BeFalse();
    }

    public void IsValidTestMethodShouldReturnFalseForStaticMethods()
    {
        SetupTestMethod();
        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "StaticTestMethod",
            BindingFlags.Static | BindingFlags.Public)!;

        _testMethodValidator.IsValidTestMethod(methodInfo, typeof(DummyTestClass), _warnings).Should().BeFalse();
    }

    public void IsValidTestMethodShouldReturnFalseForGenericTestMethods()
    {
        SetupTestMethod();
        Action action = () => new DummyTestClassWithGenericMethods().GenericMethod<int>();

        _testMethodValidator.IsValidTestMethod(action.Method, typeof(DummyTestClassWithGenericMethods), _warnings).Should().BeFalse();
    }

    public void IsValidTestMethodShouldReturnFalseForAsyncMethodsWithNonTaskReturnType()
    {
        SetupTestMethod();
        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "AsyncMethodWithVoidReturnType",
            BindingFlags.Instance | BindingFlags.Public)!;
        _mockReflectHelper.Setup(_mockReflectHelper => _mockReflectHelper.GetFirstAttributeOrDefault<AsyncStateMachineAttribute>(methodInfo))
            .CallBase();

        _testMethodValidator.IsValidTestMethod(methodInfo, typeof(DummyTestClass), _warnings).Should().BeFalse();
    }

    public void IsValidTestMethodShouldReturnFalseForMethodsWithNonVoidReturnType()
    {
        SetupTestMethod();
        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "MethodWithIntReturnType",
            BindingFlags.Instance | BindingFlags.Public)!;

        _testMethodValidator.IsValidTestMethod(methodInfo, typeof(DummyTestClass), _warnings).Should().BeFalse();
    }

    public void IsValidTestMethodShouldReturnTrueForAsyncMethodsWithTaskReturnType()
    {
        SetupTestMethod();
        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "AsyncMethodWithTaskReturnType",
            BindingFlags.Instance | BindingFlags.Public)!;

        _testMethodValidator.IsValidTestMethod(methodInfo, _type, _warnings).Should().BeTrue();
    }

    public void IsValidTestMethodShouldReturnTrueForNonAsyncMethodsWithTaskReturnType()
    {
        SetupTestMethod();
        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "MethodWithTaskReturnType",
            BindingFlags.Instance | BindingFlags.Public)!;

        _testMethodValidator.IsValidTestMethod(methodInfo, _type, _warnings).Should().BeTrue();
    }

    public void IsValidTestMethodShouldReturnTrueForMethodsWithValueTaskReturnType()
    {
        SetupTestMethod();
        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "MethodWithValueTaskReturnType",
            BindingFlags.Instance | BindingFlags.Public)!;

        _testMethodValidator.IsValidTestMethod(methodInfo, _type, _warnings).Should().BeTrue();
        _warnings.Count.Should().Be(0);
    }

    public void IsValidTestMethodShouldReturnFalseAndReportTargetedWarningForGenericValueTaskReturnType()
    {
        SetupTestMethod();
        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "MethodWithGenericValueTaskReturnType",
            BindingFlags.Instance | BindingFlags.Public)!;

        _testMethodValidator.IsValidTestMethod(methodInfo, typeof(DummyTestClass), _warnings).Should().BeFalse();
        _warnings.Should().ContainSingle();
        _warnings[0].Should().Contain("ValueTask<T>");
    }

    public void IsValidTestMethodShouldReportGenericSignatureWarningWhenGenericValueTaskMethodIsAlsoInvalidForAnotherReason()
    {
        SetupTestMethod();
        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "StaticMethodWithGenericValueTaskReturnType",
            BindingFlags.Static | BindingFlags.Public)!;

        // The method is invalid because it is static; the targeted ValueTask<T> message must NOT be used here,
        // otherwise the user could "fix" the return type and still have an invalid (static) method.
        _testMethodValidator.IsValidTestMethod(methodInfo, typeof(DummyTestClass), _warnings).Should().BeFalse();
        _warnings.Should().ContainSingle();
        _warnings[0].Should().NotContain("ValueTask<T>");
    }

    public void IsValidTestMethodShouldReturnTrueForMethodsWithVoidReturnType()
    {
        SetupTestMethod();

        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "MethodWithVoidReturnType",
            BindingFlags.Instance | BindingFlags.Public)!;

        _mockReflectHelper
            .Setup(x => x.GetFirstAttributeOrDefault<AsyncStateMachineAttribute>(methodInfo))
            .Returns(default(AsyncStateMachineAttribute?));

        _testMethodValidator.IsValidTestMethod(methodInfo, _type, _warnings).Should().BeTrue();
    }

    #region Discovery of internals enabled

    public void WhenDiscoveryOfInternalsIsEnabledIsValidTestMethodShouldReturnTrueForInternalMethods()
    {
        SetupTestMethod();

        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "InternalTestMethod",
            BindingFlags.Instance | BindingFlags.NonPublic)!;

        _mockReflectHelper
            .Setup(x => x.GetFirstAttributeOrDefault<AsyncStateMachineAttribute>(methodInfo))
            .Returns(default(AsyncStateMachineAttribute?));

        var testMethodValidator = new TestMethodValidator(_mockReflectHelper.Object, true);

        testMethodValidator.IsValidTestMethod(methodInfo, typeof(DummyTestClass), _warnings).Should().BeTrue();
    }

    public void WhenDiscoveryOfInternalsIsEnabledIsValidTestMethodShouldReturnFalseForPrivateMethods()
    {
        SetupTestMethod();
        MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(
            "PrivateTestMethod",
            BindingFlags.Instance | BindingFlags.NonPublic)!;

        var testMethodValidator = new TestMethodValidator(_mockReflectHelper.Object, true);

        testMethodValidator.IsValidTestMethod(methodInfo, typeof(DummyTestClass), _warnings).Should().BeFalse();
    }

    #endregion

    private void SetupTestMethod()
        => _mockReflectHelper.Setup(
            rh => rh.IsAttributeDefined<TestMethodAttribute>(It.IsAny<MemberInfo>())).Returns(true);
}

#region Dummy types

public class DummyTestClassWithGenericMethods
{
    public void GenericMethod<T>()
    {
    }
}

internal abstract class DummyTestClass
{
    public static void StaticTestMethod()
    {
    }

    public abstract void AbstractTestMethod();

    [SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Done on purpose")]
    public async void AsyncMethodWithVoidReturnType() => await Task.FromResult(true);

    public async Task AsyncMethodWithTaskReturnType() => await Task.Delay(TimeSpan.Zero);

    public Task MethodWithTaskReturnType() => Task.Delay(TimeSpan.Zero);

    public ValueTask MethodWithValueTaskReturnType() => default;

    public ValueTask<int> MethodWithGenericValueTaskReturnType() => default;

    public static ValueTask<int> StaticMethodWithGenericValueTaskReturnType() => default;

    public int MethodWithIntReturnType() => 0;

    public void MethodWithVoidReturnType()
    {
    }

    internal void InternalTestMethod()
    {
    }

#pragma warning disable IDE0051 // Remove unused private members
    private void PrivateTestMethod()
#pragma warning restore IDE0051 // Remove unused private members
    {
    }
}

#endregion
