using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Playwright;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration;

namespace ZB.MOM.WW.OtOpcUa.Admin.E2ETests;

/// <summary>
///     Phase 6.4 UnsTab drag-drop E2E. Task #199 landed the scaffolding; task #242 (this file)
///     drives the Blazor Server interactive circuit through a real drag-drop → confirm-modal
///     → apply flow and a 409 concurrent-edit flow, both via Chromium.
/// </summary>
/// <remarks>
///     <para>
///         <b>Prerequisite.</b> Chromium must be installed locally:
///         <c>pwsh tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/bin/Debug/net10.0/playwright.ps1 install chromium</c>.
///         When the binary is missing the tests <see cref="Assert.Skip"/> rather than fail hard,
///         so CI pipelines that don't run the install step still report green.
///     </para>
///     <para>
///         <b>Harness notes.</b> <see cref="AdminWebAppFactory"/> points the content root at
///         the Admin assembly directory + sets <c>ApplicationName</c> + calls
///         <c>UseStaticWebAssets</c> so <c>/_framework/blazor.web.js</c> + <c>/app.css</c>
///         resolve from the Admin's <c>staticwebassets.development.json</c> manifest (which
///         stitches together Admin <c>wwwroot</c> + the framework NuGet cache). Hubs
///         <c>/hubs/fleet</c> + <c>/hubs/alerts</c> are mapped so <c>ClusterDetail</c>'s
///         <c>HubConnection</c> negotiation doesn't 500 at first render. The InMemory
///         database name is captured as a stable string per fixture instance so the seed
///         scope + Blazor circuit scope + test-assertion scope all share one backing store.
///     </para>
/// </remarks>
[Trait("Category", "E2E")]
public sealed class UnsTabDragDropE2ETests
{
    [Fact]
    public async Task Admin_host_serves_HTTP_via_Playwright_scaffolding()
    {
        await using var app = new AdminWebAppFactory();
        await app.StartAsync();

        var fixture = await TryInitPlaywrightAsync();
        if (fixture is null) return;

        try
        {
            var ctx = await fixture.Browser.NewContextAsync();
            var page = await ctx.NewPageAsync();

            var response = await page.GotoAsync(app.BaseUrl);

            response.ShouldNotBeNull();
            response!.Status.ShouldBeLessThan(500,
                $"Admin host returned HTTP {response.Status} at root — scaffolding broken");

            var body = await page.Locator("body").InnerHTMLAsync();
            body.Length.ShouldBeGreaterThan(0, "empty body = routing pipeline didn't hit Razor");
        }
        finally
        {
            await fixture.DisposeAsync();
        }
    }

    [Fact]
    public async Task Dragging_line_onto_new_area_shows_preview_modal_then_confirms_the_move()
    {
        await using var app = new AdminWebAppFactory();
        await app.StartAsync();

        var fixture = await TryInitPlaywrightAsync();
        if (fixture is null) return;

        try
        {
            var ctx = await fixture.Browser.NewContextAsync();
            var page = await ctx.NewPageAsync();

            await OpenUnsTabAsync(page, app);

            // The seed wires line 'oven-line' to area 'warsaw' (area-a); dragging it onto
            // 'berlin' (area-b) should surface the preview modal. Playwright's DragToAsync
            // dispatches native dragstart / dragover / drop events that the razor's
            // @ondragstart / @ondragover / @ondrop handlers pick up.
            var lineRow = page.Locator("table >> tr", new() { HasTextString = "oven-line" });
            var berlinRow = page.Locator("table >> tr", new() { HasTextString = "berlin" });
            await lineRow.DragToAsync(berlinRow);

            var modalTitle = page.Locator(".modal-title", new() { HasTextString = "Confirm UNS move" });
            await modalTitle.WaitForAsync(new() { Timeout = 10_000 });

            var modalBody = await page.Locator(".modal-body").InnerTextAsync();
            modalBody.ShouldContain("Equipment re-homed",
                customMessage: "preview modal should render UnsImpactAnalyzer summary");

            await page.Locator("button.btn.btn-primary", new() { HasTextString = "Confirm move" })
                .ClickAsync();

            // Modal dismisses after the MoveLineAsync round-trip + ReloadAsync.
            await modalTitle.WaitForAsync(new() { State = WaitForSelectorState.Detached, Timeout = 10_000 });

            // Persisted state: the line row now shows area-b as its Area column value.
            using var scope = app.Services.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<OtOpcUaConfigDbContext>();
            var line = await db.UnsLines.AsNoTracking()
                .FirstAsync(l => l.UnsLineId == "line-a1" && l.GenerationId == app.SeededGenerationId);
            line.UnsAreaId.ShouldBe("area-b",
                "drag-drop should have moved the line to the berlin area via UnsService.MoveLineAsync");
        }
        finally
        {
            await fixture.DisposeAsync();
        }
    }

