using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.App;

namespace YMhut.Box.Tests;

[TestClass]
[DoNotParallelize]
public sealed class InstallLayoutPathsTests
{
    [TestMethod]
    public void ResolveInstallRootPrefersCurrentInstallRootEnvironment()
    {
        using var workspace = TestWorkspace.Create("ymhut-install-root-tests");
        var installRoot = workspace.CreateDirectory("install");
        var baseRoot = workspace.CreateDirectory("runtime");

        using var environment = EnvironmentScope.Capture(
            InstallLayoutPaths.InstallRootEnvironmentVariable,
            InstallLayoutPaths.ArchivedLayoutEnvironmentVariable);
        environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
        environment.Set(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, null);

        Assert.AreEqual(installRoot, InstallLayoutPaths.ResolveInstallRoot(baseRoot));
        Assert.AreEqual(installRoot, InstallLayoutPaths.CandidateRoots(baseRoot).First());
    }

    [TestMethod]
    public void ResolveInstallRootIgnoresLegacyArchivedLayoutForHealth()
    {
        using var workspace = TestWorkspace.Create("ymhut-archived-layout-tests");
        var archivedRoot = workspace.CreateDirectory("archived");
        var baseRoot = workspace.CreateDirectory("runtime");
        workspace.CreateFile(["runtime", "YMhutBox.exe"], string.Empty);

        using var environment = EnvironmentScope.Capture(
            InstallLayoutPaths.InstallRootEnvironmentVariable,
            InstallLayoutPaths.ArchivedLayoutEnvironmentVariable);
        environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, null);
        environment.Set(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, archivedRoot);

