using Bunit;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
/// Tests for (Flow Debugger vertical trace list, Gitea #53).
public class FlowTraceListTests : BunitContext
{
private static readonly DateTimeOffset T0 = new(2026, 11, 9, 12, 0, 0, TimeSpan.Zero);
private static FlowStep Step(FlowStepKind kind, FlowLane lane, string title, int offsetMs,
FlowStepOutcome outcome = FlowStepOutcome.Ok, int? durationMs = 5, IReadOnlyList? logs = null) =>
new(Guid.NewGuid(), null, null, kind, lane, title, null, T0.AddMilliseconds(offsetMs), durationMs, outcome,
null, null, null, null, null, null, null, null, Logs: logs);
private static FlowGraph Graph(params FlowStep[] steps) =>
new(steps, Array.Empty(), Predicted: false, Truncated: false);
private static FlowGraph Sample() => Graph(
Step(FlowStepKind.InboundRequest, FlowLane.Central, "POST /api/M", 0),
Step(FlowStepKind.ScriptRun, FlowLane.Site, "RunIt", 20, FlowStepOutcome.Failed, 80),
Step(FlowStepKind.AttributeWrite, FlowLane.Tags, "Inst.Flag", 30));
/// Every step renders as a row in display order with its lane and offset.
[Fact]
public void Render_ListsStepsInOrderWithLaneColumn()
{
var cut = Render(p => p.Add(c => c.Graph, Sample()));
var rows = cut.FindAll("[data-test='flow-list-row']");
Assert.Equal(3, rows.Count);
Assert.Equal(new[] { "Central", "Site", "Tags" }, rows.Select(r => r.GetAttribute("data-lane")));
Assert.Contains("+20 ms", rows[1].TextContent);
Assert.Contains("RunIt", rows[1].TextContent);
Assert.Contains("Failed", rows[1].TextContent);
Assert.Contains("failed", rows[1].ClassList);
}
/// The lane filter narrows the rows and the count line.
[Fact]
public void LaneFilter_NarrowsRows()
{
var cut = Render(p => p.Add(c => c.Graph, Sample()));
cut.Find("[data-test='flow-list-lane-filter']").Change("Site");
var row = Assert.Single(cut.FindAll("[data-test='flow-list-row']"));
Assert.Equal("Site", row.GetAttribute("data-lane"));
Assert.Contains("1 of 3", cut.Find("[data-test='flow-list-count']").TextContent);
}
/// The failures-only toggle keeps failed and timed-out steps.
[Fact]
public void FailuresOnly_KeepsFailedSteps()
{
var cut = Render(p => p.Add(c => c.Graph, Sample()));
cut.Find("[data-test='flow-list-failed-only']").Change(true);
var row = Assert.Single(cut.FindAll("[data-test='flow-list-row']"));
Assert.Equal("ScriptRun", row.GetAttribute("data-kind"));
}
/// Clicking or pressing Enter on a row raises OnSelect; the selected row is marked.
[Fact]
public void ClickAndEnter_RaiseSelect_AndSelectedRowIsMarked()
{
var graph = Sample();
var picked = new List();
var cut = Render(p => p
.Add(c => c.Graph, graph)
.Add(c => c.SelectedEventId, graph.Steps[2].EventId)
.Add(c => c.OnSelect, s => picked.Add(s)));
cut.FindAll("[data-test='flow-list-row']")[1].Click();
cut.FindAll("[data-test='flow-list-row']")[0].KeyDown(new Microsoft.AspNetCore.Components.Web.KeyboardEventArgs { Key = "Enter" });
Assert.Equal(new[] { FlowStepKind.ScriptRun, FlowStepKind.InboundRequest }, picked.Select(s => s.Kind));
var rows = cut.FindAll("[data-test='flow-list-row']");
Assert.Equal("true", rows[2].GetAttribute("aria-selected"));
Assert.Contains("selected", rows[2].ClassList);
}
/// No graph shows the empty note; log lines show a count.
[Fact]
public void EmptyGraph_ShowsNoSteps_AndLogsAreCounted()
{
Assert.NotNull(Render().Find("[data-test='flow-list-empty']"));
var logs = new[] { new FlowLogLine(Guid.NewGuid(), null, T0, "error", "script", "boom", null, null, null, 1) };
var cut = Render(p => p.Add(c => c.Graph,
Graph(Step(FlowStepKind.ScriptRun, FlowLane.Site, "S", 0, logs: logs))));
Assert.Equal("1", cut.Find("[data-test='flow-list-logs']").TextContent.Trim());
}
}