using System.Net;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.App;
using YMhut.Box.Core.DevEnvironments;
using YMhut.Box.Core.Downloads;
using YMhut.Box.Core.Net;
using YMhut.Box.Core.Updates;

namespace YMhut.Box.Tests;

[TestClass]
public sealed class DownloadAndDevEnvironmentTests
{
    [TestMethod]
    public void DevEnvironmentVersionParserRecognizesSupportedTools()
    {
        Assert.AreEqual("1.23.4", DevEnvironmentVersionParser.Parse("go", "go version go1.23.4 windows/amd64"));
        Assert.AreEqual("3.12.7", DevEnvironmentVersionParser.Parse("python", "Python 3.12.7"));
        Assert.AreEqual("21.0.2", DevEnvironmentVersionParser.Parse("java", "openjdk version \"21.0.2\" 2024-01-16"));
        Assert.AreEqual("27.3.1", DevEnvironmentVersionParser.Parse("docker", "Docker version 27.3.1, build ce12230"));
        Assert.AreEqual("8.0.36", DevEnvironmentVersionParser.Parse("mysql", "mysql  Ver 8.0.36 for Win64 on x86_64"));
        Assert.AreEqual("22.11.0", DevEnvironmentVersionParser.Parse("node", "v22.11.0"));
        Assert.AreEqual("10.0.100", DevEnvironmentVersionParser.Parse("dotnet", "10.0.100\r\n9.0.308"));
        Assert.AreEqual("2.47.1.windows.2", DevEnvironmentVersionParser.Parse("git", "git version 2.47.1.windows.2"));
        Assert.AreEqual("1.82.0", DevEnvironmentVersionParser.Parse("rust", "rustc 1.82.0 (f6e511eec 2024-10-15)"));
        Assert.AreEqual("3.31.0", DevEnvironmentVersionParser.Parse("cmake", "cmake version 3.31.0"));
    }

    [TestMethod]
    public async Task DevEnvironmentCatalogReadsOfficialVersionMetadata()
    {
        var workspace = TempWorkspace();
        var paths = AppPaths.ForCurrentUser(workspace);
        var service = new DevEnvironmentCatalogService(
            paths,
            new FakeHttpService(uri => uri.AbsoluteUri switch
            {
                "https://go.dev/dl/?mode=json" => """
                    [
                      {
                        "version": "go1.23.4",
                        "stable": true,
                        "files": [
                          { "filename": "go1.23.4.windows-amd64.msi", "sha256": "abc" },
                          { "filename": "go1.23.4.src.tar.gz", "sha256": "def" }
                        ]
                      }
                    ]
                    """,
                "https://nodejs.org/dist/index.json" => """
                    [
                      { "version": "v22.11.0", "date": "2024-10-29" }
                    ]
                    """,
                "https://api.github.com/repos/git-for-windows/git/releases" => """
                    [
                      {
                        "tag_name": "v2.47.1.windows.2",
                        "published_at": "2024-10-01T00:00:00Z",
                        "html_url": "https://github.com/git-for-windows/git/releases/tag/v2.47.1.windows.2",
                        "assets": [
                          {
                            "name": "Git-2.47.1-64-bit.exe",
                            "browser_download_url": "https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.2/Git-2.47.1-64-bit.exe",
                            "size": 1024
                          }
                        ]
                      }
                    ]
                    """,
                _ => """
                    {
                      "releases-index": [
                        {
                          "channel-version": "10.0",
                          "latest-sdk": "10.0.100",
                          "release-date": "2025-11-11"
                        }
                      ]
                    }
                    """
            }));

        var go = await service.GetVersionsAsync("go");
        var node = await service.GetVersionsAsync("node");
        var dotnet = await service.GetVersionsAsync("dotnet");

        Assert.AreEqual("1.23.4", go[0].Version);
        Assert.AreEqual("go1.23.4.windows-amd64.msi", go[0].Installer?.FileName);
        Assert.AreEqual("go1.23.4.src.tar.gz", go[0].SourceArchive?.FileName);
        Assert.AreEqual("22.11.0", node[0].Version);
        Assert.AreEqual("node-v22.11.0-x64.msi", node[0].Installer?.FileName);
        Assert.IsTrue(node[0].AllCandidates.Any(candidate => candidate.SourceType == "MirrorCN"));
        Assert.AreEqual("10.0.100", dotnet[0].Version);
        Assert.AreEqual("dotnet-sdk-10.0.100-win-x64.exe", dotnet[0].Installer?.FileName);
    }

