using System.Security.Claims;
using ZB.MOM.WW.ScadaBridge.Security;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
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.Types.Enums;
using DataConnectionsPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Design.DataConnections;

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

/// <summary>
/// bUnit rendering tests for the Connections page (Site → DataConnection tree).
/// Focuses on the Topology-style behaviors layered onto this page: always-show-empty
/// sites, search dimming, toolbar gating, and dual-route declaration.
/// </summary>
public class DataConnectionsPageTests : BunitContext
{
    private readonly ISiteRepository _siteRepo = Substitute.For<ISiteRepository>();
    // M9-T25: the design page now injects IConnectionHealthQueryService to render
    // per-connection live health badges. A substitute defaulting to an empty map
    // keeps every existing test green (no badge / unknown state when no report).
    private readonly IConnectionHealthQueryService _healthQuery =
        Substitute.For<IConnectionHealthQueryService>();
    // M9-T24b: the page now opens a Move-to-Site dialog that dispatches the move
    // through IDataConnectionMoveService (the guard-running ManagementActor path).
    // Substituted so the page renders without a real ManagementActor in scope.
    private readonly IDataConnectionMoveService _moveService =
        Substitute.For<IDataConnectionMoveService>();

    public DataConnectionsPageTests()
    {
        Services.AddSingleton(_siteRepo);
        Services.AddSingleton(_healthQuery);
        Services.AddSingleton(_moveService);
        // Satisfy the page's [Inject] IDialogService — the host that actually
        // renders the dialog lives in MainLayout, not in bUnit's render scope.
        Services.AddScoped<IDialogService, DialogService>();
        AddTestAuth();

        // Default: no health known. Tests that assert badge state override this.
        _healthQuery.GetConnectionHealthAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyDictionary<int, ConnectionHealth>>(
                new Dictionary<int, ConnectionHealth>()));

