using Microsoft.Playwright;
using Xunit;

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

/// <summary>
/// End-to-end coverage for the Audit Log results-grid column UX (#23
/// follow-ups Task 10): drag-to-resize and drag-to-reorder columns, with the
/// chosen widths + order persisted in the browser's <c>sessionStorage</c>.
///
/// <para>
/// The drag interaction is browser-side (<c>wwwroot/js/audit-grid.js</c>), so
/// Playwright — not bUnit — is the right tool: bUnit cannot drive the native
/// HTML5 drag-and-drop or pointer-capture resize. Each test seeds one
/// <c>AuditLog</c> row via <see cref="AuditDataSeeder"/> so the grid has a
/// header row to act on, then best-effort deletes it.
/// </para>
///
/// <para>
/// The DB-seeding tests are <see cref="SkippableFactAttribute"/> + <c>Skip.IfNot</c>:
/// when the cluster / MSSQL is unreachable they report as Skipped (not Failed),
/// matching the established <see cref="SiteCalls.SiteCallsPageTests"/> idiom.
/// </para>
/// </summary>
[Collection("Playwright")]
public class AuditGridColumnTests
{
    private const string AuditLogUrl = "/audit/log";

    /// <summary>Skip reason shared by the DB-seeding tests when MSSQL is down.</summary>
    private const string DbUnavailableSkipReason =
        "AuditDataSeeder cannot reach MSSQL at localhost:1433 — bring up infra/docker-compose and docker/deploy.sh, " +
        "or set SCADABRIDGE_PLAYWRIGHT_DB to a reachable connection string.";

    private readonly PlaywrightFixture _fixture;

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

    /// <summary>
    /// Seeds one audit row, opens the Audit Log page, and clicks Apply so the
    /// results grid renders a header row the column tests can act on.
    /// </summary>
    private async Task<IPage> OpenGridWithSeededRowAsync(string targetPrefix, Guid eventId)
    {
        await AuditDataSeeder.InsertAuditEventAsync(
            eventId: eventId,
            occurredAtUtc: DateTime.UtcNow,
            channel: "ApiOutbound",
            kind: "ApiCall",
            status: "Delivered",
            target: targetPrefix + "endpoint",
            httpStatus: 200,
            durationMs: 25);

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

        // Apply with no chips — the default LastHour range matches the fresh row.
        await page.Locator("[data-test='filter-apply']").ClickAsync();
        await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

        var row = page.Locator($"[data-test='grid-row-{eventId}']");
        await Assertions.Expect(row).ToBeVisibleAsync();
        return page;
    }

    /// <summary>Pixel width of a header cell, measured from its bounding box.</summary>
    private static async Task<double> HeaderWidthAsync(IPage page, string columnKey)
    {
        var box = await page.Locator($"[data-col-key='{columnKey}']").BoundingBoxAsync();
        Assert.NotNull(box);
        return box!.Width;
    }

    /// <summary>The ordered list of column keys as currently rendered in the header.</summary>
    private static async Task<IReadOnlyList<string>> HeaderOrderAsync(IPage page)
    {
        return await page.Locator("thead th[data-col-key]")
            .EvaluateAllAsync<string[]>("els => els.map(e => e.getAttribute('data-col-key'))");
    }

    /// <summary>
    /// Polls until <paramref name="storageKey"/> has been written to
    /// <c>sessionStorage</c>. The grid persists a resize/reorder
    /// asynchronously — the browser-side drag fires a fire-and-forget
    /// JS→.NET invoke (<c>OnColumnResized</c>/<c>OnColumnReordered</c>), and
    /// the .NET handler then round-trips back through JS interop to write
    /// <c>sessionStorage</c>. A bare <c>getItem</c> immediately after the drag
    /// races that round-trip; this waits for the key to actually land.
    /// </summary>
    private static async Task WaitForStorageKeyAsync(IPage page, string storageKey)
    {
        await page.WaitForFunctionAsync(
            "key => sessionStorage.getItem(key) !== null", storageKey);
    }

