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

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.AspNetCore.Shared;

namespace Microsoft.AspNetCore.InternalTesting;

/// <summary>
/// Provides access to file storage for the running test. Get access by
/// implementing <see cref="ITestMethodLifecycle"/>, and accessing <see cref="TestContext.FileOutput"/>.
/// </summary>
/// <remarks>
/// Requires defining <see cref="AspNetTestFramework"/> as the test framework.
/// </remarks>
public sealed class TestFileOutputContext
{
    private static readonly char[] InvalidFileChars = new char[]
    {
            '\"', '<', '>', '|', '\0',
            (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
            (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
            (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
            (char)31, ':', '*', '?', '\\', '/', ' ', (char)127
    };

    private readonly TestContext _parent;

    public TestFileOutputContext(TestContext parent)
    {
        _parent = parent;

        TestName = GetTestMethodName(parent.TestMethod, parent.MethodArguments);
        TestClassName = GetTestClassName(parent.TestClass);

        AssemblyOutputDirectory = GetAssemblyBaseDirectory(_parent.TestClass.Assembly);
        if (!string.IsNullOrEmpty(AssemblyOutputDirectory))
        {
            TestClassOutputDirectory = Path.Combine(AssemblyOutputDirectory, TestClassName);
        }
    }

    public string TestName { get; }

    public string TestClassName { get; }

    public string AssemblyOutputDirectory { get; }

    public string TestClassOutputDirectory { get; }

    public string GetUniqueFileName(string prefix, string extension)
    {
        ArgumentNullThrowHelper.ThrowIfNull(prefix);

        if (extension != null && !extension.StartsWith(".", StringComparison.Ordinal))
        {
            throw new ArgumentException("The extension must start with '.' if one is provided.", nameof(extension));
        }

        var path = Path.Combine(TestClassOutputDirectory, $"{prefix}{extension}");

        var i = 1;
        while (File.Exists(path))
        {
            path = Path.Combine(TestClassOutputDirectory, $"{prefix}{i++}{extension}");
        }

        return path;
    }

    // Gets the output directory without appending the TFM or assembly name.
    public static string GetOutputDirectory(Assembly assembly)
    {
        var attribute = assembly.GetCustomAttributes().OfType<TestOutputDirectoryAttribute>().FirstOrDefault();
        return attribute?.BaseDirectory;
    }

    public static string GetAssemblyBaseDirectory(Assembly assembly, string baseDirectory = null)
    {
        var attribute = assembly.GetCustomAttributes().OfType<TestOutputDirectoryAttribute>().FirstOrDefault();
        baseDirectory = baseDirectory ?? attribute?.BaseDirectory;
        if (string.IsNullOrEmpty(baseDirectory))
        {
            return string.Empty;
        }

        return Path.Combine(baseDirectory, assembly.GetName().Name, attribute.TargetFramework);
    }

    public static bool GetPreserveExistingLogsInOutput(Assembly assembly)
    {
        var attribute = assembly.GetCustomAttributes().OfType<TestOutputDirectoryAttribute>().FirstOrDefault();
        return attribute.PreserveExistingLogsInOutput;
    }

    public static string GetTestClassName(Type type)
    {
        var shortNameAttribute =
            type.GetCustomAttribute<ShortClassNameAttribute>() ??
            type.Assembly.GetCustomAttribute<ShortClassNameAttribute>();
        var name = shortNameAttribute == null ? type.FullName : type.Name;

        // Try to shorten the class name using the assembly name
        var assemblyName = type.Assembly.GetName().Name;
        if (name.StartsWith(assemblyName + ".", StringComparison.Ordinal))
        {
            name = name.Substring(assemblyName.Length + 1);
        }

        return name;
    }

    public static string GetTestMethodName(MethodInfo method, object[] arguments)
    {
        var name = arguments.Aggregate(method.Name, (a, b) => $"{a}-{(b ?? "null")}");
        return RemoveIllegalFileChars(name);
    }

    public static string RemoveIllegalFileChars(string s)
    {
        var sb = new StringBuilder();

        foreach (var c in s)
        {
            if (InvalidFileChars.Contains(c))
            {
                if (sb.Length > 0 && sb[sb.Length - 1] != '_')
                {
                    sb.Append('_');
                }
            }
            else
            {
                sb.Append(c);
            }
        }
        return sb.ToString();
    }
}
