using System.Security.Claims;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.DeploymentManager;
using ZB.MOM.WW.ScadaBridge.Security;
using DeploymentsPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment.Deployments;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;

/// <summary>
/// Coverage for the Deployment Status page's server-side paging, server-computed
/// status tiles and debounced filter input (residual R3).
///
/// <para>
/// Before this change the page read EVERY deployment record and EVERY instance on
/// each load, then site-scoped, sorted, counted the four tiles and sliced a 25-row
/// page in the Blazor circuit's memory. These tests pin the replacement contract:
/// the page asks the repository for ONE page plus the filtered total, asks for the
/// tile counts separately, pushes the site-scope grant into the query rather than
/// intersecting in memory, and never touches the instance repository.
/// </para>
/// </summary>
public class DeploymentsServerPagingTests : BunitContext
{
    private IDeploymentManagerRepository _deployRepo = null!;
    private ITemplateEngineRepository _templateRepo = null!;
    private DeploymentStatusNotifier _notifier = null!;

    /// <summary>Records every filter/page/pageSize triple the page queried with.</summary>
    private readonly List<(DeploymentListFilter Filter, int Page, int PageSize)> _pageCalls = new();

    private void RegisterServices(
        DeploymentListPage? page = null,
        DeploymentStatusCounts? counts = null,
        string[]? siteClaims = null)
    {
        _deployRepo = Substitute.For<IDeploymentManagerRepository>();
        _templateRepo = Substitute.For<ITemplateEngineRepository>();
        _notifier = new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance);

        var resultPage = page ?? DeploymentListPage.Empty;

        _deployRepo.QueryDeploymentListPageAsync(
                Arg.Any<DeploymentListFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(ci =>
            {
                _pageCalls.Add((ci.ArgAt<DeploymentListFilter>(0), ci.ArgAt<int>(1), ci.ArgAt<int>(2)));
                return Task.FromResult(resultPage);
            });

        _deployRepo.GetDeploymentStatusCountsAsync(
                Arg.Any<DeploymentListFilter>(), Arg.Any<CancellationToken>())
            .Returns(counts ?? DeploymentStatusCounts.Empty);

        Services.AddSingleton(_deployRepo);
        Services.AddSingleton(_templateRepo);
        Services.AddSingleton<IDeploymentStatusNotifier>(_notifier);

        var claims = new List<Claim> { new(ClaimTypes.Name, "deployer") };
        foreach (var siteId in siteClaims ?? Array.Empty<string>())
        {
            claims.Add(new Claim(JwtTokenService.SiteIdClaimType, siteId));
        }

        var identity = new ClaimsIdentity(claims, "TestCookie");
        var stubAuth = new StubAuthStateProvider(
            new AuthenticationState(new ClaimsPrincipal(identity)));
        Services.AddSingleton<AuthenticationStateProvider>(stubAuth);
        Services.AddScoped(_ => new SiteScopeService(stubAuth));
    }

    private sealed class StubAuthStateProvider : AuthenticationStateProvider
    {
        private readonly AuthenticationState _state;
        public StubAuthStateProvider(AuthenticationState state) => _state = state;
        public override Task<AuthenticationState> GetAuthenticationStateAsync()
            => Task.FromResult(_state);
    }

    private static DeploymentListRow Row(int id, string instance, DeploymentStatus status = DeploymentStatus.Success) =>
        new(
            Id: id,
            DeploymentId: $"dep-{id:D4}",
            InstanceId: id,
            InstanceUniqueName: instance,
            SiteId: 1,
            Status: status,
            RevisionHash: $"rev{id:D4}hash",
            DeployedBy: "deployer",
            DeployedAt: DateTimeOffset.UtcNow.AddMinutes(-id),
            CompletedAt: DateTimeOffset.UtcNow,
            ErrorMessage: null);

    // ── Paging ────────────────────────────────────────────────────────────────

    [Fact]
    public void InitialLoad_RequestsPageOneFromTheServer_AndNeverReadsEveryInstance()
    {
        RegisterServices(new DeploymentListPage(new[] { Row(1, "Inst-1") }, TotalCount: 1));

        Render<DeploymentsPage>();

        var call = Assert.Single(_pageCalls);
        Assert.Equal(1, call.Page);
        Assert.Equal(25, call.PageSize);

        // The full-instance-table read that used to build the name/site maps is gone.
        _templateRepo.DidNotReceiveWithAnyArgs().GetAllInstancesAsync();
    }

