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

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

/// <summary>
/// End-to-end coverage for the SMTP Configuration page (<c>/notifications/smtp</c>,
/// <c>RequireAdmin</c> — the test user <c>multi-role</c> has Admin).
///
/// <para>
/// SMTP config is SHARED central state with NO delete verb (neither UI nor CLI). The
/// deterministic, always-safe assertions here MUTATE NOTHING:
/// <list type="bullet">
///   <item><see cref="SmtpForm_MissingRequired_ShowsInlineError"/> opens the form, clears
///   the required Host field and clicks Save — the page's <c>Save</c> handler returns at the
///   required-field guard BEFORE persisting, so nothing is written. Then it cancels.</item>
///   <item><see cref="SmtpPage_RendersConfigOrEmptyState"/> is pure-read render verification.</item>
/// </list>
/// </para>
///
/// <para>
/// <see cref="SmtpEdit_NoopSave_ShowsSavedToast"/> is the only mutating fact, and is
/// gated on safety. Its sole safety mechanism is that the Save is a genuine no-op: the
/// page's <c>StartEdit</c> handler reloads EVERY field into the form
/// (<c>_host/_port/_authType/_tlsMode/_credentials/_fromAddress</c>, including
/// <c>_credentials = smtp.Credentials;</c>), so saving an untouched Edit form rewrites the
/// SAME stored values — zero net change. No CLI restore is used: the masked credential is
/// never exposed (un-snapshottable), and the save changes nothing, so a restore would add
/// risk without benefit. The fact only proceeds when a config already exists (probed via
/// the CLI); when none exists there is nothing to edit, so it skips — we never CREATE a
/// config we could not delete (SMTP config has no delete verb).
/// </para>
/// </summary>
[Collection("Playwright")]
public class SmtpConfigTests
{
    private const string SmtpUrl = "/notifications/smtp";

    private readonly PlaywrightFixture _pw;

    public SmtpConfigTests(PlaywrightFixture pw)
    {
        _pw = pw;
    }

    /// <summary>
    /// Opens the SMTP form (Add when empty, else Edit), clears the required Host field and
    /// clicks Save, asserting the inline 'Host and From Address are required.' error appears.
    /// The page's <c>Save</c> handler returns at this guard BEFORE persisting, so this fact
    /// mutates nothing. Then it cancels and asserts the form closed.
    /// </summary>
    [SkippableFact]
    public async Task SmtpForm_MissingRequired_ShowsInlineError()
    {
        Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);

        var page = await _pw.NewAuthenticatedPageAsync();
        await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{SmtpUrl}");
        await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

        await Assertions.Expect(page.Locator("h4:has-text('SMTP Configuration')")).ToBeVisibleAsync();

        // Open the form. Prefer the empty-state Add button (Host starts empty); otherwise
        // edit the first existing card (Host pre-filled — we clear it below).
        var addBtn = page.Locator("button.btn.btn-primary.btn-sm:has-text('Add SMTP configuration')");
        if (await addBtn.IsVisibleAsync())
        {
            await addBtn.ClickAsync();
        }
        else
        {
            await page.Locator("button.btn-outline-primary.btn-sm:has-text('Edit')").First.ClickAsync();
        }

        // Ensure Host is EMPTY. Clearing Host alone trips the required-field guard, which
        // returns before saving — so nothing is persisted regardless of the other fields.
        await page.Locator("input[type=text].form-control").First.FillAsync("");

        await page.Locator("button.btn-success:has-text('Save')").ClickAsync();

        await Assertions
            .Expect(page.Locator("div.text-danger.small:has-text('Host and From Address are required.')"))
            .ToBeVisibleAsync(new() { Timeout = 10_000 });