    [Fact]
    public async Task Preview_shown_then_peer_edit_applied_surfaces_409_conflict_modal()
    {
        await using var app = new AdminWebAppFactory();
        await app.StartAsync();

        var fixture = await TryInitPlaywrightAsync();
        if (fixture is null) return;

        try
        {
            var ctx = await fixture.Browser.NewContextAsync();
            var page = await ctx.NewPageAsync();

            await OpenUnsTabAsync(page, app);

            // Open the preview first (same drag as the happy-path test). The preview captures
            // a RevisionToken under the current draft state.
            var lineRow = page.Locator("table >> tr", new() { HasTextString = "oven-line" });
            var berlinRow = page.Locator("table >> tr", new() { HasTextString = "berlin" });
            await lineRow.DragToAsync(berlinRow);

            var modalTitle = page.Locator(".modal-title", new() { HasTextString = "Confirm UNS move" });
            await modalTitle.WaitForAsync(new() { Timeout = 10_000 });

            // Simulate a concurrent operator committing their own edit between the preview
            // open + our Confirm click — bumps the DraftRevisionToken so our stale token hits
            // DraftRevisionConflictException in UnsService.MoveLineAsync.
            using (var scope = app.Services.CreateScope())
            {
                var uns = scope.ServiceProvider.GetRequiredService<Admin.Services.UnsService>();
                await uns.AddAreaAsync(app.SeededGenerationId, app.SeededClusterId,
                    "madrid", notes: null, CancellationToken.None);
            }

            await page.Locator("button.btn.btn-primary", new() { HasTextString = "Confirm move" })
                .ClickAsync();

            var conflictTitle = page.Locator(".modal-title",
                new() { HasTextString = "Draft changed" });
            await conflictTitle.WaitForAsync(new() { Timeout = 10_000 });

            // Persisted state: line still points at the original area-a — the conflict short-
            // circuited the move.
            using var verifyScope = app.Services.CreateScope();
            var db = verifyScope.ServiceProvider.GetRequiredService<OtOpcUaConfigDbContext>();
            var line = await db.UnsLines.AsNoTracking()
                .FirstAsync(l => l.UnsLineId == "line-a1" && l.GenerationId == app.SeededGenerationId);
            line.UnsAreaId.ShouldBe("area-a",
                "conflict path must not overwrite the peer operator's draft state");
        }
        finally
        {
            await fixture.DisposeAsync();
        }
    }

    private static async Task<PlaywrightFixture?> TryInitPlaywrightAsync()
    {
        try
        {
            var fixture = new PlaywrightFixture();
            await fixture.InitializeAsync();
            return fixture;
        }
        catch (PlaywrightBrowserMissingException)
        {
            Assert.Skip("Chromium not installed. Run playwright.ps1 install chromium.");
            return null;
        }
    }

    /// <summary>
    ///     Navigates to the seeded cluster and switches to the UNS Structure tab, waiting for
    ///     the Blazor Server interactive circuit to render the draggable line table. Returns
    ///     once the drop-target cells ("drop here") are visible — that's the signal the
    ///     circuit is live and @ondragstart handlers are wired.
    /// </summary>
    private static async Task OpenUnsTabAsync(IPage page, AdminWebAppFactory app)
    {
        await page.GotoAsync($"{app.BaseUrl}/clusters/{app.SeededClusterId}",
            new() { WaitUntil = WaitUntilState.NetworkIdle, Timeout = 20_000 });

        var unsTab = page.Locator("button.nav-link", new() { HasTextString = "UNS Structure" });
        await unsTab.WaitForAsync(new() { Timeout = 15_000 });
        await unsTab.ClickAsync();

        // "drop here" is the per-area hint cell — only rendered inside <UnsTab> so its
        // visibility confirms both the tab switch and the circuit's interactive render.
        await page.Locator("td", new() { HasTextString = "drop here" })
            .First.WaitForAsync(new() { Timeout = 15_000 });
    }
}
