using Bunit;
using Bunit.TestDoubles;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Ui.Components;
using ZB.MOM.WW.Secrets.Ui.Tests.Fakes;

namespace ZB.MOM.WW.Secrets.Ui.Tests;

public class ConfirmDeleteModalTests : TestContext
{
    [Fact]
    public void ConfirmDeleteModal_IsInPageModal_NoJsDialog()
    {
        // Strict JS interop mode: any IJSRuntime call (a JS confirm/alert dialog) would throw,
        // proving the confirmation is a rendered in-page modal, not a browser dialog.
        JSInterop.Mode = JSRuntimeMode.Strict;

        var store = new RecordingSecretStore();
        var audit = new CapturingAuditWriter();
        Services.AddSingleton<ISecretStore>(store);
        Services.AddSingleton<IAuditWriter>(audit);

        var name = new SecretName("db/primary");
        var auth = this.AddTestAuthorization();
        auth.SetAuthorized("carol");
        auth.SetPolicies(SecretsAuthorization.ManagePolicy);

        bool deleted = false;
        var cut = RenderComponent<ConfirmDeleteModal>(p => p
            .Add(c => c.Visible, true)
            .Add(c => c.Name, name)
            .Add(c => c.OnDeleted, () => deleted = true));

        // The confirmation is a rendered in-page modal element.
        Assert.NotEmpty(cut.FindAll(".zb-secrets-modal"));
        Assert.Contains("db/primary", cut.Markup);

        cut.Find("[data-testid=confirm-delete]").Click();

        // Confirm tombstoned the row (with the resolved actor) and wrote a secret.delete audit.
        Assert.Single(store.Deletes);
        Assert.Equal("db/primary", store.Deletes[0].Name.Value);
        Assert.Equal("carol", store.Deletes[0].Actor);
        Assert.True(deleted);

        Assert.Single(audit.Events);
        AuditEvent evt = audit.Events[0];
        Assert.Equal("secret.delete", evt.Action);
        Assert.Equal(AuditOutcome.Success, evt.Outcome);
        Assert.Equal("Secrets", evt.Category);
        Assert.Equal("db/primary", evt.Target);
        Assert.Equal("carol", evt.Actor);
    }

    [Fact]
    public void ConfirmDeleteModal_Cancel_DoesNotDelete()
    {
        var store = new RecordingSecretStore();
        var audit = new CapturingAuditWriter();
        Services.AddSingleton<ISecretStore>(store);
        Services.AddSingleton<IAuditWriter>(audit);

        var name = new SecretName("db/primary");
        var auth = this.AddTestAuthorization();
        auth.SetAuthorized("carol");
        auth.SetPolicies(SecretsAuthorization.ManagePolicy);

        bool cancelled = false;
        var cut = RenderComponent<ConfirmDeleteModal>(p => p
            .Add(c => c.Visible, true)
            .Add(c => c.Name, name)
            .Add(c => c.OnCancelled, () => cancelled = true));

        cut.Find("[data-testid=cancel-delete]").Click();

        Assert.True(cancelled);
        Assert.Empty(store.Deletes);
        Assert.Empty(audit.Events);
    }

    [Fact]
    public void ConfirmDeleteModal_StoreFault_AuditsFailure_ShowsError_DoesNotThrow()
    {
        var store = new RecordingSecretStore
        {
            DeleteFault = new MasterKeyUnavailableException("KEK unavailable."),
        };
        var audit = new CapturingAuditWriter();
        Services.AddSingleton<ISecretStore>(store);
        Services.AddSingleton<IAuditWriter>(audit);

        var name = new SecretName("db/primary");
        var auth = this.AddTestAuthorization();
        auth.SetAuthorized("carol");
        auth.SetPolicies(SecretsAuthorization.ManagePolicy);

        bool deleted = false;
        var cut = RenderComponent<ConfirmDeleteModal>(p => p
            .Add(c => c.Visible, true)
            .Add(c => c.Name, name)
            .Add(c => c.OnDeleted, () => deleted = true));

        // The click must not throw into the Blazor event pipeline.
        cut.Find("[data-testid=confirm-delete]").Click();

        // OnDeleted not raised; a Failure audit recorded; an error surfaced.
        Assert.False(deleted);
        Assert.Single(audit.Events);
        AuditEvent evt = audit.Events[0];
        Assert.Equal("secret.delete", evt.Action);
        Assert.Equal(AuditOutcome.Failure, evt.Outcome);
        Assert.Equal("Secrets", evt.Category);
        Assert.Equal("db/primary", evt.Target);
        Assert.Equal("carol", evt.Actor);

        Assert.NotEmpty(cut.FindAll("[data-testid=delete-error]"));
    }