    [Fact]
    public void TotalCountFromServer_DrivesThePager_NotTheRowsOnScreen()
    {
        // One page of 25 rows out of 130 total => 6 pages. Under the old
        // client-materialized model the pager could only ever see the rows it held.
        var rows = Enumerable.Range(1, 25).Select(i => Row(i, $"Inst-{i}")).ToList();
        RegisterServices(new DeploymentListPage(rows, TotalCount: 130));

        var cut = Render<DeploymentsPage>();

        Assert.Contains("130 matching deployments", cut.Find("[data-test=\"deploy-result-count\"]").TextContent);

        // Windowed pager: first, last and a radius around the current page.
        var pageButtons = cut.FindAll("ul.pagination li.page-item").Count;
        Assert.True(pageButtons > 2, "Pager must render page buttons, not just Previous/Next.");
        Assert.Contains("6", cut.Find("ul.pagination").TextContent);
    }

    [Fact]
    public void NextPage_RequeriesTheServerForPageTwo()
    {
        var rows = Enumerable.Range(1, 25).Select(i => Row(i, $"Inst-{i}")).ToList();
        RegisterServices(new DeploymentListPage(rows, TotalCount: 130));

        var cut = Render<DeploymentsPage>();
        _pageCalls.Clear();

        cut.Find("[data-test=\"deploy-page-next\"]").Click();

        var call = Assert.Single(_pageCalls);
        Assert.Equal(2, call.Page);
    }

    [Fact]
    public void SinglePageResult_RendersNoPager()
    {
        RegisterServices(new DeploymentListPage(new[] { Row(1, "Inst-1") }, TotalCount: 1));

        var cut = Render<DeploymentsPage>();

        Assert.Empty(cut.FindAll("ul.pagination"));
    }

    // ── Status tiles ──────────────────────────────────────────────────────────

    [Fact]
    public void Tiles_RenderServerComputedCounts_NotCountsOfTheVisiblePage()
    {
        // The page holds ONE row, but the server reports fleet-wide counts. Under
        // the old model the tiles were Count() over the in-memory list, so paging
        // would have silently reduced them to per-page counts.
        RegisterServices(
            new DeploymentListPage(new[] { Row(1, "Inst-1") }, TotalCount: 400),
            new DeploymentStatusCounts(Pending: 7, InProgress: 3, Success: 380, Failed: 10));

        var cut = Render<DeploymentsPage>();

        Assert.Contains("7", cut.Find("[data-test=\"deploy-tile-pending\"]").TextContent);
        Assert.Contains("3", cut.Find("[data-test=\"deploy-tile-inprogress\"]").TextContent);
        Assert.Contains("380", cut.Find("[data-test=\"deploy-tile-success\"]").TextContent);
        Assert.Contains("10", cut.Find("[data-test=\"deploy-tile-failed\"]").TextContent);
        Assert.Contains("400", cut.Find("[data-test=\"deploy-tile-all\"]").TextContent);
    }

    [Fact]
    public void ClickingAStatusTile_AppliesTheFilterServerSide_AndResetsToPageOne()
    {
        var rows = Enumerable.Range(1, 25).Select(i => Row(i, $"Inst-{i}")).ToList();
        RegisterServices(new DeploymentListPage(rows, TotalCount: 130));

        var cut = Render<DeploymentsPage>();
        cut.Find("[data-test=\"deploy-page-next\"]").Click();
        _pageCalls.Clear();

        cut.Find("[data-test=\"deploy-tile-failed\"]").Click();

        var call = Assert.Single(_pageCalls);
        Assert.Equal(DeploymentStatus.Failed, call.Filter.Status);
        Assert.Equal(1, call.Page);
        Assert.Equal("true", cut.Find("[data-test=\"deploy-tile-failed\"]").GetAttribute("aria-pressed"));
    }

    [Fact]
    public void ClickingTheActiveTileAgain_ClearsTheStatusFilter()
    {
        RegisterServices(new DeploymentListPage(new[] { Row(1, "Inst-1") }, TotalCount: 1));
        var cut = Render<DeploymentsPage>();

        cut.Find("[data-test=\"deploy-tile-pending\"]").Click();
        _pageCalls.Clear();
        cut.Find("[data-test=\"deploy-tile-pending\"]").Click();

        var call = Assert.Single(_pageCalls);
        Assert.Null(call.Filter.Status);
        Assert.Equal("true", cut.Find("[data-test=\"deploy-tile-all\"]").GetAttribute("aria-pressed"));
    }

