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

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

/// <summary>
/// End-to-end round-trip for the Template design pages:
/// create → add attribute → delete, all via the Central UI against the
/// running dev cluster.
/// </summary>
[Collection("Playwright")]
public class TemplateCrudTests : IClassFixture<DeploymentFixture>
{
    private readonly PlaywrightFixture _fixture;
    private readonly DeploymentFixture _cluster;

    public TemplateCrudTests(PlaywrightFixture fixture, DeploymentFixture cluster)
    {
        _fixture = fixture;
        _cluster = cluster;
    }

    [SkippableFact]
    public async Task CreateAddAttributeDelete_Template_RoundTrips()
    {
        Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);

        var name = $"zztest-tmpl-{Guid.NewGuid().ToString("N")[..8]}";
        var page = await _fixture.NewAuthenticatedPageAsync();

        try
        {
            // ── CREATE ────────────────────────────────────────────────────────────────
            await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/templates/create");
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

            // Name input is label-anchored: <label class="form-label">Name</label>
            // followed by <input class="form-control" @bind="_createName" />.
            // Use the label's sibling input scoped to the containing div to avoid any
            // strict-mode violation from the Description input (also form-control).
            await page.Locator("div.mb-3:has(label:has-text('Name')) input.form-control").FillAsync(name);

            // Leave Parent Template at the default "(None - root template)".
            // Click the green Create button.
            await page.ClickAsync("button.btn.btn-success:has-text('Create')");

            // After a successful create, Blazor navigates to /design/templates/{id}.
            // Poll window.location until the path matches /design/templates/ + digits
            // and does not still say /create.
            await PlaywrightFixture.WaitForPathAsync(page, "/design/templates/", excludePath: "/create");
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

            // Sanity: we must be on a numeric template detail URL.
            Assert.Matches(@"/design/templates/\d+$", page.Url);

            // ── ADD ATTRIBUTE ─────────────────────────────────────────────────────────
            // The Attributes tab is the default-active tab (_activeTab = "attributes"),
            // so we don't need to click it, but we do wait for the tab panel to render.
            await Assertions.Expect(
                page.Locator("button.nav-link:has-text('Attributes')"))
                .ToBeVisibleAsync();

            // Click Add Attribute.
            await page.ClickAsync("button.btn.btn-primary.btn-sm:has-text('Add Attribute')");

            // The modal is a page-local .modal.show.d-block — NOT the global DialogHost.
            // DialogHost adds a `fade` class; the page-local modal does not, so :not(.fade)
            // ensures we match only the page-local Add-Attribute modal.
            var modal = page.Locator(".modal.show.d-block:not(.fade)");
            await Assertions.Expect(modal).ToBeVisibleAsync();
            await Assertions.Expect(modal.Locator(".modal-title")).ToHaveTextAsync("Add Attribute");

            // Fill Name field inside the modal.
            await modal.Locator("div.col-12:has(label:has-text('Name')) input.form-control").FillAsync("Val");

            // Select Data Type = Double.
            // The select is label-anchored: <label>Data Type</label> + <select class="form-select">.
            await modal.Locator("div.col-12:has(label:has-text('Data Type')) select.form-select").SelectOptionAsync("Double");

            // Click the footer Add button (btn-success, text "Add"). The dialog feet were
            // wrapped in a btn-group btn-group-sm by the 2026-08-11 UI sweep, so the size
            // class now lives on the group rather than on the button itself.
            await modal.Locator(".modal-footer button.btn-success:has-text('Add')").ClickAsync();

            // The modal should dismiss and the attribute table should show "Val".
            await Assertions.Expect(modal).ToHaveCountAsync(0, new() { Timeout = 10_000 });
            await Assertions.Expect(page.Locator("table td:has-text('Val')"))
                .ToBeVisibleAsync(new() { Timeout = 10_000 });

            // ── DELETE ────────────────────────────────────────────────────────────────
            // Click the header Delete button (btn-outline-danger btn-sm, text "Delete").
            await page.ClickAsync("button.btn.btn-outline-danger:has-text('Delete')");

            // The global DialogHost renders a confirm dialog.
            // The danger confirm button carries class "btn-danger" and text "Delete".
            var confirmBtn = page.Locator(".modal-footer .btn-danger:has-text('Delete')");
            await Assertions.Expect(confirmBtn).ToBeVisibleAsync(new() { Timeout = 5_000 });
            await confirmBtn.ClickAsync();

            // After delete, Blazor navigates back to /design/templates (the list page).
            // excludePath: "/design/templates/" rejects any /design/templates/{id} detail URL.
            await PlaywrightFixture.WaitForPathAsync(page, "/design/templates", excludePath: "/design/templates/");
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

            // The template name must no longer appear in the tree view.
            await Assertions.Expect(
                page.Locator("span.tv-label", new() { HasText = name }))
                .ToHaveCountAsync(0, new() { Timeout = 10_000 });
        }
        finally
        {
            // Best-effort safety net: clean up any leftover zztest-tmpl-* templates
            // that were not deleted by the UI (e.g. test failed mid-flow).
            try
            {
                foreach (var id in await CliRunner.ListTemplateIdsByNamePrefixAsync(name))
                {
                    await CliRunner.DeleteTemplateAsync(id);
                }
            }
            catch
            {
                // Best-effort — swallow to avoid masking the original test failure.
            }
        }
    }

    [SkippableFact]
    public async Task CreateTemplate_DuplicateName_ShowsInlineError()
    {
        Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);

        // CLI-seed an existing base template, then UI-attempt to create a duplicate.
        // Base (non-derived) Template.Name has a unique index
        // (HasIndex(t => t.Name).IsUnique().HasFilter("[IsDerived]=0")) and
        // TemplateService.CreateTemplateAsync has no friendly duplicate pre-check,
        // so the DB-constraint exception is caught into _formError and rendered inline
        // in div.text-danger.small with no navigation (stays on /create).
        // (Empirically confirmed: duplicate create surfaces inline; duplicate-name path used.)
        var name = CliRunner.UniqueName("tmpl");
        var seededId = await CliRunner.CreateTemplateAsync(name);

        try
        {
            var page = await _fixture.NewAuthenticatedPageAsync();

            await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/templates/create");
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

            // Fill the Name input with the duplicate name and click Create.
            await page.Locator("div.mb-3:has(label:has-text('Name')) input.form-control").FillAsync(name);
            await page.ClickAsync("button.btn.btn-success:has-text('Create')");

            // Web-first assertions: the inline error becomes visible and we stay on /create.
            // Do NOT assert a literal message — it is the DB-constraint exception text.
            await Assertions.Expect(page.Locator("div.text-danger.small")).ToBeVisibleAsync();
            await Assertions.Expect(page)
                .ToHaveURLAsync(new System.Text.RegularExpressions.Regex("/design/templates/create"));
        }
        finally
        {
            // Delete the seeded source template, then sweep any leftover by name.
            await CliRunner.DeleteTemplateAsync(seededId);
            try
            {
                foreach (var id in await CliRunner.ListTemplateIdsByNamePrefixAsync(name))
                {
                    await CliRunner.DeleteTemplateAsync(id);
                }
            }
            catch
            {
                // Best-effort — swallow to avoid masking the original test failure.
            }
        }
    }

    [SkippableFact]
    public async Task CreateTemplate_Cancel_ReturnsToListWithoutCreating()
    {
        Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);

        var name = CliRunner.UniqueName("tmpl");

        try
        {
            var page = await _fixture.NewAuthenticatedPageAsync();

            await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/templates/create");
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

            // Fill the Name input, then click Cancel — Blazor navigates back to the list.
            await page.Locator("div.mb-3:has(label:has-text('Name')) input.form-control").FillAsync(name);
            await page.ClickAsync("button.btn.btn-outline-secondary:has-text('Cancel')");

            // excludePath: "/create" rejects the /design/templates/create URL we came from.
            await PlaywrightFixture.WaitForPathAsync(page, "/design/templates", excludePath: "/create");
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

            // Nothing was created: no template exists with our unique name.
            Assert.Empty(await CliRunner.ListTemplateIdsByNamePrefixAsync(name));
        }
        finally
        {
            // Defensive sweep by name in case of an unexpected create.
            try
            {
                foreach (var id in await CliRunner.ListTemplateIdsByNamePrefixAsync(name))
                {
                    await CliRunner.DeleteTemplateAsync(id);
                }
            }
            catch
            {
                // Best-effort — swallow to avoid masking the original test failure.
            }
        }
    }

    [SkippableFact]
    public async Task EditAttribute_PersistsChange()
    {
        Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);

        // CLI-seed a template with a single Double "Val" attribute, then edit its
        // Value through the page-local Edit-Attribute modal and confirm it persists.
        var name = CliRunner.UniqueName("tmpl");
        var id = await CliRunner.CreateTemplateAsync(name);
        await CliRunner.AddAttributeAsync(id, "Val", "Double");

        try
        {
            var page = await _fixture.NewAuthenticatedPageAsync();

            await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/templates/{id}");
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

            // Open the Val row's actions dropdown, then click the "Edit…" item
            // (note the ellipsis char "…" — it is in the markup verbatim).
            await page.ClickAsync("button[aria-label=\"More actions for Val\"]");
            await page.ClickAsync("button.dropdown-item:has-text('Edit…')");

            // The page-local attribute modal is .modal.show.d-block WITHOUT `fade`
            // (the DialogHost confirm modal HAS `fade`). :not(.fade) pins the
            // page-local one.
            var modal = page.Locator(".modal.show.d-block:not(.fade)");
            await Assertions.Expect(modal).ToBeVisibleAsync();
            await Assertions.Expect(modal.Locator("h6.modal-title")).ToHaveTextAsync("Edit Attribute");

            // When editing, the Name input is rendered readonly (readonly="@editing").
            await Assertions.Expect(
                modal.Locator("div.col-12:has(label:has-text('Name')) input.form-control"))
                .ToHaveAttributeAsync("readonly", "");

            // The Value input is label-anchored: its containing div carries the
            // "Value" label. ("Data Source Ref" does not contain "Value", so the
            // substring match resolves uniquely to the Value field.)
            var valueInput = modal.Locator("div.col-12:has(label:has-text('Value')) input.form-control");
            await valueInput.FillAsync("42.5");

            // Footer button text is "Save" when editing (vs "Add" when adding). Size class
            // sits on the enclosing btn-group-sm since the 2026-08-11 UI sweep.
            await modal.Locator(".modal-footer button.btn-success:has-text('Save')").ClickAsync();

            // SaveAttribute persists then reloads, dismissing the modal.
            await Assertions.Expect(modal).ToHaveCountAsync(0, new() { Timeout = 10_000 });

            // The attribute table renders the value in a <td class="small">@effectiveValue</td>
            // cell (for a non-derived template effectiveValue == attr.Value), so the
            // persisted "42.5" must appear in the Val row's value cell. Web-first wait
            // covers the post-save reload.
            await Assertions.Expect(page.Locator("table td.small:has-text('42.5')"))
                .ToBeVisibleAsync(new() { Timeout = 10_000 });
        }
        finally
        {
            await CliRunner.DeleteTemplateAsync(id);
            // Defensive sweep by name in case the delete above was a no-op.
            try
            {
                foreach (var leftover in await CliRunner.ListTemplateIdsByNamePrefixAsync(name))
                {
                    await CliRunner.DeleteTemplateAsync(leftover);
                }
            }
            catch
            {
                // Best-effort — swallow to avoid masking the original test failure.
            }
        }
    }

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

        // Mint a non-deployed instance referencing the fixture's template — that
        // reference is sufficient for TemplateDeletionService to block the delete
        // with the "instance(s) reference it" error.
        var (instId, _) = await _cluster.CreateInstanceAsync();
        try
        {
            var page = await _fixture.NewAuthenticatedPageAsync();

            await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/templates/{_cluster.TemplateId}");
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

            // Header Delete button (btn-outline-danger btn-sm).
            await page.ClickAsync("button.btn.btn-outline-danger:has-text('Delete')");

            // Confirm via the global DialogHost modal's danger button (labelled
            // "Delete"). This modal HAS the `fade` class — distinct from the
            // page-local attribute modal.
            var confirmBtn = page.Locator(".modal-footer .btn-danger:has-text('Delete')");
            await Assertions.Expect(confirmBtn).ToBeVisibleAsync(new() { Timeout = 5_000 });
            await confirmBtn.ClickAsync();

            // The delete fails: DeleteTemplate surfaces result.Error on a toast and
            // does NOT navigate. The error reads
            // "Cannot delete template '{name}': {n} instance(s) reference it (...)".
            await Assertions.Expect(page.Locator(".toast"))
                .ToContainTextAsync("instance(s) reference it", new() { Timeout = 10_000 });

            // The template still exists — the page stayed on the detail URL because
            // the failed delete did not navigate away.
            await Assertions.Expect(page)
                .ToHaveURLAsync(new System.Text.RegularExpressions.Regex(
                    $"/design/templates/{_cluster.TemplateId}$"));
        }
        finally
        {
            // Remove this test's instance so the fixture template is instance-free
            // again; the fixture's DisposeAsync sweeps the template + any leftovers.
            await CliRunner.DeleteInstanceAsync(instId);
        }
    }
}