    /// <summary>
    /// Polls until the header's first column key equals <paramref name="expectedFirstKey"/>.
    /// A drag-to-reorder re-renders the header asynchronously (the JS→.NET
    /// <c>OnColumnReordered</c> invoke is fire-and-forget), so reading the
    /// header order synchronously after <c>DragToAsync</c> can observe the
    /// pre-reorder layout. This waits for the re-render to settle.
    /// </summary>
    private static async Task WaitForFirstColumnAsync(IPage page, string expectedFirstKey)
    {
        await page.WaitForFunctionAsync(
            "key => { var th = document.querySelector('thead th[data-col-key]'); " +
            "return th && th.getAttribute('data-col-key') === key; }",
            expectedFirstKey);
    }

    [SkippableFact]
    public async Task ResizeHandle_DraggingWidensColumn_AndSurvivesReload()
    {
        Skip.IfNot(await AuditDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason);

        var runId = Guid.NewGuid().ToString("N");
        var targetPrefix = $"playwright-test/grid-resize/{runId}/";
        var eventId = Guid.NewGuid();

        try
        {
            var page = await OpenGridWithSeededRowAsync(targetPrefix, eventId);

            const string columnKey = "Target";
            var before = await HeaderWidthAsync(page, columnKey);

            // Drag the resize handle on the column's right edge 120px to the
            // right. The handle is a thin strip; grab its centre and drag.
            var handle = page.Locator($"[data-test='col-resize-{columnKey}']");
            var handleBox = await handle.BoundingBoxAsync();
            Assert.NotNull(handleBox);
            var startX = handleBox!.X + handleBox.Width / 2;
            var startY = handleBox.Y + handleBox.Height / 2;

            await page.Mouse.MoveAsync(startX, startY);
            await page.Mouse.DownAsync();
            await page.Mouse.MoveAsync(startX + 120, startY, new MouseMoveOptions { Steps = 8 });
            await page.Mouse.UpAsync();

            var after = await HeaderWidthAsync(page, columnKey);
            Assert.True(after > before + 40,
                $"Expected the {columnKey} column to widen after the resize drag (before={before}, after={after}).");

            // The resize persists asynchronously: pointer-up fires a
            // fire-and-forget JS→.NET OnColumnResized invoke, and the .NET
            // handler then round-trips back through JS interop to write
            // sessionStorage. Wait for that write to land before reloading —
            // otherwise the reload races it and the restored grid falls back
            // to the default width.
            await WaitForStorageKeyAsync(page, "auditGrid:columnWidths");

            // Reload: the persisted width is restored from sessionStorage.
            await page.ReloadAsync();
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
            await page.Locator("[data-test='filter-apply']").ClickAsync();
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

            var afterReload = await HeaderWidthAsync(page, columnKey);
            // Allow a small tolerance for sub-pixel layout rounding.
            Assert.True(Math.Abs(afterReload - after) < 8,
                $"Expected the resized width to survive a reload (after={after}, afterReload={afterReload}).");
        }
        finally
        {
            await AuditDataSeeder.DeleteByTargetPrefixAsync(targetPrefix);
        }
    }

