using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Components.Web.HtmlRendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Dashboard;
using ZB.MOM.WW.MxGateway.Server.Dashboard.Components.Pages;
using HtmlRenderer = Microsoft.AspNetCore.Components.Web.HtmlRenderer;

namespace ZB.MOM.WW.MxGateway.Tests.Dashboard;

/// <summary>
/// Renders <see cref="SettingsPage"/> and asserts the two SEC-25 dashboard-ACL options —
/// <c>Dashboard:GroupToTag</c> and <c>Dashboard:UntaggedSessionVisibility</c> — reach the page.
/// </summary>
/// <remarks>
/// <para>
/// The provider tests one folder over prove the projection carries the values; they would stay
/// green with no row on the page at all. An operator debugging why a Viewer sees no sessions
/// reads the settings page, not the record, so the markup is where the evidence has to be.
/// </para>
/// <para>
/// Tag NAMES are configuration, like the group → role mapping rendered beside them. No tag VALUE
/// is involved, so nothing here is subject to the value-redaction rule.
/// </para>
/// <para>
/// Static rendering via the framework's <see cref="HtmlRenderer"/>, the idiom used by
/// <c>SecretsNavRenderTests</c> and <c>AlarmsPageTruncationBannerTests</c> — the assertion is
/// about markup the server emits, so no component-testing dependency is warranted.
/// </para>
/// </remarks>
public sealed class SettingsPageTagVisibilityRenderTests
{
    private const string EmptyMarker = "(none configured)";

    /// <summary>A configured mapping renders its group and every tag, plus the visibility policy.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task SettingsPage_WhenTagsConfigured_RendersGroupsTagsAndVisibility()
    {
        string html = await RenderAsync(new DashboardOptions
        {
            GroupToTag = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
            {
                ["GwOps"] = ["team-a", "team-b"],
            },
            UntaggedSessionVisibility = UntaggedSessionVisibility.AllViewers,
        });

        Assert.Contains("GwOps", html, StringComparison.Ordinal);
        // Both tags, not just the first: a row that rendered only the head of the array would be
        // actively misleading about which sessions a group can observe.
        Assert.Contains("team-a", html, StringComparison.Ordinal);
        Assert.Contains("team-b", html, StringComparison.Ordinal);
        Assert.Contains(nameof(UntaggedSessionVisibility.AllViewers), html, StringComparison.Ordinal);
    }

    /// <summary>
    /// The unconfigured deployment — the common one, and the one whose Viewers see nothing. The
    /// row must still render, saying so, and the fail-closed default must be on the page. The
    /// role-mapping heading is the control that keeps this from passing over a blank page.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task SettingsPage_WhenNoTagsConfigured_RendersTheEmptyStateAndDefaultVisibility()
    {
        string html = await RenderAsync(new DashboardOptions());

        Assert.Contains("Dashboard tag mapping", html, StringComparison.Ordinal);
        Assert.Contains(EmptyMarker, html, StringComparison.Ordinal);
        Assert.Contains(nameof(UntaggedSessionVisibility.AdminOnly), html, StringComparison.Ordinal);
        Assert.Contains("Dashboard role mapping", html, StringComparison.Ordinal);
    }

    /// <summary>
    /// <c>RecentFaultLimit</c> and <c>RecentSessionLimit</c> reach the page, each value landing in
    /// the row its own label names — not merely present somewhere in the document. Non-default
    /// values prove the projection is plumbed through, not just that the defaults happen to render.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task SettingsPage_RendersRecentFaultAndSessionLimits()
    {
        string html = await RenderAsync(new DashboardOptions
        {
            RecentFaultLimit = 123,
            RecentSessionLimit = 456,
        });

        // Combined label+value fragments, matching the razor's exact markup (no whitespace between
        // tags): a bare Contains on "123"/"456" would pass even if the values landed in the wrong
        // row, or any other row on the page.
        Assert.Contains("""<th scope="row">Recent fault limit</th><td>123</td>""", html, StringComparison.Ordinal);
        Assert.Contains("""<th scope="row">Recent session limit</th><td>456</td>""", html, StringComparison.Ordinal);
    }

    private static async Task<string> RenderAsync(DashboardOptions dashboard)
    {
        EffectiveGatewayConfiguration configuration =
            new GatewayConfigurationProvider(Options.Create(new GatewayOptions { Dashboard = dashboard }))
                .GetEffectiveConfiguration();

        ServiceCollection services = new();
        services.AddLogging();
        services.AddSingleton<IDashboardSnapshotService>(new StubSnapshotService(configuration));
        services.AddSingleton<IDashboardSnapshotFeed>(new IdleSnapshotFeed());

        await using ServiceProvider provider = services.BuildServiceProvider();
        await using HtmlRenderer renderer = new(
            provider,
            provider.GetRequiredService<ILoggerFactory>());

        return await renderer.Dispatcher.InvokeAsync(async () =>
        {
            HtmlRootComponent output = await renderer.RenderComponentAsync<SettingsPage>();
            return output.ToHtmlString();
        });
    }

    // Seeds the page's first (and only) render. Everything except Configuration is inert here.
    private sealed class StubSnapshotService(EffectiveGatewayConfiguration configuration)
        : IDashboardSnapshotService
    {
        /// <inheritdoc />
        public DashboardSnapshot GetSnapshot() => new(
            GeneratedAt: DateTimeOffset.UnixEpoch,
            GatewayStartedAt: DateTimeOffset.UnixEpoch,
            GatewayUptime: TimeSpan.Zero,
            GatewayStatus: "Healthy",
            GatewayVersion: "test",
            Sessions: [],
            Workers: [],
            Metrics: [],
            Faults: [],
            ApiKeys: [],
            Configuration: configuration,
            Galaxy: null!);

        /// <inheritdoc />
        public IAsyncEnumerable<DashboardSnapshot> WatchSnapshotsAsync(CancellationToken cancellationToken) =>
            new IdleSnapshotFeed().WatchAsync(cancellationToken);
    }

    // Parks until the page is disposed, so the base page's watch loop neither spins nor pushes a
    // second snapshot mid-assertion.
    private sealed class IdleSnapshotFeed : IDashboardSnapshotFeed
    {
        /// <inheritdoc />
        public async IAsyncEnumerable<DashboardSnapshot> WatchAsync(
            [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            await Task.Delay(Timeout.Infinite, cancellationToken).ConfigureAwait(false);

            yield break;
        }
    }
}
