using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Audit;

/// <summary>
/// bUnit tests for <see cref="ExecutionDetailModal"/> (Execution-Tree Node Detail
/// Modal, Task 3). The modal opens on an execution-tree node double-click: given
/// an <c>ExecutionId</c> it loads that execution's audit rows via
/// <see cref="IAuditLogQueryService"/> and shows a list → per-row detail.
///
/// Tests pin the behaviours the spec cannot lose: load-on-open-transition,
/// the four data states (multi-row list, single-row straight-to-detail,
/// zero-row empty, query-failure error), and that closing raises OnClose.
/// </summary>
public class ExecutionDetailModalTests : BunitContext
{
    private readonly IAuditLogQueryService _service;
    private readonly List<(AuditLogQueryFilter Filter, AuditLogPaging? Paging)> _calls = new();

    public ExecutionDetailModalTests()
    {
        _service = Substitute.For<IAuditLogQueryService>();
        _service.DefaultPageSize.Returns(100);
        Services.AddSingleton(_service);

        // AuditEventDetail (the per-row detail body) owns a clipboard interop
        // call. Loose mode lets that no-op for tests that don't exercise it.
        JSInterop.Mode = JSRuntimeMode.Loose;
    }

    private static AuditEventView MakeEvent(
        Guid executionId,
        AuditStatus status = AuditStatus.Delivered,
        AuditChannel channel = AuditChannel.ApiOutbound,
        AuditKind kind = AuditKind.ApiCall,
        string? target = "demo-target")
        => new()
        {
            EventId = Guid.NewGuid(),
            OccurredAtUtc = new DateTime(2026, 5, 20, 12, 30, 45, DateTimeKind.Utc),
            Channel = channel,
            Kind = kind,
            Status = status,
            ExecutionId = executionId,
            SourceSiteId = "plant-a",
            Target = target,
            Actor = "tester",
            DurationMs = 42,
            HttpStatus = status == AuditStatus.Delivered ? 200 : 500,
        };

    private void StubRows(IReadOnlyList<AuditEventView> rows)
    {
        _service.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
            .Returns(callInfo =>
            {
                _calls.Add(((AuditLogQueryFilter)callInfo[0], (AuditLogPaging?)callInfo[1]));
                return Task.FromResult(rows);
            });
    }

