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="AuditEventDetail"/> — the reusable single-row
/// detail body extracted from <see cref="AuditDrilldownDrawer"/> (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 <c>data-test</c> values must match the originals so the
/// existing <see cref="AuditDrilldownDrawer"/> selectors keep resolving.
/// </summary>
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 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 RendersFieldBlock()
    {
        var ev = MakeEvent();

        var cut = Render<AuditEventDetail>(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 RendersSourceNodeField_BetweenSiteAndInstance()
    {
        // SourceNode is rendered as a sibling row directly under SourceSiteId
        // so the popup reads "site → node → instance → script" in provenance
        // order. Populated case.
        var ev = MakeEvent() with { SourceNode = "node-a" };

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

        Assert.Contains("data-test=\"field-SourceNode\"", cut.Markup);
        Assert.Contains("node-a", cut.Markup);

        // Ordering: SourceSiteId appears before SourceNode, which appears
        // before SourceInstanceId.
        var siteIdx = cut.Markup.IndexOf("data-test=\"field-SourceSiteId\"", StringComparison.Ordinal);
        var nodeIdx = cut.Markup.IndexOf("data-test=\"field-SourceNode\"", StringComparison.Ordinal);
        var instanceIdx = cut.Markup.IndexOf("data-test=\"field-SourceInstanceId\"", StringComparison.Ordinal);
        Assert.True(siteIdx > 0 && nodeIdx > siteIdx && instanceIdx > nodeIdx,
            $"Expected SourceSiteId < SourceNode < SourceInstanceId; got {siteIdx}, {nodeIdx}, {instanceIdx}");
    }

    [Fact]
    public void RendersSourceNodeField_AsDashWhenNull()
    {
        // Null SourceNode (e.g. central direct-write row pre-feature, or a
        // reconciled row from a retired node) renders as the em-dash, same
        // convention as the sibling provenance fields.
        var ev = MakeEvent();   // SourceNode left at default null
        Assert.Null(ev.SourceNode);

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

        Assert.Contains("data-test=\"field-SourceNode\"", cut.Markup);
        // The field is present and renders the em-dash placeholder.
        Assert.Contains(">—<", cut.Markup);
    }

    [Fact]
    public void ErrorSection_RendersWhenErrorPresent()
    {
        var ev = MakeEvent(
            status: AuditStatus.Parked,
            errorMessage: "boom",
            errorDetail: "stack trace here");

        var cut = Render<AuditEventDetail>(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<AuditEventDetail>(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<AuditEventDetail>(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<AuditEventDetail>(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<AuditEventDetail>(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\":\"<redacted>\"},\"body\":\"hello\"}");

        var cut = Render<AuditEventDetail>(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<AuditEventDetail>(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<AuditEventDetail>(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<AuditEventDetail>(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<AuditEventDetail>(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<AuditEventDetail>(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<AuditEventDetail>(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<AuditEventDetail>(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<AuditEventDetail>(p => p.Add(c => c.Event, ev));

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

        var nav = (BunitNavigationManager)Services.GetRequiredService<NavigationManager>();
        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<AuditEventDetail>(p => p.Add(c => c.Event, ev));

        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()
    {
        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<AuditEventDetail>(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);
    }
}
