using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.IO.Compression;
using System.Text.Json;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Api;
using YMhut.Box.Core.Data;
using YMhut.Box.Core.Net;
using YMhut.Box.Core.Tools;

namespace YMhut.Box.Tests;

[TestClass]
public sealed class ToolExecutorTests
{
    [TestMethod]
    public async Task JsonFormatterFormatsInput()
    {
        var module = new ToolCatalog().GetById("json_formatter")!;

        var result = await ToolExecutor.ExecuteAsync(module, "{\"name\":\"YMhut\"}");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "\"name\": \"YMhut\"");
    }

    [TestMethod]
    public async Task NumberBaseSupportsCatalogId()
    {
        var module = new ToolCatalog().GetById("number_base")!;

        var result = await ToolExecutor.ExecuteAsync(module, "255");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "HEX: 0xFF");
    }

    [TestMethod]
    public async Task NumberBaseConverterSupportsPlanId()
    {
        var module = new ToolCatalog().GetById("number_base_converter")!;

        var result = await ToolExecutor.ExecuteAsync(module, "255");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "HEX: 0xFF");
    }

    [TestMethod]
    public async Task UuToolSuiteListsNativeTools()
    {
        var module = new ToolCatalog().GetById("ymhut_uutool_suite")!;

        var result = await ToolExecutor.ExecuteAsync(module, "json");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "YMhut UU Tool Suite");
        StringAssert.Contains(result.Output, "json_formatter");
    }

    [TestMethod]
    public async Task ApiToolUsesApiManagerEndpoint()
    {
        var module = new ToolCatalog().GetById("dns_query")!;
        var manager = new ApiManager(new FakeHttpService("""
            {
              "Status": 0,
              "Question": [{ "name": "example.com.", "type": 1 }],
              "Answer": [{ "name": "example.com.", "type": 1, "TTL": 300, "data": "1.2.3.4" }]
            }
            """));

        var result = await ToolExecutor.ExecuteAsync(module, "example.com", apiManager: manager);

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "Google Public DNS");
        StringAssert.Contains(result.Output, "1.2.3.4");
        Assert.IsNotNull(result.Document);
        Assert.IsTrue(result.Document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Table));
    }

    [TestMethod]
    public async Task QqProfileUsesNewUserInfoSourceAndDisplaysReturnedAvatar()
    {
        var module = new ToolCatalog().GetById("qq_avatar")!;
        var manager = new ApiManager(new FakeHttpService("""
            {
              "qq": "10001",
              "nickname": "Xiao Ming",
              "long_nick": "Good day",
              "avatar_url": "http://q.qlogo.cn/g?b=qq&nk=10001&s=640",
              "age": 25,
              "sex": "male",
              "qid": "xiaoming2024",
              "qq_level": 64,
              "location": "Guangdong Shenzhen",
              "email": "10001@qq.com",
              "is_vip": true,
              "is_years_vip": true,
              "is_svip": false,
              "is_big_club": true,
              "vip_level": 7,
              "reg_time": "2008-03-15T10:30:00Z",
              "last_updated": "2024-08-14T15:45:30Z"
            }
            """));

        var result = await ToolExecutor.ExecuteAsync(module, "10001", apiManager: manager, language: "zh-CN");

        Assert.IsTrue(result.Ok);
        Assert.IsNotNull(result.Document);
        Assert.AreEqual("Uapis QQ User Info", result.Document.SourceName);
        Assert.IsTrue(ApiEndpoints.TryGet("qq_avatar", out var endpoint));
        Assert.IsTrue(endpoint.ShouldHideEndpoint);
        Assert.IsFalse(result.Document.Metadata.ContainsKey("sourceUrl"));
        var avatar = result.Document.Blocks.FirstOrDefault(block => block.Kind == ToolResultBlockKind.Image);
        Assert.IsNotNull(avatar);
        Assert.AreEqual("http://q.qlogo.cn/g?b=qq&nk=10001&s=640", avatar.Uri);
        var pairs = result.Document.Blocks.SelectMany(block => block.Pairs).ToArray();
        Assert.IsTrue(pairs.Any(pair => pair.Value == "64"));
        Assert.IsTrue(pairs.Any(pair => pair.Key.Contains("VIP", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(pair.Value)));
        Assert.IsFalse(result.Output.Contains("uapis.cn/api/v1/social/qq/userinfo", StringComparison.OrdinalIgnoreCase));
    }

    [TestMethod]
    public async Task QrGeneratorOutputsSvg()
    {
        var module = new ToolCatalog().GetById("qr_generator")!;

        var result = await ToolExecutor.ExecuteAsync(module, "YMhut Box");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "<svg");
        Assert.IsNotNull(result.Document);
        Assert.IsTrue(result.Document.Blocks.Any(block =>
            block.Kind == ToolResultBlockKind.Image &&
            block.Metadata is not null &&
            block.Metadata.TryGetValue("contentType", out var contentType) &&
            contentType == "image/svg+xml"));
        Assert.IsTrue(result.Document.Blocks.Any(block =>
            block.Kind == ToolResultBlockKind.Image &&
            block.Metadata is not null &&
            block.Metadata.TryGetValue("pngBase64", out var pngBase64) &&
            Convert.TryFromBase64String(pngBase64, new byte[pngBase64.Length], out _)));
        Assert.AreEqual(ToolResultKind.ImagePreview, result.Document.ResultKind);
        StringAssert.Contains(result.Document.RawText, "<svg");
    }

    [TestMethod]
    public async Task TextDiffMarksAddedAndRemovedLines()
    {
        var module = new ToolCatalog().GetById("text_diff")!;

        var result = await ToolExecutor.ExecuteAsync(module, "alpha\nbeta\n---\nalpha\ngamma");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "- beta");
        StringAssert.Contains(result.Output, "+ gamma");
    }

    [TestMethod]
    public async Task ColorPickerConvertsHexToRgb()
    {
        var module = new ToolCatalog().GetById("color_picker")!;

        var result = await ToolExecutor.ExecuteAsync(module, "#336699");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "RGB: 51, 102, 153");
    }

    [TestMethod]
    public async Task AsciiArtRendersBlockText()
    {
        var module = new ToolCatalog().GetById("ascii_art")!;

        var result = await ToolExecutor.ExecuteAsync(module, "YM");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "#");
    }

    [TestMethod]
    public async Task ProfanitySupportsCustomTerms()
    {
        var module = new ToolCatalog().GetById("profanity_check")!;

        var result = await ToolExecutor.ExecuteAsync(module, "secret\n---\nthis is a secret token");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "Potential matches: secret");
        StringAssert.Contains(result.Output, "******");
    }

    [TestMethod]
    public async Task ArchiveCreatesZipForFile()
    {
        var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(tempRoot);
        try
        {
            var file = Path.Combine(tempRoot, "sample.txt");
            File.WriteAllText(file, "hello");
            var module = new ToolCatalog().GetById("archive_tool")!;

            var result = await ToolExecutor.ExecuteAsync(module, file);

            Assert.IsTrue(result.Ok);
            Assert.HasCount(1, Directory.GetFiles(tempRoot, "*.zip"));
        }
        finally
        {
            Directory.Delete(tempRoot, recursive: true);
        }
    }

    [TestMethod]
    public async Task QqAvatarBuildsStaticUrls()
    {
        var module = new ToolCatalog().GetById("qq_avatar")!;

        var result = await ToolExecutor.ExecuteAsync(module, "10000");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "qlogo.cn");
        Assert.IsNotNull(result.Document);
        Assert.IsTrue(result.Document.Blocks.Any(block =>
            block.Kind == ToolResultBlockKind.Image &&
            block.Metadata is not null &&
            block.Metadata.TryGetValue("displayMode", out var displayMode) &&
            displayMode == "preview"));
        Assert.IsTrue(result.Document.Blocks.Any(block =>
            block.Kind is ToolResultBlockKind.KeyValue or ToolResultBlockKind.Metric &&
            block.Pairs.Any(pair => pair.Key.Equals("QQ", StringComparison.OrdinalIgnoreCase) && pair.Value == "10000")));
    }

    [TestMethod]
    public async Task MarkdownTableNormalizerAlignsColumns()
    {
        var module = new ToolCatalog().GetById("markdown_table_normalizer")!;

        var result = await ToolExecutor.ExecuteAsync(module, "| a | long |\n|---|---|\n| 1 | 2 |");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "| a   | long |");
        StringAssert.Contains(result.Output, "| --- | ---- |");
    }

    [TestMethod]
    public async Task CompressionCodecRoundTripsGzipBase64()
    {
        var module = new ToolCatalog().GetById("compression_codec")!;

        var compressed = await ToolExecutor.ExecuteAsync(module, "gzip compress\nYMhut Box");
        var base64 = compressed.Output
            .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Single(line => line.StartsWith("Base64:", StringComparison.OrdinalIgnoreCase))
            .Replace("Base64:", string.Empty, StringComparison.OrdinalIgnoreCase)
            .Trim();
        var decoded = await ToolExecutor.ExecuteAsync(module, $"gzip decode\n{base64}");

        Assert.IsTrue(compressed.Ok);
        StringAssert.Contains(compressed.Output, "Algorithm: gzip");
        Assert.IsTrue(decoded.Ok);
        Assert.AreEqual("YMhut Box", decoded.Output);
    }

    [TestMethod]
    public async Task TotpGeneratorMatchesRfc6238Sample()
    {
        var module = new ToolCatalog().GetById("totp_generator")!;

        var result = await ToolExecutor.ExecuteAsync(module, "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\ntime=59\ndigits=8");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "Code: 94287082");
        StringAssert.Contains(result.Output, "Counter: 1");
    }

    [TestMethod]
    public async Task ColorContrastCheckerReportsWcagPasses()
    {
        var module = new ToolCatalog().GetById("color_contrast_checker")!;

        var result = await ToolExecutor.ExecuteAsync(module, "#000000 #ffffff");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "Contrast: 21.00:1");
        StringAssert.Contains(result.Output, "AA normal text: Pass");
        StringAssert.Contains(result.Output, "AAA normal text: Pass");
    }

    [TestMethod]
    public async Task MarkdownPreviewBuildsSummaryAndNormalizedText()
    {
        var module = new ToolCatalog().GetById("markdown_preview")!;

        var result = await ToolExecutor.ExecuteAsync(module, "# Title\n\n* item\n\n[YMhut](https://example.com)");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "Headings: 1");
        StringAssert.Contains(result.Output, "Links: 1");
        StringAssert.Contains(result.Output, "Plain preview:");
        StringAssert.Contains(result.Output, "Normalized:");
        StringAssert.Contains(result.Output, "- item");
    }

    [TestMethod]
    public async Task ImageMetadataInspectorFailsForInvalidPath()
    {
        var module = new ToolCatalog().GetById("image_metadata_inspector")!;

        var result = await ToolExecutor.ExecuteAsync(module, @"Z:\missing\not-an-image.png");

        Assert.IsFalse(result.Ok);
        StringAssert.Contains(result.Error, "本地图片路径");
    }

    [TestMethod]
    public async Task UrlRedirectTraceRejectsInvalidUrlWithoutNetwork()
    {
        var module = new ToolCatalog().GetById("url_redirect_trace")!;

        var result = await ToolExecutor.ExecuteAsync(module, "not a url", language: "zh-CN");

        Assert.IsFalse(result.Ok);
        StringAssert.Contains(result.Error, "http 或 https URL");
    }

    [TestMethod]
    public async Task JsonPathHelperListsLeafPaths()
    {
        var module = new ToolCatalog().GetById("json_path_helper")!;

        var result = await ToolExecutor.ExecuteAsync(module, "{\"items\":[{\"name\":\"YMhut\"}]}");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "$.items[0].name = \"YMhut\"");
    }

    [TestMethod]
    public async Task HashManifestBuilderAndVerifierSupportTextRows()
    {
        var builder = new ToolCatalog().GetById("hash_manifest_builder")!;
        var verifier = new ToolCatalog().GetById("hash_manifest_verify")!;

        var manifest = await ToolExecutor.ExecuteAsync(builder, "hello");
        var result = await ToolExecutor.ExecuteAsync(verifier, manifest.Output);

        Assert.IsTrue(manifest.Ok);
        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "OK  hello");
    }

    [TestMethod]
    public async Task RenamePreviewSupportsIndexedNames()
    {
        var module = new ToolCatalog().GetById("indexed_rename_preview")!;

        var result = await ToolExecutor.ExecuteAsync(module, "prefix=photo_\na.jpg\nb.png");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "a.jpg -> photo_001.jpg");
        StringAssert.Contains(result.Output, "b.png -> photo_002.png");
    }

    [TestMethod]
    public async Task ReferenceLookupUsesBundledAuthoritativeData()
    {
        var module = new ToolCatalog().GetById("mime_lookup")!;
        var reference = new ReferenceDataService(AppPaths.ForCurrentUser(
            Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N")),
            FindAssetsRoot()));

        var result = await ToolExecutor.ExecuteAsync(module, "json", referenceDataService: reference);

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "YMhut Reference Data");
        StringAssert.Contains(result.Output, "application/json");
    }

    [TestMethod]
    public async Task PortLookupLocalizesDescriptionsAndHidesSourceUrls()
    {
        var module = new ToolCatalog().GetById("port_lookup")!;
        var reference = new ReferenceDataService(AppPaths.ForCurrentUser(
            Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N")),
            FindAssetsRoot()));

        var result = await ToolExecutor.ExecuteAsync(module, "443", referenceDataService: reference, language: "zh-CN");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "HTTPS 加密网页服务");
        StringAssert.Contains(result.Output, "数据源：YMhut Reference Data / IANA");
        Assert.IsFalse(result.Output.Contains("https://", StringComparison.OrdinalIgnoreCase));
    }

    [TestMethod]
    public async Task SmartSearchUsesOnlySelectedChannel()
    {
        var module = new ToolCatalog().GetById("smart_search")!;

        var result = await ToolExecutor.ExecuteAsync(module, "bing\nYMhut Box", language: "zh-CN");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "搜索渠道：Bing");
        StringAssert.Contains(result.Output, "https://www.bing.com/search");
        Assert.IsFalse(result.Output.Contains("baidu.com", StringComparison.OrdinalIgnoreCase));
    }

    [TestMethod]
    public async Task WeatherPrefersChinaWeatherForKnownChineseCity()
    {
        var module = new ToolCatalog().GetById("weather")!;
        var manager = new ApiManager(new FakeHttpService("""
            {"weatherinfo":{"city":"北京","weather":"晴","temp1":"10℃","temp2":"20℃","ptime":"11:00"}}
            """));

        var result = await ToolExecutor.ExecuteAsync(module, "北京", apiManager: manager, language: "en-US");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, "Data source: China Weather");
        StringAssert.Contains(result.Output, "City: 北京");
        Assert.IsFalse(result.Output.Contains("weather.com.cn", StringComparison.OrdinalIgnoreCase));
    }

    [TestMethod]
    public async Task MarkdownConverterRendersJsonObjectAndLanguageLabels()
    {
        var module = new ToolCatalog().GetById("json_formatter")!;
        var result = await ToolExecutor.ExecuteAsync(module, "{\"name\":\"YMhut\",\"count\":2}");

        var markdown = ToolMarkdownConverter.FromResult(module, result, "en-US");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(markdown.MarkdownText, "## JSON Result");
        StringAssert.Contains(markdown.MarkdownText, "| Field | Value |");
        StringAssert.Contains(markdown.MarkdownText, "```json");
        Assert.IsFalse(markdown.MarkdownText.Contains("字段", StringComparison.Ordinal));
    }

    [TestMethod]
    public async Task MarkdownConverterRendersJsonArrayAsTable()
    {
        var module = new ToolCatalog().GetById("json_formatter")!;
        var result = await ToolExecutor.ExecuteAsync(module, "[{\"name\":\"YMhut\",\"meta\":{\"kind\":\"tool\"}},{\"name\":\"Box\",\"meta\":{\"kind\":\"app\"}}]");

        var markdown = ToolMarkdownConverter.FromResult(module, result, "zh-CN");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(markdown.MarkdownText, "**项目数**: 2");
        StringAssert.Contains(markdown.MarkdownText, "| name | meta |");
        StringAssert.Contains(markdown.MarkdownText, "{\"kind\":\"tool\"}");
    }

    [TestMethod]
    public void MarkdownConverterKeepsInvalidJsonInCodeBlock()
    {
        var module = new ToolCatalog().GetById("json_formatter")!;
        var result = ToolExecutionResult.Success("{not-json");

        var markdown = ToolMarkdownConverter.FromResult(module, result, "zh-CN");

        StringAssert.Contains(markdown.MarkdownText, "JSON 解析失败");
        StringAssert.Contains(markdown.MarkdownText, "```json");
        StringAssert.Contains(markdown.MarkdownText, "{not-json");
    }

    [TestMethod]
    public async Task MarkdownConverterRendersStructuredResultKinds()
    {
        var catalog = new ToolCatalog();

        var diffModule = catalog.GetById("text_diff")!;
        var diff = await ToolExecutor.ExecuteAsync(diffModule, "alpha\n---\nbeta");
        var diffMarkdown = ToolMarkdownConverter.FromResult(diffModule, diff, "zh-CN");
        StringAssert.Contains(diffMarkdown.MarkdownText, "```diff");

        var tableModule = catalog.GetById("markdown_table_normalizer")!;
        var table = await ToolExecutor.ExecuteAsync(tableModule, "| a | b |\n|---|---|\n| 1 | 2 |");
        var tableMarkdown = ToolMarkdownConverter.FromResult(tableModule, table, "zh-CN");
        StringAssert.Contains(tableMarkdown.MarkdownText, "| a | b |");

        var systemModule = catalog.GetById("system_tool")!;
        var system = await ToolExecutor.ExecuteAsync(systemModule, "status");
        var systemMarkdown = ToolMarkdownConverter.FromResult(systemModule, system, "zh-CN");
        StringAssert.Contains(systemMarkdown.MarkdownText, "| 字段 | 值 |");
    }

    [TestMethod]
    public async Task MarkdownConverterPreservesSanitizedReferenceSources()
    {
        var module = new ToolCatalog().GetById("port_lookup")!;
        var reference = new ReferenceDataService(AppPaths.ForCurrentUser(
            Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N")),
            FindAssetsRoot()));
        var result = await ToolExecutor.ExecuteAsync(module, "443", referenceDataService: reference, language: "zh-CN");

        var markdown = ToolMarkdownConverter.FromResult(module, result, "zh-CN");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(markdown.MarkdownText, "YMhut Reference Data / IANA");
        Assert.IsFalse(markdown.MarkdownText.Contains("https://", StringComparison.OrdinalIgnoreCase));
        Assert.IsNotNull(result.Document);
        Assert.IsTrue(result.Document.Blocks.Any(block => block.Kind is ToolResultBlockKind.Metric or ToolResultBlockKind.Table));
        Assert.IsFalse(result.Document.Blocks.All(block => block.Kind == ToolResultBlockKind.Text));
    }

    [TestMethod]
    public void RemoteResultBuilderAddsSanitizedSourceAndCustomBlocks()
    {
        Assert.IsTrue(ApiEndpoints.TryGet("baidu_hot", out var endpoint));
        var response = new ApiResponse(
            endpoint.Id,
            new Uri("https://top.baidu.com/api/board?secret=hidden"),
            true,
            "{}",
            null,
            DateTimeOffset.Parse("2026-05-27T10:00:00+08:00"));
        var output = """
            鏁版嵁婧愶細鐧惧害鐑悳锛堝畼鏂?鏉冨▉婧愶級
            鏉ユ簮璇存槑锛氬凡闅愯棌杩滅▼鍦板潃锛屼粎灞曠ず鑴辨晱鏉ユ簮鍚嶇О銆?            鑾峰彇鏃堕棿锛?026-05-27 10:00:00 +08:00

            1. YMhut Box 鐑偣  鐑害锛?00
               鎽樿鍐呭
            """;

        var document = ToolResultBuilder.FromRemote(endpoint, response, output, "zh-CN");

        Assert.AreEqual("PearAPI DailyHot", document.SourceName);
        Assert.AreEqual(ToolResultBlockKind.RankedList, document.Blocks[0].Kind);
        Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.KeyValue));
        Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.RankedList));
        Assert.AreEqual("source", document.Blocks[^1].Metadata?["displayMode"]);
        Assert.IsFalse(document.Metadata.ContainsKey("sourceUrl"));
        Assert.IsFalse(document.Blocks.Any(block =>
            block.Metadata is not null &&
            block.Metadata.ContainsKey("sourceUrl")));
        Assert.IsFalse(document.Blocks.SelectMany(block => block.Pairs).Any(pair =>
            pair.Key.Contains("鎺ュ彛鍦板潃", StringComparison.OrdinalIgnoreCase) ||
            pair.Key.Contains("Endpoint", StringComparison.OrdinalIgnoreCase)));
        Assert.IsFalse(document.Blocks.SelectMany(block => block.Pairs).Any(pair => pair.Value.Contains("secret=hidden", StringComparison.OrdinalIgnoreCase)));
        Assert.IsFalse(document.RawText.Contains("secret=hidden", StringComparison.OrdinalIgnoreCase));
    }

    [TestMethod]
    public void RemoteFailureBuildsRenderableDocumentInsteadOfEmptyFailure()
    {
        Assert.IsTrue(ApiEndpoints.TryGet("bili_hot", out var endpoint));
        var response = new ApiResponse(
            endpoint.Id,
            new Uri("https://api.bilibili.com/x/web-interface/popular"),
            false,
            "Forbidden",
            "HTTP 403 Forbidden",
            DateTimeOffset.Parse("2026-05-27T10:00:00+08:00"),
            403);

        var result = RemoteToolFormatter.FormatFailure(endpoint, response, string.Empty, "zh-CN");

        Assert.IsTrue(result.Ok);
        Assert.IsNotNull(result.Document);
        Assert.AreEqual("error", result.Document.Status);
        Assert.IsTrue(result.Document.Blocks.Count >= 2);
        StringAssert.Contains(result.Output, "源站拒绝");
        Assert.IsFalse(result.Output.Contains(endpoint.SourceUrl, StringComparison.OrdinalIgnoreCase));
        Assert.IsFalse(result.Output.Contains("接口地址", StringComparison.OrdinalIgnoreCase));
        Assert.IsFalse(result.Output.Contains("Endpoint", StringComparison.OrdinalIgnoreCase));
        Assert.IsTrue(result.Document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Text));
        Assert.AreEqual("source", result.Document.Blocks[^1].Metadata?["displayMode"]);
    }

    [TestMethod]
    public void RemoteRankingFormatterRecoversCardsFromAlternativeJson()
    {
        Assert.IsTrue(ApiEndpoints.TryGet("bili_hot", out var endpoint));
        var response = new ApiResponse(
            endpoint.Id,
            new Uri("https://api.bilibili.com/x/web-interface/popular"),
            true,
            """
            {
              "data": {
                "items": [
                  { "title": "First video", "owner": { "name": "UP" }, "stat": { "view": 123456 } },
                  { "title": "Second video", "stat": { "view": 888 } }
                ]
              }
            }
            """,
            null,
            DateTimeOffset.Parse("2026-05-27T10:00:00+08:00"),
            200);

        var result = RemoteToolFormatter.Format(endpoint, response, string.Empty, "zh-CN");

        Assert.IsTrue(result.Ok);
        Assert.IsNotNull(result.Document);
        var cards = result.Document.Blocks.FirstOrDefault(block => block.Kind == ToolResultBlockKind.RankedList);
        Assert.IsNotNull(cards);
        Assert.AreEqual("1", cards.Items[0].Leading);
        StringAssert.Contains(cards.Items[0].Title, "First video");
        StringAssert.Contains(cards.Items[0].Title + cards.Items[0].Subtitle, "123,456");
    }

    [TestMethod]
    public void RemoteRankingFormatterRecoversCardsFromEmbeddedHtml()
    {
        Assert.IsTrue(ApiEndpoints.TryGet("zhihu_hot", out var endpoint));
        var response = new ApiResponse(
            endpoint.Id,
            new Uri("https://www.zhihu.com/hot"),
            true,
            """
            <html><body>
            <script id="js-initialData" type="application/json">
            {"initialState":{"topstory":{"hotList":[{"target":{"title":"Hot title one"}},{"target":{"title":"Hot title two"}}]}}}
            </script>
            </body></html>
            """,
            null,
            DateTimeOffset.Parse("2026-05-27T10:00:00+08:00"),
            200);

        var result = RemoteToolFormatter.Format(endpoint, response, string.Empty, "zh-CN");

        Assert.IsTrue(result.Ok);
        Assert.IsNotNull(result.Document);
        var cards = result.Document.Blocks.FirstOrDefault(block => block.Kind == ToolResultBlockKind.RankedList);
        Assert.IsNotNull(cards);
        Assert.AreEqual(2, cards.Items.Count);
        Assert.AreEqual("1", cards.Items[0].Leading);
        StringAssert.Contains(cards.Items[0].Title, "Hot title one");
    }

    [TestMethod]
    public void RemoteRankingResultIgnoresSanitizedSourceNoticeInCards()
    {
        Assert.IsTrue(ApiEndpoints.TryGet("hotboard", out var endpoint));
        var response = new ApiResponse(
            endpoint.Id,
            new Uri("https://top.baidu.com/api/board?secret=hidden"),
            true,
            "{}",
            null,
            DateTimeOffset.Parse("2026-05-27T10:00:00+08:00"));
        var output = string.Join(Environment.NewLine, new[]
        {
            "\u6570\u636e\u6e90\uff1aBaidu Hot\uff08official\uff09",
            "\u6765\u6e90\u8bf4\u660e\uff1a\u5df2\u9690\u85cf\u8fdc\u7a0b\u5730\u5740\uff0c\u4ec5\u5c55\u793a\u8131\u654f\u6765\u6e90\u540d\u79f0\u3002",
            "\u83b7\u53d6\u65f6\u95f4\uff1a2026-05-27 10:00:00 +08:00",
            "",
            "1. YMhut Box Hot",
            "   Summary line",
            "2. WinUI Tools",
            "   Another summary"
        });

        var document = ToolResultBuilder.FromRemote(endpoint, response, output, "zh-CN");
        var cards = document.Blocks.FirstOrDefault(block => block.Kind == ToolResultBlockKind.RankedList);

        Assert.IsNotNull(cards);
        Assert.IsGreaterThanOrEqualTo(cards.Items.Count, 2);
        Assert.AreEqual("1", cards.Items[0].Leading);
        StringAssert.Contains(cards.Items[0].Title, "YMhut Box Hot");
        Assert.IsFalse(cards.Items.Any(item =>
            item.Title.Contains("\u5df2\u9690\u85cf\u8fdc\u7a0b\u5730\u5740", StringComparison.OrdinalIgnoreCase) ||
            item.Subtitle.Contains("\u5df2\u9690\u85cf\u8fdc\u7a0b\u5730\u5740", StringComparison.OrdinalIgnoreCase) ||
            item.Title.Contains("remote address is hidden", StringComparison.OrdinalIgnoreCase) ||
            item.Subtitle.Contains("remote address is hidden", StringComparison.OrdinalIgnoreCase)));
        Assert.IsFalse(document.Metadata.ContainsKey("sourceUrl"));
    }

    [TestMethod]
    public void GoldPriceRemoteResultBuildsTrendChart()
    {
        Assert.IsTrue(ApiEndpoints.TryGet("gold_price", out var endpoint));
        var response = new ApiResponse(
            endpoint.Id,
            new Uri("https://prices.lbma.org.uk/json/gold_pm.json"),
            true,
            "{}",
            null,
            DateTimeOffset.Parse("2026-06-14T10:00:00+08:00"));
        var output = string.Join(Environment.NewLine, new[]
        {
            "LBMA Gold PM Fixing",
            "日期：2026-06-12",
            "USD/oz：3362.15",
            "GBP/oz：2491.22",
            "",
            "Date | USD/oz | GBP/oz",
            "2026-06-10 | 3320.1 | 2460.4",
            "2026-06-11 | 3340.8 | 2475.2",
            "2026-06-12 | 3362.15 | 2491.22"
        });

        var document = ToolResultBuilder.FromRemote(endpoint, response, output, "zh-CN");

        Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Metric));
        Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Table));
        var chart = document.Blocks.FirstOrDefault(block => block.Kind == ToolResultBlockKind.LineChart);
        Assert.IsNotNull(chart);
        Assert.IsGreaterThanOrEqualTo(chart.Rows.Count, 4);
        Assert.AreEqual("USD/oz", chart.Metadata?["chartUnit"]);
    }

    [TestMethod]
    public void UpdateNoticeJsonKeepsPlainTextAndAddsMarkdown()
    {
        var repoRoot = Directory.GetParent(FindAssetsRoot())!.FullName;
        var noticePath = Path.Combine(repoRoot, "update-notice", "2.0.6.3.json");
        var totalPath = Path.Combine(repoRoot, "update-notice", "total.json");

        using var notice = JsonDocument.Parse(File.ReadAllText(noticePath));
        var noticeRoot = notice.RootElement;
        Assert.AreEqual("2.0.6.3", noticeRoot.GetProperty("app_version").GetString());
        Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("message").GetString()));
        Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("release_notes").GetString()));
        StringAssert.Contains(noticeRoot.GetProperty("message_md").GetString(), "YMhut Box 2.0.6.3");
        StringAssert.Contains(noticeRoot.GetProperty("release_notes_md").GetString(), "QQ 信息");
        StringAssert.Contains(noticeRoot.GetProperty("release_notes_md").GetString(), "安全浏览器");

        using var total = JsonDocument.Parse(File.ReadAllText(totalPath));
        var totalRoot = total.RootElement;
        Assert.AreEqual("2.0.6.3", totalRoot.GetProperty("latest_version").GetString());
        var latest = totalRoot.GetProperty("latest");
        Assert.AreEqual("2.0.6.3", latest.GetProperty("version").GetString());
        StringAssert.Contains(latest.GetProperty("release_notes_md").GetString(), "QQ 信息");
        Assert.AreEqual("2.0.6.3", totalRoot.GetProperty("versions")[0].GetProperty("version").GetString());
        StringAssert.Contains(totalRoot.GetProperty("versions")[0].GetProperty("summary").GetString(), "QQ 信息");
    }

    [TestMethod]
    public void WeatherRemoteResultBuildsSummaryForecastAndSourceMetadata()
    {
        Assert.IsTrue(ApiEndpoints.TryGet("weather", out var endpoint));
        var response = new ApiResponse(endpoint.Id, new Uri("https://api.open-meteo.com/v1/forecast"), true, "{}", null, DateTimeOffset.Now);
        var output = """
            Data source: Open-Meteo (public trusted source)
            Fetched at: 2026-05-28 10:00:00 +08:00

            Location: Beijing China
            Current temperature: 22.5 掳C
            Apparent temperature: 23 掳C
            Relative humidity: 56%
            Wind speed: 12 km/h

            Next three days:
            2026-05-28: 18 - 27 掳C
            2026-05-29: 19 - 28 掳C
            """;

        var document = ToolResultBuilder.FromRemote(endpoint, response, output, "zh-CN");

        Assert.IsTrue(document.Metadata.TryGetValue("sourceVisibility", out var visibility));
        Assert.AreEqual(ApiSourceVisibility.PublicTrusted.ToString(), visibility);
        Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Metric));
        Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Table));
    }

    [TestMethod]
    public void HistoryRemoteResultBuildsTimelineInsteadOfPlainText()
    {
        Assert.IsTrue(ApiEndpoints.TryGet("history_today", out var endpoint));
        var response = new ApiResponse(endpoint.Id, new Uri("https://api.wikimedia.org/feed/v1/wikipedia/zh/onthisday/all/05/28"), true, "{}", null, DateTimeOffset.Now);
        var output = """
            EVENTS
            - 585: Historical event
            - 1926: Another event

            BIRTHS
            - 1908: Person born
            """;

        var document = ToolResultBuilder.FromRemote(endpoint, response, output, "zh-CN");

        Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Timeline));
        Assert.IsFalse(document.Blocks.All(block => block.Kind == ToolResultBlockKind.Text));
    }

    [TestMethod]
    public async Task LocalToolFamiliesReturnSemanticDocumentBlocks()
    {
        var catalog = new ToolCatalog();

        var hash = await ToolExecutor.ExecuteAsync(catalog.GetById("hash_generator")!, "YMhut");
        Assert.IsTrue(hash.Document!.Blocks.Any(block => block.Kind == ToolResultBlockKind.Table));

        var base64 = await ToolExecutor.ExecuteAsync(catalog.GetById("base64_codec")!, "YMhut");
        Assert.IsTrue(base64.Document!.Blocks.Any(block => block.Kind == ToolResultBlockKind.KeyValue));

        var calculator = await ToolExecutor.ExecuteAsync(catalog.GetById("number_base")!, "255");
        Assert.IsTrue(calculator.Document!.Blocks.Any(block => block.Kind == ToolResultBlockKind.Metric));

        var diff = await ToolExecutor.ExecuteAsync(catalog.GetById("text_diff")!, "a\n---\nb");
        Assert.IsTrue(diff.Document!.Blocks.Any(block => block.Kind == ToolResultBlockKind.Diff));
    }

    [TestMethod]
    public async Task DataSetToolsReadBundledAssets()
    {
        var catalog = new ToolCatalog();
        var reference = new ReferenceDataService(AppPaths.ForCurrentUser(
            Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N")),
            FindAssetsRoot()));

        var car = await ToolExecutor.ExecuteAsync(catalog.GetById("car_info")!, "京A12345", referenceDataService: reference);
        var skin = await ToolExecutor.ExecuteAsync(catalog.GetById("sanguosha_skin")!, "刘备", referenceDataService: reference);

        Assert.IsTrue(car.Ok);
        Assert.IsTrue(skin.Ok);
        StringAssert.Contains(car.Output, "北京");
        StringAssert.Contains(skin.Output, "刘备");
    }

    [TestMethod]
    public async Task ReferenceDataServiceReadsDatPackageWhenJsonIsPacked()
    {
        var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
        var assetsRoot = Path.Combine(tempRoot, "Assets");
        var dataRoot = Path.Combine(assetsRoot, "data");
        Directory.CreateDirectory(dataRoot);
        var datPath = Path.Combine(dataRoot, "reference-data.dat");

        using (var archive = ZipFile.Open(datPath, ZipArchiveMode.Create))
        {
            var entry = archive.CreateEntry("data/reference/ymhut_reference_data.json");
            await using var stream = entry.Open();
            await using var writer = new StreamWriter(stream);
            await writer.WriteAsync("{\"source\":\"packed\"}");
        }

        try
        {
            var service = new ReferenceDataService(AppPaths.ForCurrentUser(tempRoot, assetsRoot));

            var text = await service.ReadTextAsync("data/reference/ymhut_reference_data.json");

            Assert.IsTrue(service.Exists("data/reference/ymhut_reference_data.json"));
            Assert.AreEqual("{\"source\":\"packed\"}", text);
        }
        finally
        {
            Directory.Delete(tempRoot, recursive: true);
        }
    }

    [TestMethod]
    public async Task SystemToolProvidesNativeStatus()
    {
        var module = new ToolCatalog().GetById("system_tool")!;

        var result = await ToolExecutor.ExecuteAsync(module, "status");

        Assert.IsTrue(result.Ok);
        StringAssert.Contains(result.Output, ".NET");
        StringAssert.Contains(result.Output, "calculator");
    }

    [TestMethod]
    public void OfficialSourceHidesPublicSourceUrlInDocumentMetadata()
    {
        var endpoint = new ApiEndpoint(
            "dns_query",
            "DNS",
            "https://dns.google/resolve?name={value}",
            "Google Public DNS",
            "https://developers.google.com/speed/public-dns/docs/doh/json");
        var response = new ApiResponse(endpoint.Id, new Uri("https://dns.google/resolve?name=example.com"), true, "Status: 0", null, DateTimeOffset.Now);

        var document = ToolResultBuilder.FromRemote(endpoint, response, "Status: 0", "zh-CN");

        Assert.IsFalse(document.Metadata.ContainsKey("sourceUrl"));
        Assert.IsFalse(document.Blocks.Any(block =>
            block.Metadata is not null &&
            block.Metadata.ContainsKey("sourceUrl")));
        Assert.IsTrue(document.Blocks.Any(block =>
            block.Metadata is not null &&
            block.Metadata.TryGetValue("sourceVisibility", out var visibility) &&
            visibility == ApiSourceVisibility.PublicOfficial.ToString()));
    }

    [TestMethod]
    public void SensitivePrivateSourceHidesEndpointInDocumentMetadata()
    {
        var endpoint = new ApiEndpoint(
            "media_types",
            "Media config",
            "https://update.ymhut.cn/media-types.json",
            "YMhut remote config",
            "https://update.ymhut.cn/media-types.json",
            false,
            ApiSourceVisibility.SensitivePrivate);
        var response = new ApiResponse(endpoint.Id, new Uri(endpoint.SourceUrl), true, "{}", null, DateTimeOffset.Now);

        var document = ToolResultBuilder.FromRemote(endpoint, response, "{}", "zh-CN");

        Assert.IsFalse(document.Metadata.Values.Any(value => value.Contains("update.ymhut.cn", StringComparison.OrdinalIgnoreCase)));
        Assert.IsTrue(document.Blocks.Any(block =>
            block.Metadata is not null &&
            block.Metadata.TryGetValue("sourceSensitivity", out var sensitivity) &&
            sensitivity == "sensitive"));
        Assert.IsFalse(document.Blocks.Any(block =>
            block.Metadata is not null &&
            block.Metadata.Values.Any(value => value.Contains("update.ymhut.cn", StringComparison.OrdinalIgnoreCase))));
    }

    [TestMethod]
    public void HttpDiagnosticHidesDynamicTargetAsEndpoint()
    {
        Assert.IsTrue(ApiEndpoints.TryGet("http_diagnostic", out var endpoint));
        var response = new ApiResponse(endpoint.Id, new Uri("https://example.com/private/path?token=secret"), true, "<title>Example</title>", null, DateTimeOffset.Now);

        var document = ToolResultBuilder.FromRemote(endpoint, response, "Example\n1. Status page", "zh-CN");

        Assert.IsTrue(endpoint.ShouldHideEndpoint);
        Assert.IsFalse(document.Metadata.ContainsKey("sourceUrl"));
        Assert.IsFalse(document.Blocks.Any(block =>
            block.Metadata is not null &&
            block.Metadata.TryGetValue("sourceUrl", out var sourceUrl) &&
            sourceUrl.Contains("example.com", StringComparison.OrdinalIgnoreCase)));
    }

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

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

        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 static string FindAssetsRoot()
    {
        var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
        while (directory is not null)
        {
            var candidate = Path.Combine(directory.FullName, "assets");
            if (File.Exists(Path.Combine(candidate, "data", "reference", "ymhut_reference_data.json")))
            {
                return candidate;
            }

            directory = directory.Parent;
        }

        throw new DirectoryNotFoundException("Unable to locate repository assets directory.");
    }
}