    [Fact]
    public void ClosedModal_RendersNothing_AndDoesNotQuery()
    {
        StubRows(new[] { MakeEvent(Guid.NewGuid()) });

        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, Guid.NewGuid())
            .Add(c => c.IsOpen, false));

        Assert.Empty(cut.FindAll("[data-test=\"execution-detail-modal\"]"));
        Assert.Empty(_calls);
    }

    [Fact]
    public void OpenTransition_QueriesByExecutionId_WithPageSize100()
    {
        var executionId = Guid.Parse("11111111-2222-3333-4444-555555555555");
        StubRows(new[] { MakeEvent(executionId), MakeEvent(executionId) });

        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, executionId)
            .Add(c => c.IsOpen, false));

        // Closed on first render — no query yet.
        Assert.Empty(_calls);

        // Flip open: the modal loads exactly once for the open transition.
        cut.Render(p => p.Add(c => c.IsOpen, true));

        Assert.Single(_calls);
        Assert.Equal(executionId, _calls[0].Filter.ExecutionId);
        Assert.NotNull(_calls[0].Paging);
        Assert.Equal(100, _calls[0].Paging!.PageSize);
    }

    [Fact]
    public void StillOpen_NonOpenParameterChange_DoesNotRequery()
    {
        var executionId = Guid.NewGuid();
        StubRows(new[] { MakeEvent(executionId), MakeEvent(executionId) });

        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, executionId)
            .Add(c => c.IsOpen, true));

        Assert.Single(_calls);

        // A parameter set that does NOT flip IsOpen must not re-query.
        cut.Render(p => p.Add(c => c.IsOpen, true));

        Assert.Single(_calls);
    }

    [Fact]
    public void MultiRow_RendersListView_WithOneButtonPerRow()
    {
        var executionId = Guid.NewGuid();
        var rowA = MakeEvent(executionId, AuditStatus.Delivered);
        var rowB = MakeEvent(executionId, AuditStatus.Failed);
        StubRows(new[] { rowA, rowB });

        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, executionId)
            .Add(c => c.IsOpen, true));

        // List view: a row button per audit row, keyed by EventId.
        Assert.NotNull(cut.Find($"[data-test=\"execution-detail-row-{rowA.EventId}\"]"));
        Assert.NotNull(cut.Find($"[data-test=\"execution-detail-row-{rowB.EventId}\"]"));
        // Not in detail view yet — no shared detail body rendered.
        Assert.Empty(cut.FindAll("[data-test=\"drawer-fields\"]"));
    }

    [Fact]
    public void MultiRow_ClickRow_ShowsAuditEventDetail()
    {
        var executionId = Guid.NewGuid();
        var rowA = MakeEvent(executionId, AuditStatus.Delivered);
        var rowB = MakeEvent(executionId, AuditStatus.Failed);
        StubRows(new[] { rowA, rowB });

        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, executionId)
            .Add(c => c.IsOpen, true));

        cut.Find($"[data-test=\"execution-detail-row-{rowB.EventId}\"]").Click();

        // The shared AuditEventDetail body is now rendered (its field list).
        Assert.NotNull(cut.Find("[data-test=\"drawer-fields\"]"));
        // And a Back control to return to the list.
        Assert.NotNull(cut.Find("[data-test=\"execution-detail-back\"]"));
    }

    [Fact]
    public void MultiRow_BackControl_ReturnsToList()
    {
        var executionId = Guid.NewGuid();
        var rowA = MakeEvent(executionId, AuditStatus.Delivered);
        var rowB = MakeEvent(executionId, AuditStatus.Failed);
        StubRows(new[] { rowA, rowB });

        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, executionId)
            .Add(c => c.IsOpen, true));

        cut.Find($"[data-test=\"execution-detail-row-{rowA.EventId}\"]").Click();
        Assert.NotNull(cut.Find("[data-test=\"drawer-fields\"]"));

        cut.Find("[data-test=\"execution-detail-back\"]").Click();

        // Back in the list view: row buttons present, detail body gone.
        Assert.NotNull(cut.Find($"[data-test=\"execution-detail-row-{rowA.EventId}\"]"));
        Assert.Empty(cut.FindAll("[data-test=\"drawer-fields\"]"));
    }

    [Fact]
    public void SingleRow_OpensStraightToDetail_NoBackControl()
    {
        var executionId = Guid.NewGuid();
        var only = MakeEvent(executionId);
        StubRows(new[] { only });

        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, executionId)
            .Add(c => c.IsOpen, true));

        // Straight to detail — the shared body is rendered without a click.
        Assert.NotNull(cut.Find("[data-test=\"drawer-fields\"]"));
        // Nothing to go back to: the Back control is hidden for a single row.
        Assert.Empty(cut.FindAll("[data-test=\"execution-detail-back\"]"));
    }

    [Fact]
    public void ZeroRow_ShowsFriendlyEmptyState()
    {
        var executionId = Guid.NewGuid();
        StubRows(Array.Empty<AuditEventView>());

        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, executionId)
            .Add(c => c.IsOpen, true));

        var empty = cut.Find("[data-test=\"execution-detail-empty\"]");
        Assert.Contains("This execution emitted no audit rows.", empty.TextContent);
    }

    [Fact]
    public void QueryThrows_ShowsInlineErrorState_DoesNotRethrow()
    {
        var executionId = Guid.NewGuid();
        _service.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
            .Returns<Task<IReadOnlyList<AuditEventView>>>(_ => throw new InvalidOperationException("db is down"));

        // Rendering with IsOpen=true must not throw — the modal degrades to an
        // inline error banner rather than killing the SignalR circuit.
        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, executionId)
            .Add(c => c.IsOpen, true));

        var error = cut.Find("[data-test=\"execution-detail-error\"]");
        Assert.Contains("db is down", error.TextContent);
    }

    [Fact]
    public void CloseButton_RaisesOnClose()
    {
        var executionId = Guid.NewGuid();
        StubRows(new[] { MakeEvent(executionId), MakeEvent(executionId) });

        var closed = false;
        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, executionId)
            .Add(c => c.IsOpen, true)
            .Add(c => c.OnClose, EventCallback.Factory.Create(this, () => closed = true)));

        cut.Find("[data-test=\"execution-detail-close\"]").Click();

        Assert.True(closed);
    }

    [Fact]
    public void BackdropClick_RaisesOnClose()
    {
        var executionId = Guid.NewGuid();
        StubRows(new[] { MakeEvent(executionId), MakeEvent(executionId) });

        var closed = false;
        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, executionId)
            .Add(c => c.IsOpen, true)
            .Add(c => c.OnClose, EventCallback.Factory.Create(this, () => closed = true)));

        cut.Find("[data-test=\"execution-detail-backdrop\"]").Click();

        Assert.True(closed);
    }

    [Fact]
    public void EscapeKey_RaisesOnClose()
    {
        var executionId = Guid.NewGuid();
        StubRows(new[] { MakeEvent(executionId), MakeEvent(executionId) });

        var closed = false;
        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, executionId)
            .Add(c => c.IsOpen, true)
            .Add(c => c.OnClose, EventCallback.Factory.Create(this, () => closed = true)));

        cut.Find("[data-test=\"execution-detail-modal\"]").KeyDown("Escape");

        Assert.True(closed);
    }

    [Fact]
    public void Header_ShowsShortExecutionId_AndRowCount()
    {
        var executionId = Guid.Parse("abcdef01-2222-3333-4444-555555555555");
        StubRows(new[] { MakeEvent(executionId), MakeEvent(executionId), MakeEvent(executionId) });

        var cut = Render<ExecutionDetailModal>(p => p
            .Add(c => c.ExecutionId, executionId)
            .Add(c => c.IsOpen, true));

        var modal = cut.Find("[data-test=\"execution-detail-modal\"]");
        // Short id (first 8 hex of the "N" form) appears in the header.
        Assert.Contains("abcdef01", modal.TextContent);
        // Row count surfaces in the header chrome.
        Assert.Contains("3", modal.TextContent);
    }
}
