using System.Security.Claims;
using ZB.MOM.WW.ScadaBridge.Security;
using Akka.Actor;
using Bunit;
using Bunit.TestDoubles;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
using ZB.MOM.WW.ScadaBridge.Communication;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.Transport;
using SiteCallsReportPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.SiteCalls.SiteCallsReport;
using TransportImportPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Design.TransportImport;

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

/// <summary>
/// CentralUI-033: tests for the drill-in / query-string code paths on the two
/// newest pages (TransportImport + SiteCallsReport). The base happy-path cases
/// (Parked, stuck=true, no params) live next to the rest of the page's tests in
/// <c>SiteCallsReportPageTests</c> / <c>TransportImportPageTests</c>; this file
/// fills the remaining gaps the finding called out — unrecognised values, case
/// handling, and the no-query-string default for the Transport wizard.
/// </summary>
public sealed class QueryStringDrillInTests
{
    // STM: CentralUI-033-QueryStringDrillIn marker — used by grep verification.

    // -----------------------------------------------------------------
    // SiteCallsReport — ?status=
    // -----------------------------------------------------------------

    [Fact]
    public void SiteCallsReport_StatusParam_CaseInsensitiveMatch_NormalisesToCanonicalCasing()
    {
        // The dropdown options use canonical casing ("Parked"). The KPI tiles
        // emit canonical, but a hand-crafted ?status=parked URL must still seed
        // the filter — the parser is case-insensitive and the seeded value uses
        // the canonical casing so the <select> can bind it.
        using var ctx = new SiteCallsReportFixture();
        var nav = (BunitNavigationManager)ctx.Services.GetRequiredService<NavigationManager>();
        nav.NavigateTo("/site-calls/report?status=parked");

        var cut = ctx.Render<SiteCallsReportPage>();

        cut.WaitForAssertion(() =>
        {
            Assert.Single(ctx.QueryRequests);
            // Normalised to canonical casing (the dropdown's option text), not
            // the URL's raw "parked".
            Assert.Equal("Parked", ctx.QueryRequests[0].StatusFilter);
        });
    }

    [Fact]
    public void SiteCallsReport_StatusParam_Unrecognised_IsSilentlyDropped()
    {
        // Lax parsing: an unrecognised status value is ignored, leaving the
        // filter empty so the page loads unfiltered. Mirrors AuditLogPage's
        // drill-in convention — a hand-crafted bad URL must not break the page.
        using var ctx = new SiteCallsReportFixture();
        var nav = (BunitNavigationManager)ctx.Services.GetRequiredService<NavigationManager>();
        nav.NavigateTo("/site-calls/report?status=NotARealStatus");

        var cut = ctx.Render<SiteCallsReportPage>();

        cut.WaitForAssertion(() =>
        {
            Assert.Single(ctx.QueryRequests);
            Assert.Null(ctx.QueryRequests[0].StatusFilter);
            Assert.False(ctx.QueryRequests[0].StuckOnly);
        });
    }

    [Fact]
    public void SiteCallsReport_StuckParam_NonBoolean_IsSilentlyDropped()
    {
        // bool.TryParse fails for "yes"/"1" — the parser drops the value and
        // leaves StuckOnly = false, mirroring the unrecognised-status path.
        using var ctx = new SiteCallsReportFixture();
        var nav = (BunitNavigationManager)ctx.Services.GetRequiredService<NavigationManager>();
        nav.NavigateTo("/site-calls/report?stuck=yes");

        var cut = ctx.Render<SiteCallsReportPage>();

        cut.WaitForAssertion(() =>
        {
            Assert.Single(ctx.QueryRequests);
            Assert.False(ctx.QueryRequests[0].StuckOnly);
        });
    }

    // -----------------------------------------------------------------
    // TransportImport — no query-string parameters on this route
    // -----------------------------------------------------------------