    [Fact]
    public void ConfirmDeleteModal_NotVisible_RendersNothing()
    {
        var store = new RecordingSecretStore();
        var audit = new CapturingAuditWriter();
        Services.AddSingleton<ISecretStore>(store);
        Services.AddSingleton<IAuditWriter>(audit);

        var name = new SecretName("db/primary");
        this.AddTestAuthorization().SetAuthorized("carol");

        var cut = RenderComponent<ConfirmDeleteModal>(p => p
            .Add(c => c.Visible, false)
            .Add(c => c.Name, name));

        Assert.Empty(cut.FindAll(".zb-secrets-modal"));
    }

    /// <summary>
    ///     scadaproj#2 regression pins. The modal previously used the bare <c>modal</c> class and
    ///     shipped no CSS, silently depending on the host having no opinion about it — Bootstrap 5's
    ///     <c>.modal { display: none }</c> made the overlay permanently invisible on every
    ///     Bootstrap host (ScadaBridge, OtOpcUa, HistorianGateway) while every handler kept working.
    /// </summary>
    [Fact]
    public void ConfirmDeleteModal_UsesNoFrameworkReservedClassNames()
    {
        var cut = RenderVisibleModal();

        // Bootstrap owns these selectors; any element carrying one is again at the mercy of
        // whatever stylesheet the host happens to link.
        string[] reserved = ["modal", "modal-title", "modal-backdrop", "modal-dialog", "modal-content"];
        foreach (string cls in reserved)
        {
            Assert.Empty(cut.FindAll($".{cls}"));
        }

        // The elements themselves still render, under the component-scoped vocabulary.
        Assert.NotEmpty(cut.FindAll(".zb-secrets-modal"));
        Assert.NotEmpty(cut.FindAll(".zb-secrets-modal-backdrop"));
        Assert.NotEmpty(cut.FindAll(".zb-secrets-modal-card"));
    }

    [Fact]
    public void ConfirmDeleteModal_ShipsItsOwnStyles()
    {
        var cut = RenderVisibleModal();

        // The RCL must carry the modal's CSS itself: no family host links a Secrets.Ui
        // stylesheet, and three of the four never link a scoped-CSS bundle at all — so any
        // styling delivered outside the component silently fails to load exactly like the
        // original defect. The inline block must both position the overlay and give it a
        // display mode, or a host reset/framework rule can hide it again.
        var style = cut.FindAll("style").SingleOrDefault();
        Assert.NotNull(style);
        Assert.Contains(".zb-secrets-modal", style.TextContent);
        Assert.Contains("position: fixed", style.TextContent);
        Assert.Contains("display: flex", style.TextContent);
    }

    private IRenderedComponent<ConfirmDeleteModal> RenderVisibleModal()
    {
        Services.AddSingleton<ISecretStore>(new RecordingSecretStore());
        Services.AddSingleton<IAuditWriter>(new CapturingAuditWriter());
        this.AddTestAuthorization().SetAuthorized("carol");

        return RenderComponent<ConfirmDeleteModal>(p => p
            .Add(c => c.Visible, true)
            .Add(c => c.Name, new SecretName("db/primary")));
    }
}
