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 Topology page's area authoring surface — creating an area
/// from the toolbar "+ Area" dialog (the site-picker variant), the inline
/// double-click rename on an area node's label, the right-click "Move to Area…"
/// flows for both an area (<c>MoveAreaDialog</c>) and an instance
/// (<c>MoveInstanceDialog</c>), and the read-only "Diff" comparison
/// (<c>DiffDialog</c>) for a deployed instance.
///
/// <para>
/// Every fact opens the page through <see cref="OpenStableTopologyAsync"/>, which
/// FIRST unchecks the <c>#live-updates</c> switch. The page reloads the whole tree
/// on a 15s timer; left on, that reload collapses freshly-expanded nodes and tears
/// down any open dialog or in-progress rename input mid-interaction. With it off
/// the tree stays stable for the duration of the action. The helper then clicks
/// <c>Expand all areas</c> so nested area nodes are rendered and locatable.
/// </para>
///
/// <para>
/// Toolbar create: "+ Area" opens <c>CreateAreaDialog</c> in its site-picker mode,
/// where the FIRST <c>&lt;select&gt;</c> is the Site picker (option value = site id)
/// and the second is the parent area. Success closes the dialog, raises an
/// "Area '…' created." toast, and renders the new node.
/// </para>
///
/// <para>
/// Inline rename: double-clicking an area's <c>span.tv-label</c> swaps it for a
/// rename input. <b>Enter commits, Escape reverts, and blur CANCELS</b> — so a
/// committing fact fills the input then presses Enter on that SAME input, never
/// clicking elsewhere first (a click would blur and silently discard the edit).
/// </para>
///
/// <para>
/// Areas these tests create/rename are ephemeral and cleaned up in a
/// <c>finally</c>: the rename facts create their own area over the CLI (known id →
/// delete by id), while the toolbar-create fact never learns the new id from the
/// UI, so it sweeps by name prefix
/// (<see cref="CliRunner.ListAreaIdsByNamePrefixAsync"/> →
/// <see cref="CliRunner.DeleteAreaAsync"/>). A CLI read-back after each action
/// confirms the change actually persisted, not just that the toast appeared.
/// </para>
///
/// <para>
/// Move flows: both "Move to Area…" context items open a one-<c>&lt;select&gt;</c>
/// modal whose options carry the area name as their text (root-level areas render
/// without indentation, so an exact visible-text match resolves). Unlike a text
/// input, an HTML <c>&lt;select&gt;</c> fires <c>onchange</c> immediately on
/// selection, so <see cref="ILocator.SelectOptionAsync(SelectOptionValue, FrameSelectOptionOptions)"/>
/// commits the <c>@bind</c> directly (no <c>DispatchEventAsync</c> dance). The
/// area dialog title is "Move area '…' to…" (<c>h6</c>) and the instance dialog
/// "Move '…' to…" (<c>h6</c>); both succeed with a single "moved" toast.
/// </para>
///
/// <para>
/// Diff: the instance "Diff" item is <c>disabled</c> for a <c>NotDeployed</c>
/// instance, so the Diff fact deploys first (over the CLI) to enable it. The
/// resulting <c>DiffDialog</c> is computed centrally (no site relay), so the
/// comparison is deterministic for a deployed instance; its title is
/// "Deployment Diff — &lt;uniqueName&gt;" (<c>h5</c>, em-dash) and it closes via
/// the footer Close button.
/// </para>
/// </summary>
[Collection("Playwright")]
public class TopologyAreaTests : IClassFixture<DeploymentFixture>
{
    private readonly PlaywrightFixture _pw;
    private readonly DeploymentFixture _cluster;

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

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

