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

using System.Collections;
using System.Runtime.InteropServices;

namespace Microsoft.AspNetCore.Server.IntegrationTesting;

public class TestMatrix : IEnumerable<object[]>
{
    public IList<ServerType> Servers { get; set; } = new List<ServerType>();
    public IList<string> Tfms { get; set; } = new List<string>();
    public IList<ApplicationType> ApplicationTypes { get; set; } = new List<ApplicationType>();
    public IList<RuntimeArchitecture> Architectures { get; set; } = new List<RuntimeArchitecture>();

    // ANCM specific...
    public IList<HostingModel> HostingModels { get; set; } = new List<HostingModel>();

    private IList<Tuple<Func<TestVariant, bool>, string>> Skips { get; } = new List<Tuple<Func<TestVariant, bool>, string>>();

    public static TestMatrix ForServers(params ServerType[] types)
    {
        return new TestMatrix()
        {
            Servers = types
        };
    }

    public TestMatrix WithTfms(params string[] tfms)
    {
        Tfms = tfms;
        return this;
    }

    public TestMatrix WithApplicationTypes(params ApplicationType[] types)
    {
        ApplicationTypes = types;
        return this;
    }

    public TestMatrix WithAllApplicationTypes()
    {
        ApplicationTypes.Add(ApplicationType.Portable);
        ApplicationTypes.Add(ApplicationType.Standalone);
        return this;
    }
    public TestMatrix WithArchitectures(params RuntimeArchitecture[] archs)
    {
        Architectures = archs;
        return this;
    }

    /// <summary>
    /// With all architectures that are compatible with the currently running architecture
    /// </summary>
    /// <returns></returns>
    public TestMatrix WithAllArchitectures()
    {
        Architectures.Add(RuntimeArchitectures.Current);
        if (RuntimeInformation.OSArchitecture == Architecture.X64)
        {
            Architectures.Add(RuntimeArchitecture.x86);
        }
        return this;
    }

    public TestMatrix WithHostingModels(params HostingModel[] models)
    {
        HostingModels = models;
        return this;
    }

    public TestMatrix WithAllHostingModels()
    {
        HostingModels.Add(HostingModel.OutOfProcess);
        HostingModels.Add(HostingModel.InProcess);
        return this;
    }

    /// <summary>
    /// V2 + InProc
    /// </summary>
    /// <returns></returns>
    public TestMatrix WithAncmV2InProcess() => WithHostingModels(HostingModel.InProcess);

    public TestMatrix Skip(string message, Func<TestVariant, bool> check)
    {
        Skips.Add(new Tuple<Func<TestVariant, bool>, string>(check, message));
        return this;
    }

    private IEnumerable<TestVariant> Build()
    {
        if (!Servers.Any())
        {
            throw new ArgumentException("No servers were specified.");
        }

        // TFMs.
        if (!Tfms.Any())
        {
            throw new ArgumentException("No TFMs were specified.");
        }

        ResolveDefaultArchitecture();

        if (!ApplicationTypes.Any())
        {
            ApplicationTypes.Add(ApplicationType.Portable);
        }

        if (!HostingModels.Any())
        {
            HostingModels.Add(HostingModel.OutOfProcess);
        }

        var variants = new List<TestVariant>();
        VaryByServer(variants);

        CheckForSkips(variants);

        return variants;
    }

    private void ResolveDefaultArchitecture()
    {
        if (!Architectures.Any())
        {
            Architectures.Add(RuntimeArchitectures.Current);
        }
    }

    private void VaryByServer(List<TestVariant> variants)
    {
        foreach (var server in Servers)
        {
            var skip = SkipIfServerIsNotSupportedOnThisOS(server);

            VaryByTfm(variants, server, skip);
        }
    }

    private static string SkipIfServerIsNotSupportedOnThisOS(ServerType server)
    {
        var skip = false;
        switch (server)
        {
            case ServerType.IIS:
            case ServerType.IISExpress:
            case ServerType.HttpSys:
                skip = !OperatingSystem.IsWindows();
                break;
            case ServerType.Kestrel:
                break;
            case ServerType.Nginx:
                // Technically it's possible but we don't test it.
                skip = OperatingSystem.IsWindows();
                break;
            default:
                throw new ArgumentException(server.ToString());
        }

        return skip ? "This server is not supported on this operating system." : null;
    }

