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

using System.Net.Http;
using System.Reflection;
using System.Security.Cryptography;
using AngleSharp.Dom.Html;
using AngleSharp.Parser.Html;
using Microsoft.AspNetCore.InternalTesting;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Identity.Test;

[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/38542", Queues = "OSX.1015.Amd64.Open;OSX.1015.Amd64")] //slow
public class IdentityUIScriptsTest : IDisposable
{
    private readonly ITestOutputHelper _output;
    private readonly HttpClient _httpClient;

    public IdentityUIScriptsTest(ITestOutputHelper output)
    {
        _output = output;
        _httpClient = new HttpClient(new RetryHandler(new HttpClientHandler() { }, output, TimeSpan.FromSeconds(1), 5));
    }

    public static IEnumerable<object[]> ScriptWithIntegrityData
    {
        get
        {
            return GetScriptTags()
                .Where(st => st.Integrity != null)
                .Select(st => new object[] { st });
        }
    }

    [Theory]
    [MemberData(nameof(ScriptWithIntegrityData))]
    public async Task IdentityUI_ScriptTags_SubresourceIntegrityCheck(ScriptTag scriptTag)
    {
        var integrity = await GetShaIntegrity(scriptTag);
        Assert.Equal(scriptTag.Integrity, integrity);
    }

    private async Task<string> GetShaIntegrity(ScriptTag scriptTag)
    {
        var isSha256 = scriptTag.Integrity.StartsWith("sha256", StringComparison.Ordinal);
        var isSha384 = scriptTag.Integrity.StartsWith("sha384", StringComparison.Ordinal);
        var prefix = isSha256 ? "sha256" : (isSha384 ? "sha384" : "sha512");
        using (var respStream = await _httpClient.GetStreamAsync(scriptTag.Src))
        using (var alg256 = SHA256.Create())
        using (var alg384 = SHA384.Create())
        using (var alg512 = SHA512.Create())
        {
            byte[] hash;
            if (isSha256)
            {
                hash = alg256.ComputeHash(respStream);
            }else if(isSha384)
            {
                hash = alg384.ComputeHash(respStream);
            }
            else
            {
                hash = alg512.ComputeHash(respStream);
            }
            return $"{prefix}-" + Convert.ToBase64String(hash);
        }
    }

    public static IEnumerable<object[]> ScriptWithFallbackSrcData
    {
        get
        {
            return GetScriptTags()
                .Where(st => st.FallbackSrc != null)
                .Select(st => new object[] { st });
        }
    }

    [Theory]
    [MemberData(nameof(ScriptWithFallbackSrcData))]
    public async Task IdentityUI_ScriptTags_FallbackSourceContent_Matches_CDNContent(ScriptTag scriptTag)
    {
        var wwwrootDir = Path.Combine(GetProjectBasePath(), "assets", scriptTag.Version);
        var cdnContent = await _httpClient.GetStringAsync(scriptTag.Src);
        var fallbackSrcContent = File.ReadAllText(
            Path.Combine(wwwrootDir, scriptTag.FallbackSrc.Replace("Identity", "").TrimStart('~').TrimStart('/')));

        Assert.Equal(RemoveLineEndings(cdnContent), RemoveLineEndings(fallbackSrcContent));
    }

    public struct ScriptTag
    {
        public string Version;
        public string Src;
        public string Integrity;
        public string FallbackSrc;
        public string File;

        public override string ToString()
        {
            return Src;
        }
    }

    private static List<ScriptTag> GetScriptTags()
    {
        var scriptTags = new List<ScriptTag>();
        var uiDirV4 = Path.Combine(GetProjectBasePath(), "Areas", "Identity", "Pages", "V4");
        var uiDirV5 = Path.Combine(GetProjectBasePath(), "Areas", "Identity", "Pages", "V5");
        var cshtmlFiles = GetRazorFiles(uiDirV4).Concat(GetRazorFiles(uiDirV5));
        foreach (var cshtmlFile in cshtmlFiles)
        {
            var tags = GetScriptTags(cshtmlFile);
            scriptTags.AddRange(tags);
        }

        Assert.NotEmpty(scriptTags);

        return scriptTags;

        IEnumerable<string> GetRazorFiles(string dir) => Directory.GetFiles(dir, "*.cshtml", SearchOption.AllDirectories);
    }

    private static List<ScriptTag> GetScriptTags(string cshtmlFile)
    {
        IHtmlDocument htmlDocument;
        var htmlParser = new HtmlParser();
        using (var stream = File.OpenRead(cshtmlFile))
        {
            htmlDocument = htmlParser.Parse(stream);
        }

        var scriptTags = new List<ScriptTag>();
        foreach (var scriptElement in htmlDocument.Scripts)
        {
            var fallbackSrcAttribute = scriptElement.Attributes
                .FirstOrDefault(attr => string.Equals("asp-fallback-src", attr.Name, StringComparison.OrdinalIgnoreCase));

            scriptTags.Add(new ScriptTag
            {
                Version = cshtmlFile.Contains("V4") ? "V4" : "V5",
                Src = scriptElement.Source,
                Integrity = scriptElement.Integrity,
                FallbackSrc = fallbackSrcAttribute?.Value,
                File = cshtmlFile
            });
        }
        return scriptTags;
    }

    private static string RemoveLineEndings(string originalString)
    {
        return originalString.Replace("\r\n", "").Replace("\n", "");
    }

    public void Dispose()
    {
        _httpClient.Dispose();
    }

    private static string GetProjectBasePath()
    {
        var projectPath = typeof(IdentityUIScriptsTest).Assembly.GetCustomAttributes<AssemblyMetadataAttribute>()
            .Single(a => a.Key == "Microsoft.AspNetCore.InternalTesting.DefaultUIProjectPath").Value;
        return Directory.Exists(projectPath) ? projectPath : Path.Combine(FindHelixSlnFileDirectory(), "UI");
    }

    private static string FindHelixSlnFileDirectory()
    {
        var applicationPath = Path.GetDirectoryName(typeof(IdentityUIScriptsTest).Assembly.Location);
        var directoryInfo = new DirectoryInfo(applicationPath);
        do
        {
            var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, "*.sln").FirstOrDefault();
            if (solutionPath != null)
            {
                return directoryInfo.FullName;
            }

            directoryInfo = directoryInfo.Parent;
        }
        while (directoryInfo.Parent != null);

        throw new InvalidOperationException($"Solution root could not be located using application root {applicationPath}.");
    }
}
