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

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

/// <summary>
/// End-to-end render + control-gating tests for the Site Event Logs page
/// (<c>/monitoring/event-logs</c>).
///
/// <para>
/// <b>Why this is a render + controls + tolerant-query suite (no row assertions):</b>
/// Site event logs live in the SITE's local SQLite store and are fetched on demand via
/// an Akka <c>Ask</c> round-trip to a live site — there is no central table, and no CLI
/// or DB seed path. A clean site may legitimately have zero logged events, so these
/// facts must never assert on specific event rows. Instead they verify the two things
/// that are deterministic regardless of site state:
/// <list type="bullet">
///   <item><description>The Search button is gated on site selection (disabled until a
///   site is chosen, then enabled).</description></item>
///   <item><description>Clicking Search resolves the query — the results table renders
///   either a data row or the empty-state row.</description></item>
/// </list>
/// The row-expand affordance is exercised only when real data rows happen to exist.
/// </para>
///
/// <para>
/// Gated on <see cref="ClusterAvailability"/> via <c>Skip.IfNot</c>: when the cluster is
/// unreachable the facts report as Skipped (not Failed), matching the suite idiom.
/// </para>
/// </summary>
[Collection("Playwright")]
public class EventLogsTests
{
    private const string EventLogsUrl = "/monitoring/event-logs";

    private readonly PlaywrightFixture _fixture;

    public EventLogsTests(PlaywrightFixture fixture)
    {
        _fixture = fixture;
    }

    /// <summary>
    /// The Search button must be disabled on page load (no site selected) and become
    /// enabled once a site is selected from the site dropdown. Deterministic regardless
    /// of whether the site has any logged events.
    /// </summary>
    [SkippableFact]
    public async Task EventLogs_SearchGatedOnSiteSelection()
    {
        Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);

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

        await Assertions.Expect(page.Locator("h4:has-text('Site Event Logs')")).ToBeVisibleAsync();

        // The site selector is populated from the cluster's known sites; the live cluster
        // exposes site-a. Each option's value is the site's SiteIdentifier.
        await Assertions.Expect(page.Locator("#filter-site option[value='site-a']")).ToHaveCountAsync(1);

        // Search is gated: disabled until a site is selected.
        var search = page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')");
        await Assertions.Expect(search).ToBeDisabledAsync();

        await page.Locator("#filter-site").SelectOptionAsync(new SelectOptionValue { Value = "site-a" });
        await Assertions.Expect(search).ToBeEnabledAsync(new() { Timeout = 5_000 });
    }

    /// <summary>
    /// After selecting a site and clicking Search, the query must resolve: the results
    /// table renders either a data row or the empty-state row (both match
    /// <c>table tbody tr</c>). The row-expand affordance is exercised only when real data
    /// rows exist — a site may have zero logged events, and event logs are not seedable
    /// from central, so the empty-state render is itself the assertion in that case.
    /// </summary>
    [SkippableFact]
    public async Task EventLogs_Search_RendersTableOrEmptyState()
    {
        Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);

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

        await Assertions.Expect(page.Locator("h4:has-text('Site Event Logs')")).ToBeVisibleAsync();

        await page.Locator("#filter-site").SelectOptionAsync(new SelectOptionValue { Value = "site-a" });
        var searchBtn = page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')");
        await searchBtn.ClickAsync();

        // The Search button re-enables only when _searching flips back to false, i.e. after the
        // site's Akka Ask round-trip completes. Gating on it here makes the table assertion below a
        // genuine post-query check (a hung Ask fails here instead of false-passing on the
        // intermediate empty render Blazor flushes while _entries is a momentary empty list).
        await Assertions.Expect(searchBtn).ToBeEnabledAsync(new() { Timeout = 15_000 });

        // The query has completed (Search re-enabled above), so the terminal table render is now
        // present: a data row OR the empty-state row. The empty-state markup is itself a
        // <tr><td colspan=7>No events found.</td></tr> living inside table tbody, so "table tbody
        // tr" matches it whether or not the site has events.
        var settled = page.Locator("table tbody tr");
        await Assertions.Expect(settled.First).ToBeVisibleAsync(new() { Timeout = 5_000 });

        // Exercise the row-expand ONLY IF real data rows exist. A clean site may have zero
        // logged events, and event logs are not seedable from central — so when no expand
        // toggle is present, the empty-state render above is the assertion (tolerated).
        var expandBtns = page.Locator("button[aria-label='View full message']");
        if (await expandBtns.CountAsync() > 0)
        {
            await expandBtns.First.ClickAsync();
            await Assertions.Expect(page.Locator("button[aria-label='Hide full message']").First)
                .ToBeVisibleAsync(new() { Timeout = 5_000 });
        }
        // else: no events logged on this site — the empty-state render is the assertion.
    }
}
