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

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

/// <summary>
/// bUnit tests for <see cref="AuditDrilldownDrawer"/> (#23 M7 Bundle C / M7-T4..T8).
///
/// The drawer is a child component opened from the Audit Log page when a grid row
/// is clicked. It renders the offcanvas chrome (header, open/close) and delegates
/// the <see cref="AuditEventView"/> body to the shared <see cref="AuditEventDetail"/>
/// component, which since the recent refactor owns the channel-aware bodies
/// (JSON pretty-print, SQL block for DbOutbound), redaction badges on
/// Request/Response, and conditional action buttons.
///
/// Tests pin the behaviours we cannot lose without breaking the spec:
/// offcanvas open/close, header rendering, and that the event body is handed
/// off to <see cref="AuditEventDetail"/>.
/// </summary>
public class AuditDrilldownDrawerTests : BunitContext
{
    public AuditDrilldownDrawerTests()
    {
        // Default to Loose so the cURL clipboard call does not blow up tests
        // that don't exercise it. Tests that need to assert interop calls flip
        // to Strict and configure their own setups.
        JSInterop.Mode = JSRuntimeMode.Loose;
    }

    private static AuditEventView 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 Drawer_RendersField_OccurredAtUtc()
    {
        var ev = MakeEvent();

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        // OccurredAtUtc renders ISO-8601 round-trip ("o" format). The
        // year+time fragment is sufficient evidence — the full ISO string
        // changes shape with locale-dependent formatting in some envs.
        Assert.Contains("data-test=\"field-OccurredAtUtc\"", cut.Markup);
        Assert.Contains("2026-05-20T12:30:45", cut.Markup);
    }

    [Fact]
    public void Drawer_JsonRequestSummary_PrettyPrinted_Indented()
    {
        // A single-line JSON body should be re-emitted indented.
        var ev = MakeEvent(requestSummary: "{\"a\":1,\"b\":\"two\"}");

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        // Pretty-print writes one property per line — the "  \"a\":" prefix
        // proves indentation. We don't pin the exact bytes; we pin "indented"
        // by looking for newline-prefixed property lines.
        Assert.Contains("data-test=\"request-body\"", cut.Markup);
        Assert.Matches(@"\n\s+""a"":\s*1", cut.Markup);
        Assert.Matches(@"\n\s+""b"":\s*""two""", cut.Markup);
    }

    [Fact]
    public void Drawer_NonJsonRequestSummary_RenderedVerbatim()
    {
        // Non-JSON content (e.g. plain text or invalid JSON) must round-trip
        // exactly — the drawer should not attempt to "fix" or rewrite it.
        var ev = MakeEvent(requestSummary: "not really json {{}");

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.Contains("not really json {{}", cut.Markup);
    }

    [Fact]
    public void Drawer_DbOutboundChannel_RendersSqlBlock()
    {
        // DbOutbound payloads carry a {sql, parameters} JSON shape. The drawer
        // renders sql inside a code block with language-sql class (CSS-only,
        // no JS highlighter) and lists the parameters in a definition list.
        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<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.Contains("language-sql", cut.Markup);
        Assert.Contains("UPDATE T SET x=@p1 WHERE id=@p2", cut.Markup);
        // Parameter dl shows both keys.
        Assert.Contains("p1", cut.Markup);
        Assert.Contains("p2", cut.Markup);
        Assert.Contains("42", cut.Markup);
        Assert.Contains("abc", cut.Markup);
    }

    [Fact]
    public void Drawer_ApiOutbound_ShowsCopyAsCurlButton()
    {
        var ev = MakeEvent(channel: AuditChannel.ApiOutbound);

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.Contains("data-test=\"copy-as-curl\"", cut.Markup);
    }

    [Fact]
    public void Drawer_NotApiChannel_HidesCopyAsCurlButton()
    {
        // Notification is neither an API outbound nor inbound — no cURL.
        var ev = MakeEvent(channel: AuditChannel.Notification, kind: AuditKind.NotifySend);

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.DoesNotContain("data-test=\"copy-as-curl\"", cut.Markup);
    }

    [Fact]
    public void Drawer_NullCorrelationId_HidesShowAllButton()
    {
        var ev = MakeEvent(correlationId: null);

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.DoesNotContain("data-test=\"show-all-events\"", cut.Markup);
    }

    [Fact]
    public void Drawer_RedactedBody_ShowsRedactionBadge()
    {
        // The redaction sentinel is the literal string `<redacted>` (or
        // `<redacted: redactor error>`) — the drawer must flag it visibly.
        var ev = MakeEvent(requestSummary: "{\"headers\":{\"Authorization\":\"<redacted>\"},\"body\":\"hello\"}");

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.Contains("data-test=\"redaction-badge-request\"", cut.Markup);
    }

