using System.Collections.Generic;
using FluentAssertions;
using Scripting.Application.DTOs;
using Scripting.Application.Services;
using Xunit;
namespace BuildFlow.Scripting.Tests;
///
/// 单元测试:反射扫描、关键字/Kind 过滤、分页与类型格式化。
/// 扫描依赖测试运行时加载的 BuildFlow.* 程序集;本测试项目通过 Scripting.Application
/// 传递引用了 Platform.Application,其 Query 类型应被扫描到。
///
public class ScriptTypeRegistryTests
{
private readonly ScriptTypeRegistry _registry = new();
public ScriptTypeRegistryTests()
{
// 测试构造时扫描一次,保证后续断言基于已填充状态
_registry.Scan();
}
/// 扫描应至少发现一个 BuildFlow.* IBaseRequest 实现(来自 Platform.Application)。
[Fact]
public void Scan_Populates_Known_MediatR_Types()
{
var all = _registry.GetAll();
all.Should().NotBeEmpty("Platform.Application 含大量 IRequest 实现,扫描后应非空");
// 抽样:应能找到带 Query/Command 后缀的类型(启发式分类的输入)
all.Should().Contain(m => m.Kind == "Query" || m.Kind == "Command");
}
/// 关键字搜索应按 名/全名/命名空间 模糊过滤。
[Fact]
public void Search_By_Keyword_Filters()
{
var all = _registry.GetAll();
all.Should().NotBeEmpty();
// 任取一个类型的名字片段作为关键字,搜索结果必须全部命中该关键字
var keyword = PickProbe(all);
var results = _registry.Search(keyword, kind: null, pageIndex: 1, pageSize: 100).ToList();
results.Should().NotBeEmpty($"关键字 '{keyword}' 来源于已扫描类型");
foreach (var m in results)
{
var hit = m.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase)
|| m.FullName.Contains(keyword, StringComparison.OrdinalIgnoreCase)
|| m.Namespace.Contains(keyword, StringComparison.OrdinalIgnoreCase);
hit.Should().BeTrue($"结果 '{m.FullName}' 应匹配关键字 '{keyword}'");
}
}
/// Kind=Query 过滤后,结果应全部是 Query;Total 计数应与分页前一致。
[Fact]
public void Search_By_Kind_Query_Filters()
{
if (_registry.GetAll().Count == 0)
{
// 没有类型可过滤时跳过(不应失败)
return;
}
var queryTotal = _registry.Total(keyword: null, kind: "Query");
var queryPage = _registry.Search(keyword: null, kind: "Query", pageIndex: 1, pageSize: 1000).ToList();
queryPage.Count.Should().Be(queryTotal, "Total 与 Search(不限页大小)计数应一致");
queryPage.Should().AllSatisfy(m => m.Kind.Should().Be("Query"));
queryTotal.Should().BeGreaterThan(0, "Platform.Application 至少含一个 Query");
}
/// 分页:第二页应正确跳过第一页条数。
[Fact]
public void Search_Paginates()
{
var total = _registry.Total(keyword: null, kind: null);
if (total < 2) return;
var firstPageFirst = _registry.Search(null, null, pageIndex: 1, pageSize: 1).First();
var secondPageFirst = _registry.Search(null, null, pageIndex: 2, pageSize: 1).First();
firstPageFirst.FullName.Should().NotBe(secondPageFirst.FullName, "翻页后首页首项应不同于第二页首项");
}
/// ResponseType 字段:IRequest<T> 实现应能解析出 T 的友好名。
[Fact]
public void BuildMeta_Extracts_Response_Type()
{
var all = _registry.GetAll();
all.Should().Contain(m => m.ResponseType != null,
"至少一个类型实现了 IRequest,应解析出非空 ResponseType");
}
/// 重复扫描是幂等的:再次扫描后总数不变。
[Fact]
public void Scan_Is_Idempotent()
{
var before = _registry.GetAll().Count;
_registry.Scan();
var after = _registry.GetAll().Count;
after.Should().Be(before, "重复扫描不应改变类型集合");
}
// -------- FormatTypeName(internal static,经 InternalsVisibleTo 暴露) --------
[Theory]
[InlineData(typeof(int), "int")]
[InlineData(typeof(string), "string")]
[InlineData(typeof(bool), "bool")]
[InlineData(typeof(long), "long")]
[InlineData(typeof(decimal), "decimal")]
[InlineData(typeof(double), "double")]
[InlineData(typeof(Guid), "Guid")]
[InlineData(typeof(DateTime), "DateTime")]
[InlineData(typeof(object), "object")]
public void FormatTypeName_Primitives_Use_CSharp_Alias(Type type, string expected)
{
ScriptTypeRegistry.FormatTypeName(type).Should().Be(expected);
}
[Fact]
public void FormatTypeName_Null_Returns_Question_Mark()
{
ScriptTypeRegistry.FormatTypeName(null).Should().Be("?");
}
[Fact]
public void FormatTypeName_Nullable_Primitive_Appends_Question_Mark()
{
ScriptTypeRegistry.FormatTypeName(typeof(int?)).Should().Be("int?");
ScriptTypeRegistry.FormatTypeName(typeof(Guid?)).Should().Be("Guid?");
ScriptTypeRegistry.FormatTypeName(typeof(DateTime?)).Should().Be("DateTime?");
}
[Fact]
public void FormatTypeName_Generic_Collection_Formats_With_Angle_Brackets()
{
ScriptTypeRegistry.FormatTypeName(typeof(List)).Should().Be("List");
ScriptTypeRegistry.FormatTypeName(typeof(IEnumerable)).Should().Be("IEnumerable");
ScriptTypeRegistry.FormatTypeName(typeof(IReadOnlyList)).Should().Be("IReadOnlyList");
}
[Fact]
public void FormatTypeName_Dictionary_Formats_Two_Type_Arguments()
{
ScriptTypeRegistry.FormatTypeName(typeof(Dictionary))
.Should().Be("Dictionary");
}
[Fact]
public void FormatTypeName_Nested_Generic_Formats_Recursively()
{
ScriptTypeRegistry.FormatTypeName(typeof(List>))
.Should().Be("List>");
}
[Fact]
public void FormatTypeName_Array_Appends_Brackets()
{
ScriptTypeRegistry.FormatTypeName(typeof(int[])).Should().Be("int[]");
ScriptTypeRegistry.FormatTypeName(typeof(string[])).Should().Be("string[]");
ScriptTypeRegistry.FormatTypeName(typeof(List[])).Should().Be("List[]");
}
[Fact]
public void FormatTypeName_Complex_Type_Uses_FullName()
{
ScriptTypeRegistry.FormatTypeName(typeof(System.Text.Json.JsonElement))
.Should().Be("System.Text.Json.JsonElement");
}
// -------- 辅助 --------
/// 从已扫描集合中挑一个适合做关键字探测的样本(取名字的子串,避免取到单字符)。
private static string PickProbe(IReadOnlyCollection all)
{
// 选一个名字至少 4 字符的类型,截取前 4 字符作为关键字,保证可复现命中
var pick = all.First(m => m.Name.Length >= 4);
return pick.Name[..4];
}
}