        JSInterop.Setup<string?>("treeviewStorage.load", _ => true).SetResult(null);
        JSInterop.SetupVoid("treeviewStorage.save", _ => true);
    }

    private void AddTestAuth()
    {
        var claims = new[]
        {
            new Claim(JwtTokenService.UsernameClaimType, "tester"),
            new Claim(JwtTokenService.RoleClaimType, "Administrator")
        };
        var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
        Services.AddSingleton<AuthenticationStateProvider>(new TestAuthStateProvider(user));
        Services.AddAuthorizationCore();
    }

    private void SeedRepos(
        IEnumerable<Site>? sites = null,
        IEnumerable<DataConnection>? connections = null)
    {
        _siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Site>>(sites?.ToList() ?? new List<Site>()));
        _siteRepo.GetAllDataConnectionsAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<DataConnection>>(connections?.ToList() ?? new List<DataConnection>()));
    }

    private static AngleSharp.Dom.IElement? FindToggleForLabel(IRenderedComponent<DataConnectionsPage> cut, string label) =>
        cut.FindAll(".tv-row")
            .FirstOrDefault(row => row.QuerySelector(".tv-label")?.TextContent == label)
            ?.QuerySelector(".tv-toggle");

    [Fact]
    public void Renders_EmptyState_WhenNoSites()
    {
        SeedRepos();

        var cut = Render<DataConnectionsPage>();

        Assert.Contains("No sites configured", cut.Markup);
    }

    [Fact]
    public void Renders_EmptySite_AsTopLevelNode()
    {
        // A site with no connections must still appear so it can be right-clicked
        // to "Add Connection here".
        SeedRepos(sites: new[] { new Site("Plant-A", "plant-a") { Id = 1 } });

        var cut = Render<DataConnectionsPage>();

        Assert.Contains("Plant-A", cut.Markup);
    }

    [Fact]
    public void Renders_SiteConnection_Nesting()
    {
        SeedRepos(
            sites: new[] { new Site("Plant-A", "plant-a") { Id = 1 } },
            connections: new[]
            {
                new DataConnection("PLC-1", "OpcUa", 1) { Id = 100 }
            });

        var cut = Render<DataConnectionsPage>();
        FindToggleForLabel(cut, "Plant-A")!.Click();

        Assert.Contains("PLC-1", cut.Markup);
        Assert.Contains("OpcUa", cut.Markup);
    }

    [Fact]
    public void Search_DimsNonMatches_PreservesShape()
    {
        SeedRepos(
            sites: new[]
            {
                new Site("Plant-A", "plant-a") { Id = 1 },
                new Site("Plant-B", "plant-b") { Id = 2 }
            },
            connections: new[]
            {
                new DataConnection("PLC-1", "OpcUa", 1) { Id = 100 },
                new DataConnection("RTU-9", "Custom", 2) { Id = 200 }
            });

        var cut = Render<DataConnectionsPage>();

        var search = cut.Find("input[type='text']");
        search.Input("Plant-A");

        // Both sites remain in the DOM (shape preserved). Plant-B gets the dim style.
        Assert.Contains("Plant-A", cut.Markup);
        Assert.Contains("Plant-B", cut.Markup);
        var dimmedNodes = cut.FindAll("span.tv-label[style*='opacity']");
        Assert.Contains(dimmedNodes, n => n.TextContent.Contains("Plant-B"));
    }

    [Fact]
    public void AddConnectionButton_DisabledUntilSiteSelected()
    {
        SeedRepos(sites: new[] { new Site("Plant-A", "plant-a") { Id = 1 } });

        var cut = Render<DataConnectionsPage>();

        var addBtn = cut.FindAll("button")
            .First(b => b.TextContent.Contains("+ Connection"));
        Assert.True(addBtn.HasAttribute("disabled"));

        // Click the site content (TreeView wires selection on .tv-content).
        var siteContent = cut.FindAll(".tv-row")
            .First(r => r.QuerySelector(".tv-label")?.TextContent == "Plant-A")
            .QuerySelector(".tv-content")!;
        siteContent.Click();

        var addBtnAfter = cut.FindAll("button")
            .First(b => b.TextContent.Contains("+ Connection"));
        Assert.False(addBtnAfter.HasAttribute("disabled"));
    }

    [Fact]
    public void DataConnectionsRoutes_AreDeclaredOnListPage()
    {
        // The page moved from Admin to Design; both the canonical
        // /design/connections route and the /design/data-connections alias
        // must resolve to the list page.
        var routes = typeof(DataConnectionsPage).GetCustomAttributes(
            typeof(Microsoft.AspNetCore.Components.RouteAttribute), inherit: false)
            .Cast<Microsoft.AspNetCore.Components.RouteAttribute>()
            .Select(a => a.Template)
            .ToList();

        Assert.Contains("/design/connections", routes);
        Assert.Contains("/design/data-connections", routes);
    }

    [Fact]
    public void ConnectionNode_RendersLiveHealthBadge_MatchingStatus()
    {
        // M9-T25: a connection whose live health is Connected gets the success
        // health badge next to its node; a Disconnected one gets the danger badge.
        SeedRepos(
            sites: new[] { new Site("Plant-A", "plant-a") { Id = 1 } },
            connections: new[]
            {
                new DataConnection("PLC-1", "OpcUa", 1) { Id = 100 },
                new DataConnection("RTU-9", "Custom", 1) { Id = 200 }
            });
        _healthQuery.GetConnectionHealthAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyDictionary<int, ConnectionHealth>>(
                new Dictionary<int, ConnectionHealth>
                {
                    [100] = ConnectionHealth.Connected,
                    [200] = ConnectionHealth.Disconnected,
                }));

        var cut = Render<DataConnectionsPage>();
        FindToggleForLabel(cut, "Plant-A")!.Click();

        // Each connection node carries a status badge keyed by connection id so the
        // assertion targets the right node regardless of tree ordering.
        var connectedBadge = cut.Find("[data-test='conn-health-100']");
        Assert.Contains("bg-success", connectedBadge.GetAttribute("class"));
        Assert.Contains("Connected", connectedBadge.TextContent);

        var disconnectedBadge = cut.Find("[data-test='conn-health-200']");
        Assert.Contains("bg-danger", disconnectedBadge.GetAttribute("class"));
        Assert.Contains("Disconnected", disconnectedBadge.TextContent);
    }

    [Fact]
    public void ConnectionNode_Has_MoveToSiteAction()
    {
        // M9-T24b: each connection node exposes a "Move to Site…" action alongside
        // Edit/Delete.
        SeedRepos(
            sites: new[]
            {
                new Site("Plant-A", "plant-a") { Id = 1 },
                new Site("Plant-B", "plant-b") { Id = 2 }
            },
            connections: new[] { new DataConnection("PLC-1", "OpcUa", 1) { Id = 100 } });

        var cut = Render<DataConnectionsPage>();
        FindToggleForLabel(cut, "Plant-A")!.Click();

        Assert.Contains(cut.FindAll("button"),
            b => b.TextContent.Contains("Move to Site"));
    }

    [Fact]
    public void MoveToSite_OpensDialog_ExcludingCurrentSite()
    {
        // M9-T24b: clicking "Move to Site…" opens the move dialog whose target-site
        // picker omits the connection's current site (Plant-A) and lists the others.
        SeedRepos(
            sites: new[]
            {
                new Site("Plant-A", "plant-a") { Id = 1 },
                new Site("Plant-B", "plant-b") { Id = 2 },
                new Site("Plant-C", "plant-c") { Id = 3 }
            },
            connections: new[] { new DataConnection("PLC-1", "OpcUa", 1) { Id = 100 } });

        // T33b: the move dialog is now opened via IDialogService.ShowAsync and rendered
        // by the shared DialogHost (which lives in MainLayout in production). Render a
        // host in the same DI scope so the dialog the page opens is displayed; the host's
        // focus-trap JS interop runs as no-ops under loose mode.
        JSInterop.Mode = JSRuntimeMode.Loose;
        var host = Render<DialogHost>();

        var cut = Render<DataConnectionsPage>();
        FindToggleForLabel(cut, "Plant-A")!.Click();

        cut.FindAll("button").First(b => b.TextContent.Contains("Move to Site")).Click();
        host.Render();

        // The dialog renders with a site picker; the current site is excluded.
        var dialog = host.Find(".modal.show");
        var optionLabels = dialog.QuerySelectorAll("select option")
            .Select(o => o.TextContent).ToList();
        Assert.Contains(optionLabels, l => l.Contains("Plant-B"));
        Assert.Contains(optionLabels, l => l.Contains("Plant-C"));
        Assert.DoesNotContain(optionLabels, l => l.Contains("Plant-A"));
    }
}