    // ── Site scoping ──────────────────────────────────────────────────────────

    [Fact]
    public void SystemWideUser_QueriesWithNoSiteScope()
    {
        RegisterServices();

        Render<DeploymentsPage>();

        Assert.Null(Assert.Single(_pageCalls).Filter.SiteIdScope);
    }

    [Fact]
    public void SiteScopedUser_PushesThePermittedSiteIdsIntoTheQuery()
    {
        RegisterServices(siteClaims: new[] { "3", "7" });

        Render<DeploymentsPage>();

        var scope = Assert.Single(_pageCalls).Filter.SiteIdScope;
        Assert.NotNull(scope);
        Assert.Equal(new[] { 3, 7 }, scope!.OrderBy(x => x));
    }

    // ── Filter debounce ───────────────────────────────────────────────────────

    [Fact]
    public void SearchInput_IsDebounced_SoABurstOfKeystrokesIssuesOneQuery()
    {
        RegisterServices(new DeploymentListPage(new[] { Row(1, "Inst-1") }, TotalCount: 1));
        var cut = Render<DeploymentsPage>();
        _pageCalls.Clear();

        // Type "Reactor" one character at a time, well inside the 500ms window.
        const string term = "Reactor";
        for (var i = 1; i <= term.Length; i++)
        {
            cut.Find("[data-test=\"deploy-search\"]").Input(term[..i]);
        }

        // No query yet — the trailing debounce has not elapsed.
        Assert.Empty(_pageCalls);

        cut.WaitForAssertion(() => Assert.NotEmpty(_pageCalls), TimeSpan.FromSeconds(3));
        Thread.Sleep(300);

        var call = Assert.Single(_pageCalls);
        Assert.Equal(term, call.Filter.Search);
        Assert.Equal(1, call.Page);
    }

    [Fact]
    public void ClearFilters_ResetsSearchAndStatus_AndRequeries()
    {
        RegisterServices(new DeploymentListPage(new[] { Row(1, "Inst-1") }, TotalCount: 1));
        var cut = Render<DeploymentsPage>();

        cut.Find("[data-test=\"deploy-tile-failed\"]").Click();
        _pageCalls.Clear();

        cut.Find("[data-test=\"deploy-clear-filters\"]").Click();

        var call = Assert.Single(_pageCalls);
        Assert.Null(call.Filter.Status);
        Assert.Null(call.Filter.NormalizedSearch);
        Assert.Equal(1, call.Page);
    }

    [Fact]
    public void ClearFilters_IsEnabledAsSoonAsTheOperatorTypes_NotOnlyOnceTheDebounceFires()
    {
        RegisterServices(new DeploymentListPage(new[] { Row(1, "Inst-1") }, TotalCount: 1));
        var cut = Render<DeploymentsPage>();

        Assert.True(cut.Find("[data-test=\"deploy-clear-filters\"]").HasAttribute("disabled"));

        cut.Find("[data-test=\"deploy-search\"]").Input("Rea");

        // Still inside the 500ms window — the escape hatch must not be dead.
        Assert.False(cut.Find("[data-test=\"deploy-clear-filters\"]").HasAttribute("disabled"));
    }

    [Fact]
    public void DisposeWithAnArmedFilterTimer_DoesNotQuery()
    {
        RegisterServices(new DeploymentListPage(new[] { Row(1, "Inst-1") }, TotalCount: 1));
        var cut = Render<DeploymentsPage>();

        cut.Find("[data-test=\"deploy-search\"]").Input("abc");
        cut.Instance.Dispose();
        _pageCalls.Clear();

        Thread.Sleep(900);

        Assert.Empty(_pageCalls);
    }

    // ── Rendering ─────────────────────────────────────────────────────────────

    [Fact]
    public void InstanceName_ComesFromTheJoinedRow_NotAClientSideLookupTable()
    {
        RegisterServices(new DeploymentListPage(new[] { Row(42, "Line3.Filler") }, TotalCount: 1));

        var cut = Render<DeploymentsPage>();

        Assert.Contains("Line3.Filler", cut.Markup);
    }
}
