using Microsoft.Playwright;
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
using Xunit;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment;

/// <summary>
/// E2E coverage for the per-instance deploy-family actions exposed from the
/// Topology tree's right-click context menu — Deploy, Disable, Enable, and
/// Delete. The <see cref="OpenInstanceContextMenuAsync"/> helper (navigate →
/// expand → locate the instance row → right-click) is shared across every fact so
/// none has to re-derive the tree navigation.
///
/// <para>
/// Each fact mints a fresh ephemeral instance on the real <c>site-a</c> (via
/// <see cref="DeploymentFixture.CreateInstanceAsync"/>), exercises the action
/// against the live cluster, then deletes the instance in a <c>finally</c>.
/// Outcomes are tolerant: the relay to <c>site-a</c> may return a confirmation or
/// a fast error, but either way a single outcome toast appears, which every fact
/// asserts with a single web-first <c>ToHaveCountAsync(1)</c> (it retries
/// atomically, so it neither flakes on the relay round-trip nor races the toast's
/// ~5s auto-dismiss).
/// </para>
///
/// <para>
/// The context menu offered per instance depends on the instance's config state
/// (verified against <c>Topology.razor</c>): <c>Deploy</c>/<c>Redeploy</c> and
/// <c>Delete</c> are always present; <c>Disable</c> is shown only when the state
/// is <c>Enabled</c>, and <c>Enable</c> only when <c>Disabled</c>. So the
/// Disable/Enable facts drive the precondition state deterministically over the
/// CLI (<c>deploy</c> → Enabled; <c>deploy</c> then <c>disable</c> → Disabled)
/// before opening the menu. <c>Deploy</c> and <c>Enable</c> fire with no confirm
/// dialog; <c>Disable</c> and <c>Delete</c> are danger actions that first raise a
/// confirm modal whose confirm button is <c>.modal-footer .btn-danger</c> (labelled
/// <c>Delete</c>).
/// </para>
/// </summary>
[Collection("Playwright")]
public class DeploymentActionTests : IClassFixture<DeploymentFixture>
{
    private readonly PlaywrightFixture _pw;
    private readonly DeploymentFixture _cluster;

    public DeploymentActionTests(PlaywrightFixture pw, DeploymentFixture cluster)
    {
        _pw = pw;
        _cluster = cluster;
    }

    [SkippableFact]
    public async Task Deploy_Instance_ShowsOutcomeToast()
    {
        Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);