    [TestMethod]
    public async Task DevEnvironmentCatalogReadsGithubReleaseAssets()
    {
        var workspace = TempWorkspace();
        var service = new DevEnvironmentCatalogService(
            AppPaths.ForCurrentUser(workspace),
            new FakeHttpService(_ => """
                [
                  {
                    "tag_name": "v3.31.0",
                    "published_at": "2024-11-01T00:00:00Z",
                    "html_url": "https://github.com/Kitware/CMake/releases/tag/v3.31.0",
                    "assets": [
                      {
                        "name": "cmake-3.31.0-windows-x86_64.msi",
                        "browser_download_url": "https://github.com/Kitware/CMake/releases/download/v3.31.0/cmake-3.31.0-windows-x86_64.msi",
                        "size": 2048
                      },
                      {
                        "name": "cmake-3.31.0.tar.gz",
                        "browser_download_url": "https://github.com/Kitware/CMake/releases/download/v3.31.0/cmake-3.31.0.tar.gz",
                        "size": 4096
                      }
                    ]
                  }
                ]
                """));

        var cmake = await service.GetVersionsAsync("cmake");

        Assert.HasCount(1, cmake);
        Assert.AreEqual("3.31.0", cmake[0].Version);
        Assert.IsTrue(cmake[0].AllCandidates.Any(candidate => candidate.Mode == DevEnvironmentInstallMode.QuickInstall && candidate.SourceType == "GitHub"));
        Assert.IsTrue(cmake[0].AllCandidates.Any(candidate => candidate.Mode == DevEnvironmentInstallMode.SourceBuild && candidate.Source.SizeBytes == 4096));
    }

    [TestMethod]
    public async Task DownloadQueueStorePersistsAndRestoresItems()
    {
        var workspace = TempWorkspace();
        var store = new DownloadQueueStore(AppPaths.ForCurrentUser(workspace));
        var item = DownloadItem.Create(
                new DownloadSource("https://example.com/tool.exe", "Tool", "tool.exe"),
                Path.Combine(workspace, "tool.exe"),
                "installer")
            .WithResumeMetadata(
                "\"abc\"",
                "Wed, 10 Jun 2026 08:00:00 GMT",
                "bytes",
                2048,
                "https://cdn.example.com/tool.exe",
                true)
            .WithProgress(DownloadState.Completed, 1024, 2048, 0);

        await store.SaveAsync([item]);
        var loaded = await store.LoadAsync();

        Assert.HasCount(1, loaded);
        Assert.AreEqual(item.Id, loaded[0].Id);
        Assert.AreEqual(DownloadState.Completed, loaded[0].State);
        Assert.AreEqual("tool.exe", loaded[0].Source.FileName);
        Assert.AreEqual("installer", loaded[0].InstallCommand);
        Assert.AreEqual(Path.Combine(workspace, "tool.exe.partial"), loaded[0].EffectivePartialPath);
        Assert.AreEqual("\"abc\"", loaded[0].ETag);
        Assert.AreEqual("Wed, 10 Jun 2026 08:00:00 GMT", loaded[0].LastModified);
        Assert.AreEqual("bytes", loaded[0].AcceptRanges);
        Assert.AreEqual(2048, loaded[0].ContentLength);
        Assert.AreEqual("https://cdn.example.com/tool.exe", loaded[0].FinalUrl);
        Assert.IsTrue(loaded[0].ResumeSupported);
    }

