using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ScadaLink.CentralUI.Components.Audit;
using ScadaLink.CentralUI.Services;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Types.Audit;
using ScadaLink.Commons.Types.Enums;
namespace ScadaLink.CentralUI.Tests.Components.Audit;
///
/// bUnit tests for (#23 M7-T3 / Bundle B). The grid
/// renders 10 columns, paginates via keyset (passing the last row's
/// (OccurredAtUtc, EventId) back to the service), raises a row-click callback
/// that Bundle C wires to the drilldown drawer, and styles non-success status
/// rows with an error-coded badge.
///
public class AuditResultsGridTests : BunitContext
{
private readonly IAuditLogQueryService _service;
private readonly List<(AuditLogQueryFilter Filter, AuditLogPaging? Paging)> _calls = new();
private static AuditEvent MakeEvent(DateTime occurredAtUtc, AuditStatus status, AuditChannel channel = AuditChannel.ApiOutbound, AuditKind kind = AuditKind.ApiCall, string? site = "plant-a")
=> new()
{
EventId = Guid.NewGuid(),
OccurredAtUtc = occurredAtUtc,
Channel = channel,
Kind = kind,
Status = status,
SourceSiteId = site,
Target = "demo-target",
Actor = "tester",
DurationMs = 42,
HttpStatus = status == AuditStatus.Delivered ? 200 : 500,
ErrorMessage = status == AuditStatus.Failed ? "boom — unreachable" : null,
};
public AuditResultsGridTests()
{
_service = Substitute.For();
_service.DefaultPageSize.Returns(100);
Services.AddSingleton(_service);
}
private void StubPage(IReadOnlyList rows)
{
_service.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any())
.Returns(callInfo =>
{
_calls.Add(((AuditLogQueryFilter)callInfo[0], (AuditLogPaging?)callInfo[1]));
return Task.FromResult(rows);
});
}
[Fact]
public void Render_TenColumns_FromStubService()
{
StubPage(new List
{
MakeEvent(DateTime.UtcNow.AddMinutes(-1), AuditStatus.Delivered),
});
var cut = Render(p => p.Add(c => c.Filter, new AuditLogQueryFilter()));
// 10 column headers per Component-AuditLog.md §10.
var expectedHeaders = new[]
{
"OccurredAtUtc", "Site", "Channel", "Kind", "Status",
"Target", "Actor", "DurationMs", "HttpStatus", "ErrorMessage",
};
foreach (var header in expectedHeaders)
{
Assert.Contains($"data-test=\"col-header-{header}\"", cut.Markup);
}
}
[Fact]
public void Click_NextPage_CallsService_WithCursor_OfLastRow()
{
// First page: two rows, descending by OccurredAtUtc. The grid must pass the
// LAST row (the older one) back as the keyset cursor for the next page.
var first = MakeEvent(new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc), AuditStatus.Delivered);
var second = MakeEvent(new DateTime(2026, 5, 20, 11, 30, 0, DateTimeKind.Utc), AuditStatus.Failed);
StubPage(new[] { first, second });
var cut = Render(p => p.Add(c => c.Filter, new AuditLogQueryFilter()));
cut.Find("[data-test=\"grid-next-page\"]").Click();
// Two service calls: initial + next.
Assert.Equal(2, _calls.Count);
var nextCall = _calls[1];
Assert.NotNull(nextCall.Paging);
Assert.Equal(second.OccurredAtUtc, nextCall.Paging!.AfterOccurredAtUtc);
Assert.Equal(second.EventId, nextCall.Paging.AfterEventId);
}
[Fact]
public void Click_Row_RaisesOnRowSelected()
{
var target = MakeEvent(DateTime.UtcNow.AddMinutes(-5), AuditStatus.Delivered);
StubPage(new[] { target });
AuditEvent? captured = null;
var cut = Render(p => p
.Add(c => c.Filter, new AuditLogQueryFilter())
.Add(c => c.OnRowSelected, EventCallback.Factory.Create(this, e => captured = e)));
cut.Find($"[data-test=\"grid-row-{target.EventId}\"]").Click();
Assert.NotNull(captured);
Assert.Equal(target.EventId, captured!.EventId);
}
[Fact]
public void Status_FailedRow_HasErrorBadgeClass()
{
var failed = MakeEvent(DateTime.UtcNow.AddMinutes(-2), AuditStatus.Failed);
var delivered = MakeEvent(DateTime.UtcNow.AddMinutes(-1), AuditStatus.Delivered);
StubPage(new[] { delivered, failed });
var cut = Render(p => p.Add(c => c.Filter, new AuditLogQueryFilter()));
// Failed badge => bg-danger (red). Delivered => bg-success (green).
var failedBadge = cut.Find($"[data-test=\"status-badge-{failed.EventId}\"]");
Assert.Contains("bg-danger", failedBadge.GetAttribute("class") ?? string.Empty);
var deliveredBadge = cut.Find($"[data-test=\"status-badge-{delivered.EventId}\"]");
Assert.Contains("bg-success", deliveredBadge.GetAttribute("class") ?? string.Empty);
}
}