        var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
        try
        {
            var page = await _pw.NewAuthenticatedPageAsync();
            await OpenInstanceContextMenuAsync(page, uniqueName);

            // A fresh instance is NotDeployed, so the action reads "Deploy" (it would
            // read "Redeploy" only for a stale, already-deployed node). Deploy has no
            // confirm dialog — clicking relays to site-a immediately.
            await page.Locator(".dropdown-menu.show button.dropdown-item", new() { HasText = "Deploy" })
                .ClickAsync();

            // The relay outcome surfaces on a toast — deployed confirmation or a fast
            // error. We assert exactly one toast (the single-toast contract), not which
            // outcome, since the live cluster may answer either way. The toast
            // auto-dismisses ~5s after it appears, so a separate ToBeVisible + CountAsync
            // would race that dismissal (a TOCTOU between the two reads); ToHaveCountAsync
            // retries atomically against the live DOM, with a generous wait for the relay
            // round-trip.
            var toast = page.Locator(".toast");
            await Assertions.Expect(toast).ToHaveCountAsync(1, new() { Timeout = 15_000 });
        }
        finally
        {
            await CliRunner.DeleteInstanceAsync(instanceId);
        }
    }

    [SkippableFact]
    public async Task Disable_Instance_ShowsOutcomeToast()
    {
        Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);

        var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
        try
        {
            // Deploy moves the instance config state to Enabled, so the context menu
            // offers "Disable" (it would offer "Enable" only for a Disabled instance).
            await CliRunner.DeployInstanceAsync(instanceId);
            var page = await _pw.NewAuthenticatedPageAsync();
            await OpenInstanceContextMenuAsync(page, uniqueName);

            await page.Locator(".dropdown-menu.show button.dropdown-item", new() { HasText = "Disable" })
                .ClickAsync();

            // Disable is a danger action: it raises a confirm modal first. Confirm via
            // the danger button (labelled "Delete" for every danger confirm).
            await page.Locator(".modal-footer .btn-danger", new() { HasText = "Delete" })
                .ClickAsync();

            // One outcome toast (confirmation or fast error). ToHaveCountAsync retries
            // atomically, so it survives the relay round-trip and the toast's ~5s dismiss.
            await Assertions.Expect(page.Locator(".toast"))
                .ToHaveCountAsync(1, new() { Timeout = 15_000 });
        }
        finally
        {
            await CliRunner.DeleteInstanceAsync(instanceId);
        }
    }

    [SkippableFact]
    public async Task Enable_Instance_ShowsOutcomeToast()
    {
        Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);

        var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
        try
        {
            // Deploy then Disable leaves the instance config state at Disabled, so the
            // context menu offers "Enable".
            await CliRunner.DeployInstanceAsync(instanceId);
            await CliRunner.DisableInstanceAsync(instanceId);
            var page = await _pw.NewAuthenticatedPageAsync();
            await OpenInstanceContextMenuAsync(page, uniqueName);

            // Enable fires immediately — no confirm dialog.
            await page.Locator(".dropdown-menu.show button.dropdown-item", new() { HasText = "Enable" })
                .ClickAsync();

            await Assertions.Expect(page.Locator(".toast"))
                .ToHaveCountAsync(1, new() { Timeout = 15_000 });
        }
        finally
        {
            await CliRunner.DeleteInstanceAsync(instanceId);
        }
    }

    [SkippableFact]
    public async Task Delete_Instance_RemovesFromTree()
    {
        Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);

        var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
        try
        {
            var page = await _pw.NewAuthenticatedPageAsync();
            await OpenInstanceContextMenuAsync(page, uniqueName);

            // Delete is always present (independent of state) and is the danger item.
            await page.Locator(".dropdown-menu.show button.dropdown-item.text-danger")
                .ClickAsync();

            // Danger action: confirm via the modal's danger button.
            await page.Locator(".modal-footer .btn-danger", new() { HasText = "Delete" })
                .ClickAsync();

            // The delete succeeds locally (NotDeployed instance) and the tree reloads,
            // so the row for this instance disappears. ToHaveCountAsync(0) retries until
            // the reload settles.
            await Assertions.Expect(page.Locator("div.tv-row", new() { HasText = uniqueName }))
                .ToHaveCountAsync(0, new() { Timeout = 15_000 });
        }
        finally
        {
            // Best-effort net: if the UI delete failed, this removes the leftover
            // instance; if it already deleted, this is a no-op (delete is best-effort).
            await CliRunner.DeleteInstanceAsync(instanceId);
        }
    }

    /// <summary>
    /// Navigates to the Topology page, expands the tree so the instance row under
    /// <c>site-a → zztest area</c> is rendered, locates the row by its
    /// <paramref name="uniqueName"/> label, and opens its right-click context menu.
    /// Returns the located row so callers can re-target it if needed.
    ///
    /// <para>
    /// Live updates are switched off first: the page reloads the tree on a 15s timer,
    /// which would collapse a freshly-expanded node and tear down an open context
    /// menu mid-interaction. With it off the tree stays stable through the action.
    /// </para>
    /// </summary>
    private static async Task<ILocator> OpenInstanceContextMenuAsync(IPage page, string uniqueName)
    {
        await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/topology");
        await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

        // Stop the 15s live-updates timer from rebuilding the tree under us.
        var liveToggle = page.Locator("#live-updates");
        if (await liveToggle.IsCheckedAsync())
        {
            await liveToggle.UncheckAsync();
        }

        // ExpandAll() recurses every branch (site → area → …), so one click reveals
        // the nested instance row under site-a's zztest area.
        await page.Locator("button[aria-label='Expand all areas']").ClickAsync();

        var row = page.Locator("div.tv-row", new() { HasText = uniqueName });
        await Assertions.Expect(row).ToBeVisibleAsync(new() { Timeout = 15_000 });

        // The tree scrolls inside a fixed-height container; bring the row into view
        // so the right-click lands on it and the menu renders at a usable position.
        await row.ScrollIntoViewIfNeededAsync();
        await row.ClickAsync(new() { Button = MouseButton.Right });

        await Assertions.Expect(page.Locator(".dropdown-menu.show"))
            .ToBeVisibleAsync(new() { Timeout = 5_000 });

        return row;
    }
}