    [TestMethod]
    public async Task DownloadQueueStorePersistsSettings()
    {
        var workspace = TempWorkspace();
        var store = new DownloadQueueStore(AppPaths.ForCurrentUser(workspace));
        var directory = Path.Combine(workspace, "custom");

        await store.SaveSettingsAsync(new DownloadSettings(directory, 99));
        var loaded = await store.LoadSettingsAsync();

        Assert.AreEqual(directory, loaded.DefaultDirectory);
        Assert.AreEqual(5, loaded.MaxConcurrentDownloads);
    }

    [TestMethod]
    public void DownloadProgressAndProtocolRoundTrip()
    {
        var progress = new DownloadProgressSnapshot(
            "a",
            DownloadState.Running,
            512,
            1024,
            2048,
            ETag: "\"abc\"",
            LastModified: "Wed, 10 Jun 2026 08:00:00 GMT",
            AcceptRanges: "bytes",
            ContentLength: 1024,
            FinalUrl: "https://cdn.example.com/a.zip");
        var message = new DownloadHostMessage(DownloadHostProtocol.Progress, progress.Id, DownloadHostProtocol.Version, Progress: progress);

        var serialized = DownloadHostProtocol.Serialize(message);
        var roundTrip = DownloadHostProtocol.Deserialize(serialized);

        Assert.AreEqual(0.5, progress.Progress, 0.001);
        Assert.IsNotNull(roundTrip);
        Assert.AreEqual(DownloadHostProtocol.Progress, roundTrip.Type);
        Assert.AreEqual(DownloadState.Running, roundTrip.Progress?.State);
        Assert.AreEqual(2048, roundTrip.Progress?.BytesPerSecond);
        Assert.AreEqual("\"abc\"", roundTrip.Progress?.ETag);
        Assert.AreEqual("bytes", roundTrip.Progress?.AcceptRanges);
        Assert.AreEqual("https://cdn.example.com/a.zip", roundTrip.Progress?.FinalUrl);
    }

    [TestMethod]
    public async Task DirectDownloadValidatorParsesInternetShortcut()
    {
        using var validator = new DirectDownloadValidator(new HttpClient(new StubHttpHandler(request =>
        {
            var content = new ByteArrayContent(Encoding.UTF8.GetBytes("package"));
            content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/zip");
            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                RequestMessage = request,
                Content = content
            };
        })));

        var result = await validator.ValidateAsync("""
            [InternetShortcut]
            URL=https://example.com/package.zip
            """);

