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

#if NETFRAMEWORK
using AwesomeAssertions;

using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities;

using Moq;

using TestFramework.ForTestingMSTest;

namespace MSTestAdapter.PlatformServices.UnitTests.Deployment;

public class AssemblyLoadWorkerTests : TestContainer
{
    public void GetFullPathToDependentAssembliesShouldReturnV1FrameworkAssembly()
    {
        // Arrange.
        var v1AssemblyName = new AssemblyName("Microsoft.VisualStudio.QualityTools.UnitTestFramework");
        var testableAssembly = new TestableAssembly
        {
            GetReferencedAssembliesSetter = () => [v1AssemblyName],
        };

        var mockAssemblyUtility = new Mock<IAssemblyUtility>();
        mockAssemblyUtility.Setup(au => au.ReflectionOnlyLoadFrom(It.IsAny<string>())).Returns(testableAssembly);
        mockAssemblyUtility.Setup(au => au.ReflectionOnlyLoad(It.IsAny<string>()))
            .Returns(new TestableAssembly(v1AssemblyName.Name));

        var worker = new AssemblyLoadWorker(mockAssemblyUtility.Object);

        // Act.
        IReadOnlyCollection<string> dependentAssemblies = worker.GetFullPathToDependentAssemblies("C:\\temp\\test3424.dll", out IList<string> warnings);

        // Assert.
        string utfAssembly = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll");
        dependentAssemblies.Should().Contain(utfAssembly);
    }

    public void GetFullPathToDependentAssembliesShouldIncludeAssemblyPathAndExceptionDetailsInWarningWhenLoadFromThrows()
    {
        // Arrange.
        var loadException = new BadImageFormatException("The module was expected to contain an assembly manifest.");
        var mockAssemblyUtility = new Mock<IAssemblyUtility>();
        mockAssemblyUtility.Setup(au => au.ReflectionOnlyLoadFrom(It.IsAny<string>())).Throws(loadException);

        var worker = new AssemblyLoadWorker(mockAssemblyUtility.Object);

        // Act.
        IReadOnlyCollection<string> dependentAssemblies = worker.GetFullPathToDependentAssemblies("C:\\temp\\test3424.dll", out IList<string> warnings);

        // Assert.
        // The warning must retain the assembly path together with the exception type/message so the
        // underlying reason the dependency graph could not be walked is discoverable, not silently lost.
        dependentAssemblies.Should().BeEmpty();
        warnings.Should().ContainSingle();
        warnings[0].Should().Contain("Failed to load");
        warnings[0].Should().NotContain("was not found");
        warnings[0].Should().Contain("C:\\temp\\test3424.dll");
        warnings[0].Should().Contain(nameof(BadImageFormatException));
        warnings[0].Should().Contain(loadException.Message);
    }

    public void GetFullPathToDependentAssembliesShouldReturnV1FrameworkReferencedInADependency()
    {
        // Arrange.
        var v1AssemblyName = new AssemblyName("Microsoft.VisualStudio.QualityTools.UnitTestFramework");

        var dependentAssemblyName = new AssemblyName("Common.TestFramework");
        var dependentAssembly = new TestableAssembly(dependentAssemblyName.Name)
        {
            GetReferencedAssembliesSetter = () => [v1AssemblyName],
        };

        var testableAssembly = new TestableAssembly
        {
            GetReferencedAssembliesSetter = () => [dependentAssemblyName],
        };

        var mockAssemblyUtility = new Mock<IAssemblyUtility>();
        mockAssemblyUtility.Setup(au => au.ReflectionOnlyLoadFrom(It.IsAny<string>()))
            .Returns(
                (string assemblyPath) =>
                    {
                        if (assemblyPath.Contains(dependentAssembly.Name))
                        {
                            return dependentAssembly;
                        }
                        else if (assemblyPath.Contains("test3424"))
                        {
                            return testableAssembly;
                        }

                        return null!;
                    });

        mockAssemblyUtility.Setup(au => au.ReflectionOnlyLoad(It.IsAny<string>()))
            .Returns(
            (string assemblyPath) =>
            {
                if (assemblyPath.Contains(dependentAssembly.FullName))
                {
                    return dependentAssembly;
                }
                else if (assemblyPath.Contains(v1AssemblyName.FullName))
                {
                    return new TestableAssembly(v1AssemblyName.FullName);
                }

                return null!;
            });

        var worker = new AssemblyLoadWorker(mockAssemblyUtility.Object);

        // Act.
        IReadOnlyCollection<string> dependentAssemblies = worker.GetFullPathToDependentAssemblies("C:\\temp\\test3424.dll", out IList<string> warnings);

        // Assert.
        string utfAssembly = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll");
        dependentAssemblies.Should().Contain(utfAssembly);
    }

    public void GetFullPathToDependentAssembliesShouldIncludeExceptionDetailsInWarningWhenDependentAssemblyLoadThrows()
    {
        // Arrange.
        var dependentAssemblyName = new AssemblyName("Missing.Dependency");
        var testableAssembly = new TestableAssembly
        {
            GetReferencedAssembliesSetter = () => [dependentAssemblyName],
        };

        var loadException = new FileNotFoundException("Could not load file or assembly 'Missing.Dependency'.");
        var mockAssemblyUtility = new Mock<IAssemblyUtility>();
        mockAssemblyUtility.Setup(au => au.ReflectionOnlyLoadFrom(It.IsAny<string>())).Returns(testableAssembly);
        mockAssemblyUtility.Setup(au => au.ReflectionOnlyLoad(It.IsAny<string>())).Throws(loadException);

        var worker = new AssemblyLoadWorker(mockAssemblyUtility.Object);

        // Act.
        IReadOnlyCollection<string> dependentAssemblies = worker.GetFullPathToDependentAssemblies("C:\\temp\\test3424.dll", out IList<string> warnings);

        // Assert.
        // The warning must retain the dependent assembly identity together with the exception type/message
        // so the reason the dependency could not be resolved/loaded is discoverable, not silently lost.
        dependentAssemblies.Should().BeEmpty();
        warnings.Should().ContainSingle();
        warnings[0].Should().Contain(dependentAssemblyName.FullName);
        warnings[0].Should().Contain(nameof(FileNotFoundException));
        warnings[0].Should().Contain(loadException.Message);
    }

    #region Testable Implementations

    private class TestableAssembly : Assembly
    {
        public TestableAssembly()
        {
        }

        public TestableAssembly(string assemblyName)
        {
            // YAGNI *.exe.
            if (!assemblyName.EndsWith(".dll", StringComparison.Ordinal))
            {
                Name = string.Concat(assemblyName, ".dll");
            }

            FullNameSetter = () => assemblyName;
        }

        public Func<AssemblyName[]> GetReferencedAssembliesSetter { get; set; } = null!;

        public Func<string> FullNameSetter { get; set; } = null!;

        public override AssemblyName[] GetReferencedAssemblies()
            => GetReferencedAssembliesSetter != null ? GetReferencedAssembliesSetter.Invoke() : [];

        public string? Name { get; set; }

        public override string FullName => FullNameSetter != null ? FullNameSetter.Invoke() : GetExecutingAssembly().FullName;

        public override bool GlobalAssemblyCache => false;

        public override string Location => Path.Combine(Path.GetDirectoryName(GetExecutingAssembly().Location), Name);

        public override Module[] GetModules(bool getResourceModules) => [];
    }

    #endregion
}
#endif