        var areaName = CliRunner.UniqueName("topoarea");
        try
        {
            var page = await OpenStableTopologyAsync(_pw);

            await page.Locator("button:has-text('+ Area')").ClickAsync();
            var dialog = page.Locator(".modal.show:has(h6.modal-title:has-text('New Area'))");
            await Assertions.Expect(dialog).ToBeVisibleAsync();

            // First select = Site (option value = site.Id); then the name; then Create.
            await dialog.Locator("select").First.SelectOptionAsync(
                new SelectOptionValue { Value = _cluster.SiteAId.ToString() });
            await dialog.Locator("input[placeholder='Area name']").FillAsync(areaName);
            await dialog.Locator("button.btn.btn-primary.btn-sm:has-text('Create')").ClickAsync();

            await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 });
            await Assertions.Expect(page.Locator($"span.tv-label:has-text('{areaName}')"))
                .ToBeVisibleAsync(new() { Timeout = 10_000 });

            var ids = await CliRunner.ListAreaIdsByNamePrefixAsync(_cluster.SiteAId, areaName);
            Assert.Single(ids);
        }
        finally
        {
            foreach (var id in await CliRunner.ListAreaIdsByNamePrefixAsync(_cluster.SiteAId, areaName))
                await CliRunner.DeleteAreaAsync(id);
        }
    }

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

        var original = CliRunner.UniqueName("rnarea");
        var renamed = CliRunner.UniqueName("rndone");
        var areaId = await CliRunner.CreateAreaAsync(_cluster.SiteAId, original);
        try
        {
            var page = await OpenStableTopologyAsync(_pw);

            var label = page.Locator($"span.tv-label:has-text('{original}')");
            await Assertions.Expect(label).ToBeVisibleAsync(new() { Timeout = 10_000 });
            await label.DblClickAsync();

            var input = page.Locator($"input[aria-label='Rename {original}']");
            await Assertions.Expect(input).ToBeVisibleAsync();
            await input.FillAsync(renamed);
            // The input is @bind="_renameBuffer" with the default onchange trigger, which a
            // raw Enter keystroke does NOT fire (onchange only fires on blur in the browser) —
            // and blur CANCELS the rename. So dispatch a `change` event to flush the bound
            // value to the server WITHOUT losing focus, then press Enter to run CommitRename
            // against the now-updated _renameBuffer.
            await input.DispatchEventAsync("change");
            await input.PressAsync("Enter");   // commit WITHOUT blurring (blur would cancel)

            await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 });
            await Assertions.Expect(page.Locator($"span.tv-label:has-text('{renamed}')"))
                .ToBeVisibleAsync(new() { Timeout = 10_000 });

            Assert.Single(await CliRunner.ListAreaIdsByNamePrefixAsync(_cluster.SiteAId, renamed));
        }
        finally
        {
            await CliRunner.DeleteAreaAsync(areaId);
        }
    }

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

        var original = CliRunner.UniqueName("esarea");
        var areaId = await CliRunner.CreateAreaAsync(_cluster.SiteAId, original);
        try
        {
            var page = await OpenStableTopologyAsync(_pw);

            var label = page.Locator($"span.tv-label:has-text('{original}')");
            await Assertions.Expect(label).ToBeVisibleAsync(new() { Timeout = 10_000 });
            await label.DblClickAsync();

            var input = page.Locator($"input[aria-label='Rename {original}']");
            await Assertions.Expect(input).ToBeVisibleAsync();
            await input.FillAsync(CliRunner.UniqueName("discarded"));
            await input.PressAsync("Escape");

            await Assertions.Expect(input).ToHaveCountAsync(0, new() { Timeout = 5_000 });
            await Assertions.Expect(page.Locator($"span.tv-label:has-text('{original}')")).ToBeVisibleAsync();
            Assert.Single(await CliRunner.ListAreaIdsByNamePrefixAsync(_cluster.SiteAId, original));
        }
        finally
        {
            await CliRunner.DeleteAreaAsync(areaId);
        }
    }

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

        var parentName = CliRunner.UniqueName("mvpar");
        var childName = CliRunner.UniqueName("mvchild");
        var parentId = await CliRunner.CreateAreaAsync(_cluster.SiteAId, parentName);
        var childId = await CliRunner.CreateAreaAsync(_cluster.SiteAId, childName);
        try
        {
            var page = await OpenStableTopologyAsync(_pw);

            var childRow = page.Locator("div.tv-row", new() { HasText = childName });
            await Assertions.Expect(childRow).ToBeVisibleAsync(new() { Timeout = 10_000 });
            await childRow.ScrollIntoViewIfNeededAsync();
            await childRow.ClickAsync(new() { Button = MouseButton.Right });
            await page.Locator(".dropdown-menu.show button.dropdown-item:has-text('Move to Area')").ClickAsync();

            // MoveAreaDialog title is "Move area '<name>' to…" (h6) — distinct from the
            // instance dialog ("Move '<name>' to…"), so scope by the "Move area" text.
            var dialog = page.Locator(".modal.show:has(h6.modal-title:has-text('Move area'))");
            await Assertions.Expect(dialog).ToBeVisibleAsync();
            // Root-level area options render without indentation, so the parent's name is
            // the option's exact visible text. A <select> commits @bind on change, so the
            // selection alone flushes _targetParentId (no DispatchEventAsync needed).
            await dialog.Locator("select").SelectOptionAsync(new SelectOptionValue { Label = parentName });
            await dialog.Locator("button.btn.btn-primary.btn-sm:has-text('Move')").ClickAsync();

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

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

        var areaName = CliRunner.UniqueName("mvtgt");
        var areaId = await CliRunner.CreateAreaAsync(_cluster.SiteAId, areaName);
        var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
        try
        {
            var page = await OpenStableTopologyAsync(_pw);

            var row = page.Locator("div.tv-row", new() { HasText = uniqueName });
            await Assertions.Expect(row).ToBeVisibleAsync(new() { Timeout = 10_000 });
            await row.ScrollIntoViewIfNeededAsync();
            await row.ClickAsync(new() { Button = MouseButton.Right });
            await page.Locator(".dropdown-menu.show button.dropdown-item:has-text('Move to Area')").ClickAsync();

            // MoveInstanceDialog title is "Move '<name>' to…" (h6); the leading "Move '"
            // distinguishes it from the area dialog ("Move area '…'").
            var dialog = page.Locator(".modal.show:has(h6.modal-title:has-text(\"Move '\"))");
            await Assertions.Expect(dialog).ToBeVisibleAsync();
            await dialog.Locator("select").SelectOptionAsync(new SelectOptionValue { Label = areaName });
            await dialog.Locator("button.btn.btn-primary.btn-sm:has-text('Move')").ClickAsync();

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

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

        var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
        try
        {
            // Deploy leaves the instance in a state != NotDeployed, which enables the
            // otherwise-disabled "Diff" context item.
            await CliRunner.DeployInstanceAsync(instanceId);

            var page = await OpenStableTopologyAsync(_pw);

            var row = page.Locator("div.tv-row", new() { HasText = uniqueName });
            await Assertions.Expect(row).ToBeVisibleAsync(new() { Timeout = 10_000 });
            await row.ScrollIntoViewIfNeededAsync();
            await row.ClickAsync(new() { Button = MouseButton.Right });
            // :has-text('Diff') won't match "Debug View"; the menu's only "Diff" item is the diff action.
            await page.Locator(".dropdown-menu.show button.dropdown-item:has-text('Diff')").ClickAsync();

            // DiffDialog markup is `.modal.fade.show.d-block`; `.modal.show` substring-matches.
            // Its title is an h5 (the move dialogs use h6) and reads "Deployment Diff — <uniqueName>".
            var diff = page.Locator(".modal.show:has(h5.modal-title:has-text('Deployment Diff'))");
            await Assertions.Expect(diff).ToBeVisibleAsync(new() { Timeout = 15_000 });
            await Assertions.Expect(diff.Locator("h5.modal-title")).ToContainTextAsync(uniqueName);
            await diff.Locator("button.btn.btn-secondary.btn-sm:has-text('Close')").ClickAsync();
            await Assertions.Expect(diff).ToHaveCountAsync(0, new() { Timeout = 5_000 });
        }
        finally
        {
            await CliRunner.DeleteInstanceAsync(instanceId);
        }
    }

    /// <summary>
    /// Navigates to the Topology page and returns a page whose tree is stable and
    /// fully expanded: it unchecks the <c>#live-updates</c> switch (stopping the 15s
    /// timer that would otherwise reload the tree and collapse expansions / tear down
    /// open dialogs and rename inputs), then clicks <c>Expand all areas</c> so nested
    /// area nodes are rendered and locatable.
    /// </summary>
    private static async Task<IPage> OpenStableTopologyAsync(PlaywrightFixture pw)
    {
        var page = await pw.NewAuthenticatedPageAsync();
        await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/topology");
        await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
        var live = page.Locator("#live-updates");
        if (await live.IsCheckedAsync()) await live.UncheckAsync();   // stop the 15s tree reload
        await page.Locator("button[aria-label='Expand all areas']").ClickAsync();
        return page;
    }
}