        Assert.IsTrue(result.IsDirectDownload);
        Assert.IsTrue(result.WasInternetShortcut);
        Assert.AreEqual("https://example.com/package.zip", result.EffectiveUrl);
    }

    [TestMethod]
    public async Task DirectDownloadValidatorRejectsHtmlDownloadPage()
    {
        using var validator = new DirectDownloadValidator(new HttpClient(new StubHttpHandler(request =>
        {
            var content = new ByteArrayContent(Encoding.UTF8.GetBytes("<!doctype html><html></html>"));
            content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/html");
            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                RequestMessage = request,
                Content = content
            };
        })));

        var result = await validator.ValidateAsync("https://example.com/download");

        Assert.IsFalse(result.IsDirectDownload);
        StringAssert.Contains(result.Message, "web page");
    }

    [TestMethod]
    public async Task ReleaseManifestSerializerRoundTripsFullInstallerManifest()
    {
        var workspace = TempWorkspace();
        var path = Path.Combine(workspace, "manifest.json");
        var manifest = new ReleaseManifest(
            "2.0.7.0",
            "2.0.7.0",
            "stable",
            DateTimeOffset.Parse("2026-06-13T00:00:00Z"),
            [
                new ReleaseFileEntry("downloads/YMhut_Box_WinUI_Setup_2.0.7.0.exe", 445_000_000, "installer"),
                new ReleaseFileEntry("downloads/YMhut_Box_WinUI_2.0.7.0.msix", 571_000_000, "msix")
            ],
            [],
            "https://update.ymhut.cn/downloads/",
            "Full");

        await ReleaseManifestSerializer.WriteManifestAsync(path, manifest);
        var loaded = await ReleaseManifestSerializer.ReadManifestAsync(path);

        Assert.AreEqual("2.0.7.0", loaded.Version);
        Assert.AreEqual("stable", loaded.Channel);
        Assert.AreEqual("Full", loaded.Flavor);
        Assert.HasCount(2, loaded.Files);
        Assert.IsTrue(loaded.Files.All(file =>
            file.Path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ||
            file.Path.EndsWith(".msix", StringComparison.OrdinalIgnoreCase)));
    }

    [TestMethod]
    public void UpdateInfoEndpointPolicyPrefersCanonicalUpdateInfo()
    {
        var endpoints = UpdateInfoEndpointPolicy.BuildDefaultUris("https://update.ymhut.cn/");

        CollectionAssert.AreEqual(
            new[]
            {
                "https://update.ymhut.cn/api/client/bootstrap",
                "https://update.ymhut.cn/update-info.json",
                "https://update.ymhut.cn/update-info",
                "https://update.ymhut.cn/api/update-info"
            },
            endpoints.Select(endpoint => endpoint.ToString()).ToArray());
    }

    [TestMethod]
    public async Task HttpRedirectResolverFollowsMultiHopAndRelativeLocations()
    {
        using var client = new HttpClient(new StubHttpHandler(request =>
        {
            return request.RequestUri!.AbsoluteUri switch
            {
                "https://media.example/start" => new HttpResponseMessage(HttpStatusCode.Redirect)
                {
                    Headers = { Location = new Uri("/hop-1", UriKind.Relative) },
                    RequestMessage = request
                },
                "https://media.example/hop-1" => new HttpResponseMessage(HttpStatusCode.TemporaryRedirect)
                {
                    Headers = { Location = new Uri("https://cdn.example/final.mp4") },
                    RequestMessage = request
                },
                "https://cdn.example/final.mp4" => new HttpResponseMessage(HttpStatusCode.OK)
                {
                    RequestMessage = request,
                    Content = new ByteArrayContent(Array.Empty<byte>())
                },
                _ => new HttpResponseMessage(HttpStatusCode.NotFound)
                {
                    RequestMessage = request,
                    Content = new StringContent("missing")
                }
            };
        }));

        var resolved = await HttpRedirectResolver.ResolveAsync(new Uri("https://media.example/start"), client);

        Assert.AreEqual("https://cdn.example/final.mp4", resolved.ToString());
    }

    private static string TempWorkspace()
    {
        var path = Path.Combine(Path.GetTempPath(), "ymhut-box-download-tests", Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(path);
        return path;
    }

    private sealed class FakeHttpService(Func<Uri, string> handler) : IHttpService
    {
        public Task<HttpServiceResult> GetAsync(Uri uri, CancellationToken cancellationToken = default)
        {
            return Task.FromResult(new HttpServiceResult(HttpStatusCode.OK, handler(uri), new Dictionary<string, string[]>(), TimeSpan.Zero));
        }

        public Task<string> GetStringAsync(Uri uri, CancellationToken cancellationToken = default)
        {
            return Task.FromResult(handler(uri));
        }

        public Task<HttpServiceResult> SendAsync(
            Uri uri,
            string method = "GET",
            string? body = null,
            IReadOnlyDictionary<string, string>? headers = null,
            bool ensureSuccess = true,
            HttpRequestPolicy? policy = null,
            CancellationToken cancellationToken = default)
        {
            return GetAsync(uri, cancellationToken);
        }
    }

    private sealed class StubHttpHandler(Func<HttpRequestMessage, HttpResponseMessage> handler) : HttpMessageHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return Task.FromResult(handler(request));
        }
    }
}