    [SkippableFact]
    public async Task ReorderDrag_MovesColumn_AndSurvivesReload()
    {
        Skip.IfNot(await AuditDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason);

        var runId = Guid.NewGuid().ToString("N");
        var targetPrefix = $"playwright-test/grid-reorder/{runId}/";
        var eventId = Guid.NewGuid();

        try
        {
            var page = await OpenGridWithSeededRowAsync(targetPrefix, eventId);

            var initialOrder = await HeaderOrderAsync(page);
            // Default order opens with OccurredAtUtc first, Status fifth.
            Assert.Equal("OccurredAtUtc", initialOrder[0]);
            Assert.Contains("Status", initialOrder);

            // Drag the Status header onto the OccurredAtUtc header — Status
            // should move into the leading slot.
            var source = page.Locator("[data-col-key='Status']");
            var target = page.Locator("[data-col-key='OccurredAtUtc']");
            await source.DragToAsync(target);
            // The reorder re-renders the header asynchronously (fire-and-forget
            // JS→.NET invoke); wait for it to settle before reading the order.
            await WaitForFirstColumnAsync(page, "Status");

            var afterOrder = await HeaderOrderAsync(page);
            Assert.Equal("Status", afterOrder[0]);
            Assert.True(afterOrder.ToList().IndexOf("Status") < afterOrder.ToList().IndexOf("OccurredAtUtc"),
                "Expected Status to be reordered ahead of OccurredAtUtc.");

            // Reload: the persisted order is restored from sessionStorage on
            // the grid's first render — wait for the header to reflect it.
            await page.ReloadAsync();
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
            await page.Locator("[data-test='filter-apply']").ClickAsync();
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
            await WaitForFirstColumnAsync(page, "Status");

            var afterReload = await HeaderOrderAsync(page);
            Assert.Equal("Status", afterReload[0]);
        }
        finally
        {
            await AuditDataSeeder.DeleteByTargetPrefixAsync(targetPrefix);
        }
    }

    [SkippableFact]
    public async Task ColumnOrderAndWidths_PersistAcrossReload_ViaSessionStorage()
    {
        Skip.IfNot(await AuditDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason);

        var runId = Guid.NewGuid().ToString("N");
        var targetPrefix = $"playwright-test/grid-persist/{runId}/";
        var eventId = Guid.NewGuid();

        try
        {
            var page = await OpenGridWithSeededRowAsync(targetPrefix, eventId);

            // Reorder then resize, then confirm sessionStorage carries both.
            await page.Locator("[data-col-key='Status']")
                .DragToAsync(page.Locator("[data-col-key='OccurredAtUtc']"));
            // Wait for the reorder re-render to settle before measuring the
            // resize handle, so the handle's bounding box is read off the
            // post-reorder layout.
            await WaitForFirstColumnAsync(page, "Status");

            var handle = page.Locator("[data-test='col-resize-Target']");
            var handleBox = await handle.BoundingBoxAsync();
            Assert.NotNull(handleBox);
            var startX = handleBox!.X + handleBox.Width / 2;
            var startY = handleBox.Y + handleBox.Height / 2;
            await page.Mouse.MoveAsync(startX, startY);
            await page.Mouse.DownAsync();
            await page.Mouse.MoveAsync(startX + 90, startY, new MouseMoveOptions { Steps = 6 });
            await page.Mouse.UpAsync();

            // Both keys are written under the auditGrid: namespace — but the
            // write is asynchronous: pointer-up fires a fire-and-forget
            // OnColumnResized/OnColumnReordered JS→.NET invoke, and the .NET
            // handler then round-trips back through JS interop to call
            // auditGrid.save. Reading sessionStorage synchronously right after
            // Mouse.UpAsync races that round-trip, so poll for both keys to
            // land before asserting on them.
            await WaitForStorageKeyAsync(page, "auditGrid:columnOrder");
            await WaitForStorageKeyAsync(page, "auditGrid:columnWidths");
            var orderJson = await page.EvaluateAsync<string?>(
                "() => sessionStorage.getItem('auditGrid:columnOrder')");
            var widthsJson = await page.EvaluateAsync<string?>(
                "() => sessionStorage.getItem('auditGrid:columnWidths')");
            Assert.NotNull(orderJson);
            Assert.Contains("Status", orderJson!);
            Assert.NotNull(widthsJson);
            Assert.Contains("Target", widthsJson!);

            // After a reload the restored grid reflects the stored order. The
            // restore happens on the grid's first render (LoadPersistedState →
            // StateHasChanged), so wait for the header to reflect it.
            await page.ReloadAsync();
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
            await page.Locator("[data-test='filter-apply']").ClickAsync();
            await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
            await WaitForFirstColumnAsync(page, "Status");

            var restoredOrder = await HeaderOrderAsync(page);
            Assert.Equal("Status", restoredOrder[0]);
        }
        finally
        {
            await AuditDataSeeder.DeleteByTargetPrefixAsync(targetPrefix);
        }
    }
}