    /// <summary>
    /// CentralUI-033: TransportImport.razor declares no <c>[Parameter]</c> /
    /// <c>SupplyParameterFromQuery</c> bindings — the wizard's initial state is
    /// purely <c>ImportWizardStep.Upload</c> regardless of the query-string. This
    /// test pins that contract: navigating with an unrecognised query-string
    /// param does not throw and does not change the initial step.
    /// </summary>
    [Fact]
    public void TransportImport_UnrecognisedQueryStringParam_DoesNotChangeInitialStep()
    {
        using var ctx = new TransportImportFixture();
        var nav = (BunitNavigationManager)ctx.Services.GetRequiredService<NavigationManager>();
        nav.NavigateTo("/design/transport/import?bundleImportId=11111111-1111-1111-1111-111111111111&foo=bar");

        var cut = ctx.Render<TransportImportPage>();

        // The wizard starts at Upload regardless of any drill-in query string —
        // the page has no [Parameter]-bound properties so unknown keys are
        // silently ignored by Blazor's parameter binding.
        var step = (TransportImportPage.ImportWizardStep)typeof(TransportImportPage)
            .GetField("_step",
                System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
            .GetValue(cut.Instance)!;
        Assert.Equal(TransportImportPage.ImportWizardStep.Upload, step);

        // And the Step-1 InputFile control is rendered — the page came up clean.
        Assert.NotNull(cut.Find("input[type='file']"));
    }

    // -----------------------------------------------------------------
    // Test-scoped fixtures — kept inside this file to bound the diff.
    // The existing page-level test files have their own larger fixtures;
    // these copies are intentionally minimal (only what the drill-in
    // tests need).
    // -----------------------------------------------------------------

    private sealed class SiteCallsReportFixture : BunitContext
    {
        private readonly ActorSystem _system = ActorSystem.Create("qs-drillin-tests");

        public readonly CommunicationService Comms;
        public readonly List<SiteCallQueryRequest> QueryRequests = new();

        public SiteCallsReportFixture()
        {
            Comms = new CommunicationService(
                Options.Create(new CommunicationOptions()),
                NullLogger<CommunicationService>.Instance);

            var auditProxy = _system.ActorOf(Props.Create(() => new ScriptedSiteCallAuditActor(this)));
            Comms.SetSiteCallAudit(auditProxy);

            Services.AddSingleton(Comms);
            Services.AddSingleton<IDialogService>(new AlwaysConfirmDialogService());
            // M6 (K14): the SiteCallsReport page now injects IKpiHistoryQueryService
            // for the Trends section. Provide a stand-in so these drill-in tests render.
            Services.AddSingleton(Substitute.For<IKpiHistoryQueryService>());

            var siteRepo = Substitute.For<ISiteRepository>();
            siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>())
                .Returns(Task.FromResult<IReadOnlyList<Site>>(new List<Site>
                {
                    new("Plant A", "plant-a") { Id = 1 },
                }));
            Services.AddSingleton(siteRepo);

            var claims = new[]
            {
                new Claim(JwtTokenService.UsernameClaimType, "tester"),
                new Claim(JwtTokenService.RoleClaimType, "Deployer"),
            };
            var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
            Services.AddSingleton<AuthenticationStateProvider>(new TestAuthStateProvider(user));
            Services.AddAuthorizationCore();
            Services.AddScoped<ZB.MOM.WW.ScadaBridge.CentralUI.Auth.SiteScopeService>();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                _system.Terminate().Wait(TimeSpan.FromSeconds(5));
            }
            base.Dispose(disposing);
        }
    }

    private sealed class ScriptedSiteCallAuditActor : ReceiveActor
    {
        public ScriptedSiteCallAuditActor(SiteCallsReportFixture fixture)
        {
            Receive<SiteCallQueryRequest>(req =>
            {
                fixture.QueryRequests.Add(req);
                Sender.Tell(new SiteCallQueryResponse(
                    req.CorrelationId, true, null,
                    new List<SiteCallSummary>(), null, null));
            });
        }
    }

    private sealed class AlwaysConfirmDialogService : IDialogService
    {
        public Task<bool> ConfirmAsync(string title, string message, bool danger = false)
            => Task.FromResult(true);
        public Task<string?> PromptAsync(string title, string label, string initialValue = "", string? placeholder = null)
            => Task.FromResult<string?>(null);

        public Task<TResult?> ShowAsync<TResult>(
            string title,
            Microsoft.AspNetCore.Components.RenderFragment<DialogContext<TResult>> body,
            string? size = null)
            => Task.FromResult<TResult?>(default);
    }

    private sealed class TransportImportFixture : BunitContext
    {
        public TransportImportFixture()
        {
            JSInterop.Mode = JSRuntimeMode.Loose;

            var importer = Substitute.For<IBundleImporter>();
            Services.AddSingleton(importer);
            Services.AddSingleton(Substitute.For<IAuditService>());
            // M8 E2: TransportImport's Map step injects ISiteRepository.
            Services.AddSingleton(Substitute.For<ISiteRepository>());
            Services.AddSingleton<IOptions<TransportOptions>>(
                Microsoft.Extensions.Options.Options.Create(new TransportOptions
                {
                    MaxBundleSizeMb = 10,
                    MaxUnlockAttemptsPerSession = 3,
                }));

            var dbOptions = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
                .UseSqlite("DataSource=:memory:")
                .ConfigureWarnings(w => w.Ignore(RelationalEventId.AmbientTransactionWarning))
                .Options;
            var dbContext = new ScadaBridgeDbContext(dbOptions);
            dbContext.Database.OpenConnection();
            dbContext.Database.EnsureCreated();
            Services.AddSingleton(dbContext);

            var claims = new List<Claim>
            {
                new(ZB.MOM.WW.ScadaBridge.Security.JwtTokenService.UsernameClaimType, "alice"),
                new(ZB.MOM.WW.ScadaBridge.Security.JwtTokenService.RoleClaimType, "Administrator"),
            };
            var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
            Services.AddSingleton<AuthenticationStateProvider>(new TestAuthStateProvider(principal));
            Services.AddAuthorizationCore();
        }
    }
}