        // Cancel and confirm the form closed (Save button gone). Leaves config exactly as found.
        await page.Locator("button.btn-outline-secondary:has-text('Cancel')").ClickAsync();
        await Assertions.Expect(page.Locator("button.btn-success:has-text('Save')")).ToHaveCountAsync(0);
        await Assertions.Expect(page.Locator("h4:has-text('SMTP Configuration')")).ToBeVisibleAsync();
    }

    /// <summary>
    /// Navigates to the SMTP page and asserts it resolved to a real state — EITHER a config
    /// card (Credentials value '(stored)' or '(not set)') is present, OR the empty-state
    /// 'No SMTP configuration set.' text is shown. Pure-read; mutates nothing.
    /// </summary>
    [SkippableFact]
    public async Task SmtpPage_RendersConfigOrEmptyState()
    {
        Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);

        var page = await _pw.NewAuthenticatedPageAsync();
        await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{SmtpUrl}");
        await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

        await Assertions.Expect(page.Locator("h4:has-text('SMTP Configuration')")).ToBeVisibleAsync();

        // Web-first: assert the page resolved to EITHER a config card (Credentials '(stored)' /
        // '(not set)') OR the empty-state, with retry so a slow Blazor init does not spuriously
        // fail. A union locator + single expectation replaces the point-in-time boolean OR.
        await Assertions.Expect(
            page.GetByText(new System.Text.RegularExpressions.Regex(@"\(stored\)|\(not set\)|No SMTP configuration set\."))
                .First
        ).ToBeVisibleAsync(new() { Timeout = 10_000 });
    }

    /// <summary>
    /// Re-saves an existing SMTP config's Edit form with NOTHING changed and asserts the
    /// 'SMTP configuration saved.' toast appears.
    ///
    /// <para>
    /// The no-op save is the SOLE safety mechanism, and it is genuinely sufficient: the page's
    /// <c>StartEdit</c> handler reloads every field into the form
    /// (<c>_host/_port/_authType/_tlsMode/_credentials/_fromAddress</c>, including
    /// <c>_credentials = smtp.Credentials;</c>), so an untouched Save rewrites the SAME stored
    /// values — the credential included — for zero net change. No CLI restore is used: the
    /// masked credential is never exposed so it could not be snapshotted/restored anyway, and
    /// the save mutates nothing. When no config exists there is nothing to edit and the fact
    /// skips (SMTP config has no delete verb, so we never create one we could not delete).
    /// </para>
    /// </summary>
    [SkippableFact]
    public async Task SmtpEdit_NoopSave_ShowsSavedToast()
    {
        Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);

        // Probe whether a config exists; if not, there is nothing safe to edit. We never
        // CREATE a config here because SMTP config has no delete verb (we could not clean up).
        using var listDoc = await CliRunner.RunJsonAsync("notification", "smtp", "list");
        var configs = listDoc.RootElement;
        Skip.If(
            configs.ValueKind != JsonValueKind.Array || configs.GetArrayLength() == 0,
            "No SMTP configuration exists to no-op-edit; SMTP config has no delete verb, " +
            "so we never create one we cannot delete. Validation gate is covered by " +
            "SmtpForm_MissingRequired_ShowsInlineError.");

        var page = await _pw.NewAuthenticatedPageAsync();
        await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{SmtpUrl}");
        await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

        await Assertions.Expect(page.Locator("h4:has-text('SMTP Configuration')")).ToBeVisibleAsync();

        // Open the first card's Edit form. StartEdit reloads EVERY field (including the stored
        // credential, _credentials = smtp.Credentials), so re-saving untouched rewrites the
        // identical values — a true no-op. This is the sole safety mechanism: no CLI restore is
        // used, because the masked credential is never exposed (un-snapshottable) and the save
        // changes nothing, so a restore would add risk without benefit.
        await page.Locator("button.btn-outline-primary.btn-sm:has-text('Edit')").First.ClickAsync();
        await Assertions.Expect(page.Locator("button.btn-success:has-text('Save')")).ToBeVisibleAsync();

        // Change NOTHING; click Save. StartEdit reloaded all fields, so this persists identical
        // values — zero net change.
        await page.Locator("button.btn-success:has-text('Save')").ClickAsync();

        await Assertions
            .Expect(page.Locator(".toast", new() { HasText = "SMTP configuration saved." }))
            .ToHaveCountAsync(1, new() { Timeout = 15_000 });
    }
}