        Assert.AreEqual(baseRoot, InstallLayoutPaths.ResolveInstallRoot(baseRoot));
        Assert.IsFalse(InstallLayoutPaths.CandidateRoots(baseRoot).Contains(archivedRoot));
        Assert.AreEqual(archivedRoot, InstallLayoutPaths.ResolveArchivedLayoutRoot());
    }

    [TestMethod]
    public void ResolveAssetsRootAndExecutableUseInstallRoot()
    {
        using var workspace = TestWorkspace.Create("ymhut-install-assets-tests");
        var installRoot = workspace.CreateDirectory("install");
        var assetsRoot = workspace.CreateDirectory("install", "Assets");
        var executable = workspace.CreateFile(["install", "YMhutBox.exe"], string.Empty);
        var runtimeRoot = workspace.CreateDirectory("runtime");

        using var environment = EnvironmentScope.Capture(
            InstallLayoutPaths.InstallRootEnvironmentVariable,
            InstallLayoutPaths.ArchivedLayoutEnvironmentVariable);
        environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
        environment.Set(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, null);

        Assert.AreEqual(assetsRoot, InstallLayoutPaths.ResolveAssetsRoot(runtimeRoot));
        Assert.AreEqual(executable, InstallLayoutPaths.ResolveInstalledExecutablePath(runtimeRoot));
    }

    [TestMethod]
    public void RuntimeLayoutPolicySkipsLargeToolsAndUserDataRoots()
    {
        foreach (var relativePath in new[]
        {
            "Tools/vendor/tool.exe",
            "Metadata/tools.json",
            "data/settings.json",
            "FeedbackPackages/feedback.zip"
        })
        {
            Assert.IsTrue(RuntimeLayoutPolicy.ShouldSkipRelativePath(relativePath), relativePath);
            Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile(relativePath), relativePath);
        }

        Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("unins000.exe"));
        Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("YMhutBox.exe"));
        Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("YMhutBox.dll"));
        Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("resources.pri"));
        Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("zh-CN/Microsoft.ui.xaml.dll.mui"));
        Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("lang/zh-CN/Microsoft.ui.xaml.dll.mui"));
        Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("runtimes/win-x64/native/Microsoft.ui.xaml.dll"));
    }

    [TestMethod]
    public void InstallManifestParserReadsReleaseRequiredFilesAndPayloadFiles()
    {
        var manifest = InstallManifest.Parse(
            """
            [Release]
            Version=2.0.6
            Build=2
            Channel=stable
            PackageVersion=2.0.6.2

            [RequiredFiles]
            YMhutBox.exe
            YMhutBox.dll

            [Files]
            YMhutBox.exe
            lang/zh-CN/Microsoft.ui.xaml.dll.mui
            Tools/SampleTool/tool.exe
            """);

        Assert.IsNotNull(manifest.Release);
        Assert.AreEqual("2.0.6", manifest.Release.Version);
        Assert.AreEqual("2", manifest.Release.Build);
        Assert.AreEqual("stable", manifest.Release.Channel);
        Assert.AreEqual("2.0.6.2", manifest.Release.PackageVersion);
        Assert.IsTrue(manifest.RequiredFiles.SequenceEqual(["YMhutBox.exe", "YMhutBox.dll"]));
        Assert.IsTrue(manifest.ContainsFile(@"lang\zh-CN\Microsoft.ui.xaml.dll.mui"));
        Assert.IsTrue(manifest.ContainsFile("Tools/SampleTool/tool.exe"));
    }

    [TestMethod]
    public void RuntimeLayoutPolicySkipsRuntimePayloadRoots()
    {
        foreach (var relativePath in new[]
        {
            "lang/en-US/Microsoft.UI.Xaml.Phone.dll.mui",
            "Microsoft.UI.Xaml/Microsoft.UI.Xaml.dll",
            "runtimes/win-x64/native/WebView2Loader.dll",
            "worker/YMhut.Box.Worker.exe"
        })
        {
            Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile(relativePath), relativePath);
        }
    }

    [TestMethod]
    public void SourceFilesDoNotContainCommonMojibakeSequences()
    {
        var root = FindRepositoryRoot();
        var scanRoots = new[]
        {
            Path.Combine(root, "src", "YMhut.Box.Core"),
            Path.Combine(root, "src", "box-winUI"),
            Path.Combine(root, "installer")
        };
        var extensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".cs", ".xaml", ".iss" };
        var mojibakeMarkers = new[]
        {
            "Ã",
            "Â",
            "â",
            "鈥",
            "锛",
            "銆",
            "鐑",
            "澶",
            "璁",
            "鏍",
            "棰",
            "缃"
        };

        var offenders = scanRoots
            .Where(Directory.Exists)
            .SelectMany(scanRoot => Directory.EnumerateFiles(scanRoot, "*.*", SearchOption.AllDirectories))
            .Where(path => extensions.Contains(Path.GetExtension(path)))
            .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
            .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
            .SelectMany(path => FindMojibakeMarkers(path, mojibakeMarkers))
            .Take(12)
            .ToArray();

        Assert.IsEmpty(offenders, string.Join(Environment.NewLine, offenders));
    }

    private static IEnumerable<string> FindMojibakeMarkers(string path, IReadOnlyList<string> markers)
    {
        var text = File.ReadAllText(path);
        foreach (var marker in markers)
        {
            if (text.Contains(marker, StringComparison.Ordinal))
            {
                yield return $"{Path.GetFileName(path)} contains '{marker}'";
            }
        }
    }

    private static string FindRepositoryRoot()
    {
        var directory = new DirectoryInfo(AppContext.BaseDirectory);
        while (directory is not null)
        {
            if (Directory.Exists(Path.Combine(directory.FullName, "src")) &&
                Directory.Exists(Path.Combine(directory.FullName, "installer")))
            {
                return directory.FullName;
            }

            directory = directory.Parent;
        }

        Assert.Fail("Repository root could not be found from test output directory.");
        return AppContext.BaseDirectory;
    }

    private sealed class EnvironmentScope : IDisposable
    {
        private readonly Dictionary<string, string?> _snapshot;

        private EnvironmentScope(IEnumerable<string> names)
        {
            _snapshot = names.ToDictionary(name => name, Environment.GetEnvironmentVariable, StringComparer.Ordinal);
        }

        public static EnvironmentScope Capture(params string[] names) => new(names);

        public void Set(string name, string? value) => Environment.SetEnvironmentVariable(name, value);

        public void Dispose()
        {
            foreach (var item in _snapshot)
            {
                Environment.SetEnvironmentVariable(item.Key, item.Value);
            }
        }
    }

    private sealed class TestWorkspace : IDisposable
    {
        private TestWorkspace(string root)
        {
            Root = root;
        }

        public string Root { get; }

        public static TestWorkspace Create(string prefix)
        {
            var root = Path.Combine(Path.GetTempPath(), prefix, Guid.NewGuid().ToString("N"));
            Directory.CreateDirectory(root);
            return new TestWorkspace(root);
        }

        public string CreateDirectory(params string[] parts)
        {
            var directory = Path.Combine([Root, .. parts]);
            Directory.CreateDirectory(directory);
            return directory;
        }

        public string CreateFile(string[] parts, string contents)
        {
            var path = Path.Combine([Root, .. parts]);
            Directory.CreateDirectory(Path.GetDirectoryName(path)!);
            File.WriteAllText(path, contents);
            return path;
        }

        public void Dispose()
        {
            if (Directory.Exists(Root))
            {
                Directory.Delete(Root, recursive: true);
            }
        }
    }
}
