using Microsoft.Playwright;

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

/// <summary>
/// Shared fixture that manages the Playwright browser connection.
/// Creates a single browser connection per test collection, reused across all tests.
/// Requires the Playwright Docker container running at ws://localhost:3000.
/// </summary>
public class PlaywrightFixture : IAsyncLifetime
{
    /// <summary>
    /// Playwright Server WebSocket endpoint (Docker container on host port 3000).
    /// </summary>
    private const string PlaywrightWsEndpoint = "ws://localhost:3000";

    /// <summary>
    /// Central UI base URL as seen from inside the Docker network.
    /// The browser runs in the Playwright container, so it uses the Docker hostname.
    /// </summary>
    public const string BaseUrl = "http://scadabridge-traefik";

    /// <summary>Test LDAP credentials (multi-role user with Admin + Design + Deployment).</summary>
    public const string TestUsername = "multi-role";
    public const string TestPassword = "password";

    public IPlaywright Playwright { get; private set; } = null!;
    public IBrowser Browser { get; private set; } = null!;

    /// <summary>
    /// Live browser contexts created by <see cref="NewPageAsync"/>, oldest first.
    /// Capped to <see cref="MaxRetainedContexts"/> so finished tests' contexts are
    /// closed eagerly rather than leaking for the whole run (see <see cref="NewPageAsync"/>).
    /// </summary>
    private readonly List<IBrowserContext> _contexts = new();

    /// <summary>
    /// Maximum number of browser contexts kept open at once. Each context holds a live
    /// Blazor Server SignalR circuit on the Central UI; the full suite runs serially within
    /// the <c>Playwright</c> collection, so contexts from already-finished tests can be closed
    /// safely. Leaving every context open accumulated ~one circuit per test and slowed late
    /// tests into navigation/visibility timeouts. A small cap (covering any single test that
    /// opens more than one page) keeps server-side circuit pressure flat across the run.
    /// </summary>
    private const int MaxRetainedContexts = 4;

    public async Task InitializeAsync()
    {
        Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
        Browser = await Playwright.Chromium.ConnectAsync(PlaywrightWsEndpoint);
    }

    public async Task DisposeAsync()
    {
        await Browser.CloseAsync();
        Playwright.Dispose();
    }

    /// <summary>
    /// Create a new browser context and page. Each test gets an isolated session. Contexts
    /// from already-finished tests are closed eagerly once more than
    /// <see cref="MaxRetainedContexts"/> are open, to bound the number of concurrent Blazor
    /// Server circuits the run holds on the Central UI.
    /// </summary>
    public async Task<IPage> NewPageAsync()
    {
        var context = await Browser.NewContextAsync();
        var page = await context.NewPageAsync();

        List<IBrowserContext> toClose = new();
        lock (_contexts)
        {
            _contexts.Add(context);
            int excess = _contexts.Count - MaxRetainedContexts;
            if (excess > 0)
            {
                toClose = _contexts.GetRange(0, excess);
                _contexts.RemoveRange(0, excess);
            }
        }

        foreach (var old in toClose)
        {
            // Best-effort: a finished test's context may already be gone; never fail a test
            // (or the next page creation) on teardown of a stale context.
            try { await old.CloseAsync(); } catch { /* best-effort */ }
        }

        return page;
    }

    /// <summary>
    /// Create a new page and log in with the default multi-role test user.
    /// </summary>
    public Task<IPage> NewAuthenticatedPageAsync() =>
        NewAuthenticatedPageAsync(TestUsername, TestPassword);

    /// <summary>
    /// Create a new page and log in with specific credentials.
    /// Uses JavaScript fetch() to POST to /auth/login from within the browser,
    /// which sets the auth cookie in the browser context. Then navigates to the dashboard.
    /// </summary>
    public async Task<IPage> NewAuthenticatedPageAsync(string username, string password)
    {
        var page = await NewPageAsync();

        // Navigate to the login page first to establish the origin
        await page.GotoAsync($"{BaseUrl}/login");
        await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);

        // POST to /auth/login via fetch() inside the browser.
        // This sets the auth cookie in the browser context automatically.
        var finalUrl = await page.EvaluateAsync<string>(@"
            async ([u, p]) => {
                const resp = await fetch('/auth/login', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: 'username=' + encodeURIComponent(u) + '&password=' + encodeURIComponent(p),
                    redirect: 'follow'
                });
                return resp.url;
            }
        ", new object[] { username, password });

        if (finalUrl.Contains("/login"))
        {
            throw new InvalidOperationException($"Login failed for '{username}' — redirected back to login: {finalUrl}");
        }

        // Navigate to the dashboard — cookie authenticates us
        await page.GotoAsync(BaseUrl);
        await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

        return page;
    }

    /// <summary>
    /// Wait for Blazor enhanced navigation to update the URL path.
    /// Blazor Server uses SignalR for client-side navigation (no full page reload),
    /// so standard WaitForURLAsync times out. This polls window.location instead.
    /// </summary>
    public static async Task WaitForPathAsync(IPage page, string path, string? excludePath = null, int timeoutMs = 10000)
    {
        var js = excludePath != null
            ? $"() => window.location.pathname.includes('{path}') && !window.location.pathname.includes('{excludePath}')"
            : $"() => window.location.pathname.includes('{path}')";
        await page.WaitForFunctionAsync(js, null, new() { Timeout = timeoutMs });
    }

    /// <summary>
    /// Expand every collapsed sidebar nav section so its links are visible and
    /// clickable. The ZB.MOM.WW.Theme kit renders each section as a native
    /// <c>&lt;details class="rail-section"&gt;</c> whose <c>&lt;summary
    /// class="rail-eyebrow-toggle"&gt;</c> carries an <c>aria-expanded</c> attribute
    /// (rendered from the section's open state at SSR time and kept in sync by the
    /// kit's nav-state.js). Sections are expanded by default, but a section the user
    /// previously collapsed is restored collapsed from localStorage — its links stay
    /// in the DOM but are hidden until the section is expanded. Call this after
    /// authenticating, before interacting with sectioned nav links.
    /// </summary>
    public static async Task ExpandAllNavSectionsAsync(IPage page)
    {
        var toggles = page.Locator("summary.rail-eyebrow-toggle");
        int count = await toggles.CountAsync();
        for (int i = 0; i < count; i++)
        {
            var toggle = toggles.Nth(i);
            if (await toggle.GetAttributeAsync("aria-expanded") == "false")
            {
                await toggle.ClickAsync();
                // Wait for the section's own state to flip (the kit's nav-state.js
                // mirrors the native <details open> onto aria-expanded) before
                // moving to the next section.
                await Assertions.Expect(toggle).ToHaveAttributeAsync("aria-expanded", "true");
            }
        }
    }
}

[CollectionDefinition("Playwright")]
public class PlaywrightCollection : ICollectionFixture<PlaywrightFixture>;
