using Bunit;
using Bunit.TestDoubles;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using ScadaLink.CentralUI.Components.Audit;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Types.Enums;
namespace ScadaLink.CentralUI.Tests.Components.Audit;
///
/// bUnit tests for — the reusable single-row
/// detail body extracted from (Task 1 of the
/// Execution-Tree Node Detail Modal feature).
///
/// These tests render the detail component directly (not via the drawer) and
/// pin the contract the drawer — and any future modal host — relies on:
/// the read-only field block, the conditional Error/Request/Response/Extra
/// sections, the redaction badge, channel-aware body rendering, and the
/// action buttons. All data-test values must match the originals so the
/// existing selectors keep resolving.
///
public class AuditEventDetailTests : BunitContext
{
public AuditEventDetailTests()
{
// Loose so the cURL clipboard call does not blow up tests that do not
// exercise it. The clipboard test flips to Strict itself.
JSInterop.Mode = JSRuntimeMode.Loose;
}
private static AuditEvent MakeEvent(
AuditChannel channel = AuditChannel.ApiOutbound,
AuditKind kind = AuditKind.ApiCall,
AuditStatus status = AuditStatus.Delivered,
string? requestSummary = null,
string? responseSummary = null,
string? extra = null,
Guid? correlationId = null,
Guid? executionId = null,
Guid? parentExecutionId = null,
string? errorMessage = null,
string? errorDetail = null,
string? target = "demo-target")
=> new()
{
EventId = Guid.Parse("11111111-2222-3333-4444-555555555555"),
OccurredAtUtc = new DateTime(2026, 5, 20, 12, 30, 45, DateTimeKind.Utc),
IngestedAtUtc = new DateTime(2026, 5, 20, 12, 30, 46, DateTimeKind.Utc),
Channel = channel,
Kind = kind,
CorrelationId = correlationId,
ExecutionId = executionId,
ParentExecutionId = parentExecutionId,
SourceSiteId = "plant-a",
SourceInstanceId = "boiler-3",
SourceScript = "OnAlarm.csx",
Actor = "tester",
Target = target,
Status = status,
HttpStatus = status == AuditStatus.Delivered ? 200 : 500,
DurationMs = 42,
ErrorMessage = errorMessage,
ErrorDetail = errorDetail,
RequestSummary = requestSummary,
ResponseSummary = responseSummary,
Extra = extra,
};
[Fact]
public void RendersFieldBlock()
{
var ev = MakeEvent();
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.Contains("data-test=\"drawer-fields\"", cut.Markup);
Assert.Contains("data-test=\"field-Channel\"", cut.Markup);
Assert.Contains("data-test=\"field-Status\"", cut.Markup);
Assert.Contains("data-test=\"field-OccurredAtUtc\"", cut.Markup);
Assert.Contains("2026-05-20T12:30:45", cut.Markup);
}
[Fact]
public void ErrorSection_RendersWhenErrorPresent()
{
var ev = MakeEvent(
status: AuditStatus.Parked,
errorMessage: "boom",
errorDetail: "stack trace here");
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.Contains("data-test=\"section-error\"", cut.Markup);
Assert.Contains("boom", cut.Markup);
Assert.Contains("stack trace here", cut.Markup);
}
[Fact]
public void ErrorSection_HiddenWhenNoError()
{
var ev = MakeEvent();
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.DoesNotContain("data-test=\"section-error\"", cut.Markup);
}
[Fact]
public void RequestSection_PrettyPrintsJson()
{
var ev = MakeEvent(requestSummary: "{\"a\":1,\"b\":\"two\"}");
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.Contains("data-test=\"section-request\"", cut.Markup);
Assert.Contains("data-test=\"request-body\"", cut.Markup);
Assert.Matches(@"\n\s+""a"":\s*1", cut.Markup);
}
[Fact]
public void ResponseSection_RendersWhenPresent()
{
var ev = MakeEvent(responseSummary: "{\"ok\":true}");
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.Contains("data-test=\"section-response\"", cut.Markup);
Assert.Contains("data-test=\"response-body\"", cut.Markup);
}
[Fact]
public void ExtraSection_RendersWhenPresent()
{
var ev = MakeEvent(extra: "{\"note\":\"hi\"}");
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.Contains("data-test=\"section-extra\"", cut.Markup);
}
[Fact]
public void RedactedBody_ShowsRedactionBadge()
{
var ev = MakeEvent(requestSummary: "{\"headers\":{\"Authorization\":\"\"},\"body\":\"hello\"}");
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.Contains("data-test=\"redaction-badge-request\"", cut.Markup);
}
[Fact]
public void NonRedactedBody_HidesRedactionBadge()
{
var ev = MakeEvent(requestSummary: "{\"headers\":{\"Authorization\":\"Bearer abc\"},\"body\":\"hello\"}");
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.DoesNotContain("data-test=\"redaction-badge-request\"", cut.Markup);
}
[Fact]
public void DbOutboundChannel_RendersSqlBlock()
{
const string body = "{\"sql\":\"UPDATE T SET x=@p1 WHERE id=@p2\",\"parameters\":{\"p1\":42,\"p2\":\"abc\"}}";
var ev = MakeEvent(channel: AuditChannel.DbOutbound, kind: AuditKind.DbWrite, requestSummary: body);
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.Contains("language-sql", cut.Markup);
Assert.Contains("UPDATE T SET x=@p1 WHERE id=@p2", cut.Markup);
Assert.Contains("data-test=\"sql-parameters\"", cut.Markup);
}
[Fact]
public void ApiChannel_ShowsCopyAsCurlButton()
{
var ev = MakeEvent(channel: AuditChannel.ApiOutbound);
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.Contains("data-test=\"copy-as-curl\"", cut.Markup);
}
[Fact]
public void NonApiChannel_HidesCopyAsCurlButton()
{
var ev = MakeEvent(channel: AuditChannel.Notification, kind: AuditKind.NotifySend);
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.DoesNotContain("data-test=\"copy-as-curl\"", cut.Markup);
}
[Fact]
public void NullCorrelationId_HidesShowAllButton()
{
var ev = MakeEvent(correlationId: null);
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.DoesNotContain("data-test=\"show-all-events\"", cut.Markup);
}
[Fact]
public void NonNullCorrelationId_ShowsShowAllButton()
{
var ev = MakeEvent(correlationId: Guid.NewGuid());
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.Contains("data-test=\"show-all-events\"", cut.Markup);
}
[Fact]
public void ExecutionButtons_ConditionalOnExecutionIds()
{
var ev = MakeEvent(
executionId: Guid.Parse("aaaaaaaa-1111-2222-3333-444444444444"),
parentExecutionId: Guid.Parse("bbbbbbbb-1111-2222-3333-444444444444"));
var cut = Render(p => p.Add(c => c.Event, ev));
Assert.Contains("data-test=\"view-this-execution\"", cut.Markup);
Assert.Contains("data-test=\"view-parent-execution\"", cut.Markup);
Assert.Contains("data-test=\"view-execution-chain\"", cut.Markup);
}
[Fact]
public void ShowAllForOperation_Navigates_WithCorrelationIdQueryString()
{
var corr = Guid.Parse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
var ev = MakeEvent(correlationId: corr);
var cut = Render(p => p.Add(c => c.Event, ev));
cut.Find("[data-test=\"show-all-events\"]").Click();
var nav = (BunitNavigationManager)Services.GetRequiredService();
Assert.Contains($"/audit/log?correlationId={corr}", nav.Uri);
}
[Fact]
public void ViewExecutionChain_Navigates_ToExecutionTreePage()
{
var exec = Guid.Parse("12345678-aaaa-bbbb-cccc-1234567890ab");
var ev = MakeEvent(executionId: exec);
var cut = Render(p => p.Add(c => c.Event, ev));
cut.Find("[data-test=\"view-execution-chain\"]").Click();
var nav = (BunitNavigationManager)Services.GetRequiredService();
Assert.Contains($"/audit/execution-tree?executionId={exec}", nav.Uri);
}
[Fact]
public async Task CopyAsCurl_InvokesClipboard_WithCurlString()
{
JSInterop.Mode = JSRuntimeMode.Strict;
var clipboardCall = JSInterop.SetupVoid(
"navigator.clipboard.writeText",
invocation => invocation.Arguments.Count == 1
&& invocation.Arguments[0] is string s
&& s.StartsWith("curl ", StringComparison.Ordinal));
var ev = MakeEvent(
channel: AuditChannel.ApiOutbound,
target: "https://example.test/api/v1/widgets",
requestSummary: "{\"headers\":{\"Content-Type\":\"application/json\"},\"body\":\"{\\\"x\\\":1}\"}");
var cut = Render(p => p.Add(c => c.Event, ev));
await cut.InvokeAsync(() => cut.Find("[data-test=\"copy-as-curl\"]").Click());
var calls = clipboardCall.Invocations.ToList();
Assert.NotEmpty(calls);
var argString = (string)calls[0].Arguments[0]!;
Assert.StartsWith("curl ", argString);
Assert.Contains("https://example.test/api/v1/widgets", argString);
}
}