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

namespace YMhut.Box.Tests;

[TestClass]
[DoNotParallelize]
public sealed class ReferenceToolsTests
{
    [TestMethod]
    public async Task ExternalToolCatalogScansLaunchablesFromToolsDirectory()
    {
        var category = "YMhutTestTools_" + Guid.NewGuid().ToString("N");
        var toolsRoot = Path.Combine(AppContext.BaseDirectory, "Tools");
        var categoryRoot = Path.Combine(toolsRoot, category);
        var toolRoot = Path.Combine(categoryRoot, "SampleTool");
        Directory.CreateDirectory(toolRoot);
        var launchPath = Path.Combine(toolRoot, "SampleTool.cmd");
        await File.WriteAllTextAsync(launchPath, "@echo off\r\necho YMhut");

        var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-reference-tests", Guid.NewGuid().ToString("N"));
        try
        {
            var service = new ExternalToolCatalogService(AppPaths.ForCurrentUser(tempRoot));

            var modules = await service.GetModulesAsync();
            var module = modules.FirstOrDefault(item => item.Tool.CategoryName == category);

            Assert.IsNotNull(module);
            Assert.AreEqual("SampleTool", module.Tool.Name);
            Assert.AreEqual("CMD", module.Tool.Extension);
            Assert.AreEqual(ReferenceToolRiskLevel.High, module.Tool.RiskLevel);
            Assert.IsTrue(module.Id.StartsWith(ExternalToolModule.IdPrefix, StringComparison.OrdinalIgnoreCase));
        }
        finally
        {
            if (Directory.Exists(categoryRoot))
            {
                Directory.Delete(categoryRoot, recursive: true);
            }

            if (Directory.Exists(tempRoot))
            {
                Directory.Delete(tempRoot, recursive: true);
            }
        }
    }