    [Fact]
    public void Drawer_NonRedactedBody_HidesBadge()
    {
        var ev = MakeEvent(requestSummary: "{\"headers\":{\"Authorization\":\"Bearer abc\"},\"body\":\"hello\"}");

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.DoesNotContain("data-test=\"redaction-badge-request\"", 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<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        cut.Find("[data-test=\"show-all-events\"]").Click();

        var nav = (BunitNavigationManager)Services.GetRequiredService<NavigationManager>();
        Assert.Contains("/audit/log?correlationId=", nav.Uri);
        Assert.Contains(corr.ToString(), nav.Uri);
    }

    [Fact]
    public void Drawer_NullExecutionId_HidesViewThisExecutionButton()
    {
        var ev = MakeEvent(executionId: null);

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.DoesNotContain("data-test=\"view-this-execution\"", cut.Markup);
    }

    [Fact]
    public void Drawer_NonNullExecutionId_ShowsViewThisExecutionButton()
    {
        var ev = MakeEvent(executionId: Guid.Parse("aaaaaaaa-1111-2222-3333-444444444444"));

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.Contains("data-test=\"view-this-execution\"", cut.Markup);
    }

    [Fact]
    public void ViewThisExecution_Navigates_WithExecutionIdQueryString()
    {
        var exec = Guid.Parse("dddddddd-cccc-bbbb-aaaa-999999999999");
        var ev = MakeEvent(executionId: exec);

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

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

        var nav = (BunitNavigationManager)Services.GetRequiredService<NavigationManager>();
        Assert.Contains($"/audit/log?executionId={exec}", nav.Uri);
    }

    [Fact]
    public void Drawer_NullParentExecutionId_HidesViewParentExecutionButton()
    {
        var ev = MakeEvent(parentExecutionId: null);

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.DoesNotContain("data-test=\"view-parent-execution\"", cut.Markup);
    }

    [Fact]
    public void Drawer_NonNullParentExecutionId_ShowsViewParentExecutionButton()
    {
        var ev = MakeEvent(parentExecutionId: Guid.Parse("bbbbbbbb-1111-2222-3333-444444444444"));

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.Contains("data-test=\"view-parent-execution\"", cut.Markup);
    }

    [Fact]
    public void ViewParentExecution_Navigates_WithExecutionIdQueryString()
    {
        // A routed (child) row drills in to its spawner: the "View parent
        // execution" action navigates to /audit/log?executionId={ParentExecutionId}
        // so the user sees the spawner execution's rows.
        var parent = Guid.Parse("eeeeeeee-dddd-cccc-bbbb-aaaaaaaaaaaa");
        var ev = MakeEvent(parentExecutionId: parent);

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

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

        var nav = (BunitNavigationManager)Services.GetRequiredService<NavigationManager>();
        Assert.Contains($"/audit/log?executionId={parent}", nav.Uri);
    }

    [Fact]
    public void Drawer_NullExecutionId_HidesViewExecutionChainButton()
    {
        var ev = MakeEvent(executionId: null);

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.DoesNotContain("data-test=\"view-execution-chain\"", cut.Markup);
    }

    [Fact]
    public void Drawer_NonNullExecutionId_ShowsViewExecutionChainButton()
    {
        var ev = MakeEvent(executionId: Guid.Parse("aaaaaaaa-9999-8888-7777-666666666666"));

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        Assert.Contains("data-test=\"view-execution-chain\"", cut.Markup);
    }

    [Fact]
    public void ViewExecutionChain_Navigates_ToExecutionTreePage()
    {
        // The "View execution chain" action opens the tree view rooted at the
        // chain containing this row's ExecutionId.
        var exec = Guid.Parse("12345678-aaaa-bbbb-cccc-1234567890ab");
        var ev = MakeEvent(executionId: exec);

        var cut = Render<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

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

        var nav = (BunitNavigationManager)Services.GetRequiredService<NavigationManager>();
        Assert.Contains($"/audit/execution-tree?executionId={exec}", nav.Uri);
    }

    [Fact]
    public async Task CopyAsCurl_InvokesClipboard_WithCurlString()
    {
        // Set up Strict mode interop so the call must match exactly.
        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));

        // Build an event with a {headers, body} RequestSummary so the cURL
        // builder has material to fold in.
        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<AuditDrilldownDrawer>(p => p
            .Add(c => c.Event, ev)
            .Add(c => c.IsOpen, true));

        await cut.InvokeAsync(() => cut.Find("[data-test=\"copy-as-curl\"]").Click());

        // Bunit's JSRuntimeInvocationDictionary is keyed by identifier
        // (string) — we enumerate it instead of indexing by int.
        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);
        Assert.Contains("Content-Type: application/json", argString);
    }
}