    private void VaryByTfm(List<TestVariant> variants, ServerType server, string skip)
    {
        foreach (var tfm in Tfms)
        {
            if (!CheckTfmIsSupportedForServer(tfm, server))
            {
                // Don't generate net462 variations for nginx server.
                continue;
            }

            var skipTfm = skip ?? SkipIfTfmIsNotSupportedOnThisOS(tfm);

            VaryByApplicationType(variants, server, tfm, skipTfm);
        }
    }

    private static bool CheckTfmIsSupportedForServer(string tfm, ServerType server)
    {
        // Not a combination we test
        return !(Tfm.Matches(Tfm.Net462, tfm) && ServerType.Nginx == server);
    }

    private static string SkipIfTfmIsNotSupportedOnThisOS(string tfm)
    {
        if (Tfm.Matches(Tfm.Net462, tfm) && !OperatingSystem.IsWindows())
        {
            return "This TFM is not supported on this operating system.";
        }

        return null;
    }

    private void VaryByApplicationType(List<TestVariant> variants, ServerType server, string tfm, string skip)
    {
        foreach (var t in ApplicationTypes)
        {
            var type = t;
            if (Tfm.Matches(Tfm.Net462, tfm) && type == ApplicationType.Portable)
            {
                if (ApplicationTypes.Count == 1)
                {
                    // Override the default
                    type = ApplicationType.Standalone;
                }
                else
                {
                    continue;
                }
            }

            VaryByArchitecture(variants, server, tfm, skip, type);
        }
    }

    private void VaryByArchitecture(List<TestVariant> variants, ServerType server, string tfm, string skip, ApplicationType type)
    {
        foreach (var arch in Architectures)
        {
            if (!IsArchitectureSupportedOnServer(arch, server))
            {
                continue;
            }
            var archSkip = skip ?? SkipIfArchitectureNotSupportedOnCurrentSystem(arch);

            if (server == ServerType.IISExpress || server == ServerType.IIS)
            {
                VaryByAncmHostingModel(variants, server, tfm, type, arch, archSkip);
            }
            else
            {
                variants.Add(new TestVariant()
                {
                    Server = server,
                    Tfm = tfm,
                    ApplicationType = type,
                    Architecture = arch,
                    Skip = archSkip,
                });
            }
        }
    }

    private static string SkipIfArchitectureNotSupportedOnCurrentSystem(RuntimeArchitecture arch)
    {
        if (arch == RuntimeArchitecture.x64)
        {
            // Can't run x64 on a x86 OS.
            return (RuntimeInformation.OSArchitecture == Architecture.Arm || RuntimeInformation.OSArchitecture == Architecture.X86)
                ? $"Cannot run {arch} on your current system." : null;
        }

        // No x86 runtimes available on MacOS or Linux.
        return OperatingSystem.IsWindows() ? null : $"No {arch} available for non-Windows systems.";
    }

    private static bool IsArchitectureSupportedOnServer(RuntimeArchitecture arch, ServerType server)
    {
        // No x86 Mac/Linux runtime, don't generate a test variation that will always be skipped.
        return !(arch == RuntimeArchitecture.x86 && ServerType.Nginx == server);
    }

    private void VaryByAncmHostingModel(IList<TestVariant> variants, ServerType server, string tfm, ApplicationType type, RuntimeArchitecture arch, string skip)
    {
        foreach (var hostingModel in HostingModels)
        {
            var skipAncm = skip;
            if (hostingModel == HostingModel.InProcess)
            {
                // Not supported
                if (Tfm.Matches(Tfm.Net462, tfm) || Tfm.Matches(Tfm.NetCoreApp20, tfm))
                {
                    continue;
                }

                if (!IISExpressAncmSchema.SupportsInProcessHosting)
                {
                    skipAncm = skipAncm ?? IISExpressAncmSchema.SkipReason;
                }
            }

            variants.Add(new TestVariant()
            {
                Server = server,
                Tfm = tfm,
                ApplicationType = type,
                Architecture = arch,
                HostingModel = hostingModel,
                Skip = skipAncm,
            });
        }
    }

    private void CheckForSkips(List<TestVariant> variants)
    {
        foreach (var variant in variants)
        {
            foreach (var skipPair in Skips)
            {
                if (skipPair.Item1(variant))
                {
                    variant.Skip = skipPair.Item2;
                    break;
                }
            }
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable<object[]>)this).GetEnumerator();
    }

    // This is what Xunit MemberData expects
    public IEnumerator<object[]> GetEnumerator()
    {
        foreach (var v in Build())
        {
            yield return new[] { v };
        }
    }
}