    [TestMethod]
    public async Task ExternalToolCatalogUsesInstallRootForToolsAndMetadata()
    {
        var category = "InstallRootCategory_" + Guid.NewGuid().ToString("N");
        var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-install-tool-tests", Guid.NewGuid().ToString("N"));
        var installRoot = Path.Combine(tempRoot, "install");
        var toolRoot = Path.Combine(installRoot, "Tools", category, "MetaTool");
        var metadataRoot = Path.Combine(installRoot, "Metadata");
        var appRoot = Path.Combine(tempRoot, "appdata");
        var launchPath = Path.Combine(toolRoot, "MetaTool.cmd");

        var oldInstallRoot = Environment.GetEnvironmentVariable(InstallLayoutPaths.InstallRootEnvironmentVariable);
        var oldArchivedRoot = Environment.GetEnvironmentVariable(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable);
        try
        {
            Directory.CreateDirectory(toolRoot);
            Directory.CreateDirectory(metadataRoot);
            await File.WriteAllTextAsync(launchPath, "@echo off\r\necho install-root");
            await File.WriteAllTextAsync(
                Path.Combine(metadataRoot, "tools.json"),
                """
                {
                  "tools": [
                    {
                      "match": "MetaTool",
                      "description": "Install root metadata description",
                      "publisher": "YMhut Tests",
                      "launchTarget": "MetaTool.cmd",
                      "tags": [ "install-root" ]
                    }
                  ]
                }
                """);

            Environment.SetEnvironmentVariable(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
            Environment.SetEnvironmentVariable(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, null);

            var service = new ExternalToolCatalogService(AppPaths.ForCurrentUser(appRoot));
            var modules = await service.GetModulesAsync();
            var module = modules.FirstOrDefault(item => item.Tool.CategoryName == category);

            Assert.AreEqual(Path.Combine(installRoot, "Tools"), service.ResolveToolsRoot());
            Assert.IsNotNull(module);
            Assert.AreEqual("MetaTool", module.Tool.Name);
            Assert.AreEqual("Install root metadata description", module.Tool.Description);
            Assert.AreEqual("YMhut Tests", module.Tool.Publisher);
            Assert.Contains("install-root", module.Tool.Tags);
            Assert.IsTrue(module.Tool.LaunchPath.StartsWith(installRoot, StringComparison.OrdinalIgnoreCase));
        }
        finally
        {
            Environment.SetEnvironmentVariable(InstallLayoutPaths.InstallRootEnvironmentVariable, oldInstallRoot);
            Environment.SetEnvironmentVariable(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, oldArchivedRoot);

            if (Directory.Exists(tempRoot))
            {
                Directory.Delete(tempRoot, recursive: true);
            }
        }
    }

    [TestMethod]
    public async Task RiskConfirmationStorePersistsAndResetsChoices()
    {
        var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-risk-tests", Guid.NewGuid().ToString("N"));
        try
        {
            var paths = AppPaths.ForCurrentUser(tempRoot);
            var store = new RiskConfirmationStore(paths);

            await store.RememberAsync("builtin:hosts-editor", "execute");

            var secondStore = new RiskConfirmationStore(paths);
            Assert.IsTrue(await secondStore.IsRememberedAsync("builtin:hosts-editor", "execute"));

            await secondStore.ResetAsync();
            Assert.IsFalse(await secondStore.IsRememberedAsync("builtin:hosts-editor", "execute"));
        }
        finally
        {
            if (Directory.Exists(tempRoot))
            {
                Directory.Delete(tempRoot, recursive: true);
            }
        }
    }

    [TestMethod]
    public async Task OpenSourceReferenceServiceLoadsBundledToolNoticesFromInstallRoot()
    {
        var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-install-reference-tests", Guid.NewGuid().ToString("N"));
        var installRoot = Path.Combine(tempRoot, "install");
        var noticePath = Path.Combine(installRoot, "Tools", "Category", "VendorTool", "LICENSE.txt");
        var appRoot = Path.Combine(tempRoot, "appdata");

        var oldInstallRoot = Environment.GetEnvironmentVariable(InstallLayoutPaths.InstallRootEnvironmentVariable);
        var oldArchivedRoot = Environment.GetEnvironmentVariable(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable);
        try
        {
            Directory.CreateDirectory(Path.GetDirectoryName(noticePath)!);
            await File.WriteAllTextAsync(noticePath, "Vendor license text");

            Environment.SetEnvironmentVariable(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
            Environment.SetEnvironmentVariable(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, null);

            var service = new OpenSourceReferenceService(AppPaths.ForCurrentUser(appRoot));
            var references = await service.GetReferencesAsync();

            Assert.IsTrue(references.Any(item =>
                item.Kind == "Third-party tool notice" &&
                string.Equals(item.Path, noticePath, StringComparison.OrdinalIgnoreCase)));
        }
        finally
        {
            Environment.SetEnvironmentVariable(InstallLayoutPaths.InstallRootEnvironmentVariable, oldInstallRoot);
            Environment.SetEnvironmentVariable(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, oldArchivedRoot);

            if (Directory.Exists(tempRoot))
            {
                Directory.Delete(tempRoot, recursive: true);
            }
        }
    }

    [TestMethod]
    public void BuiltinReferenceCatalogRegistersExpectedToolsAndRiskLevels()
    {
        var catalog = new BuiltinReferenceToolCatalog();
        var modules = catalog.GetModules();
        var ids = modules.Select(module => module.Definition.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);

        foreach (var id in new[]
        {
            "cert-block",
            "port-viewer",
            "hosts-editor",
            "keyboard-test",
            "junk-cleaner",
            "bsod-analysis",
            "winget-installer",
            "battery-report",
            "speed-test",
            "wifi-password",
            "disk-space-analyzer",
            "lite-monitor",
            "windows-activation",
            "defender-control",
            "cpu-ranking",
            "gpu-ranking",
            "context-menu-mgr"
        })
        {
            Assert.Contains(id, ids);
        }

        Assert.AreEqual(ReferenceToolRiskLevel.High, catalog.GetByModuleId("builtin:hosts-editor")?.RiskLevel);
        Assert.AreEqual(ReferenceToolRiskLevel.None, catalog.GetByModuleId("builtin:windows-activation")?.RiskLevel);
        Assert.HasCount(17, modules);
    }

    [TestMethod]
    public async Task ResilientLogServiceFallsBackToJsonlWhenPrimaryFails()
    {
        var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-log-fallback-tests", Guid.NewGuid().ToString("N"));
        try
        {
            var service = new ResilientLogService(new ThrowingLogService(), AppPaths.ForCurrentUser(tempRoot));

            await service.WriteAsync("Information", "tool-run", "External tool success", "toolId=external:test");
            var entries = await service.ReadAsync(take: 10);

            Assert.HasCount(1, entries);
            Assert.AreEqual("tool-run", entries[0].Category);
            Assert.IsTrue(File.Exists(Path.Combine(tempRoot, "Logs", "app-log-fallback.jsonl")));
        }
        finally
        {
            if (Directory.Exists(tempRoot))
            {
                Directory.Delete(tempRoot, recursive: true);
            }
        }
    }

    [TestMethod]
    public async Task OpenSourceReferenceServiceLoadsPackagesAndBundledToolNotices()
    {
        var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-reference-attribution-tests", Guid.NewGuid().ToString("N"));
        var toolsRoot = Path.Combine(AppContext.BaseDirectory, "Tools");
        var noticeCategory = "YMhutTestNotice_" + Guid.NewGuid().ToString("N");
        var noticeCategoryRoot = Path.Combine(toolsRoot, noticeCategory);
        var noticePath = Path.Combine(noticeCategoryRoot, "SampleVendor", "LICENSE.txt");
        try
        {
            var projectRoot = Path.Combine(tempRoot, "src", "FakeProject");
            Directory.CreateDirectory(projectRoot);
            await File.WriteAllTextAsync(
                Path.Combine(projectRoot, "FakeProject.csproj"),
                """
                <Project Sdk="Microsoft.NET.Sdk">
                  <ItemGroup>
                    <PackageReference Include="Sample.Reference.Package" Version="1.2.3" />
                  </ItemGroup>
                </Project>
                """);

            Directory.CreateDirectory(Path.GetDirectoryName(noticePath)!);
            await File.WriteAllTextAsync(noticePath, "Sample vendor license");

            var service = new OpenSourceReferenceService(AppPaths.ForCurrentUser(tempRoot));
            var references = await service.GetReferencesAsync();

            Assert.IsTrue(references.Any(item => item.Kind == "Reference project attribution" && item.Name.Contains("tubatool", StringComparison.OrdinalIgnoreCase)));
            Assert.IsTrue(references.Any(item => item.Kind == "NuGet package" && item.Name == "Sample.Reference.Package" && item.Version == "1.2.3"));
            Assert.IsTrue(references.Any(item => item.Kind == "Third-party tool notice" && item.Path == noticePath));
        }
        finally
        {
            if (Directory.Exists(noticeCategoryRoot))
            {
                Directory.Delete(noticeCategoryRoot, recursive: true);
            }

            if (Directory.Exists(tempRoot))
            {
                Directory.Delete(tempRoot, recursive: true);
            }
        }
    }

    private sealed class ThrowingLogService : ILogService
    {
        public event EventHandler<LogEntry>? EntryWritten;

        public string LogPath => "throwing";

        public Task WriteAsync(string level, string category, string message, string? detail = null, CancellationToken cancellationToken = default)
        {
            EntryWritten?.Invoke(this, new LogEntry(DateTimeOffset.Now, level, category, message, detail));
            throw new IOException("primary failed");
        }

        public Task<IReadOnlyList<LogEntry>> ReadAsync(string? level = null, string? category = null, int take = 500, CancellationToken cancellationToken = default)
            => throw new IOException("primary failed");

        public Task<IReadOnlyList<LogEntry>> ReadByDateAsync(DateOnly date, string? level = null, string? query = null, int take = 0, CancellationToken cancellationToken = default)
            => throw new IOException("primary failed");

        public Task<IReadOnlyList<LogEntry>> ReadByDatePageAsync(DateOnly date, string? level = null, string? query = null, int skip = 0, int take = 100, CancellationToken cancellationToken = default)
            => throw new IOException("primary failed");

        public Task CleanupAsync(int retentionCount, CancellationToken cancellationToken = default)
            => throw new IOException("primary failed");

        public Task ClearAllAsync(CancellationToken cancellationToken = default)
            => throw new IOException("primary failed");

        public Task ClearTodayAsync(CancellationToken cancellationToken = default)
            => throw new IOException("primary failed");

        public Task ClearFilteredTodayAsync(string? level = null, string? query = null, CancellationToken cancellationToken = default)
            => throw new IOException("primary failed");
    }
}
