using System.Security.Claims;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ScadaLink.CentralUI.Components.Shared;
using ScadaLink.Commons.Entities.Sites;
using ScadaLink.Commons.Interfaces.Repositories;
using DataConnectionsPage = ScadaLink.CentralUI.Components.Pages.Design.DataConnections;

namespace ScadaLink.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>();

    public DataConnectionsPageTests()
    {
        Services.AddSingleton(_siteRepo);
        // 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();

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

    private void AddTestAuth()
    {
        var claims = new[]
        {
            new Claim("Username", "tester"),
            new Claim(ClaimTypes.Role, "Admin")
        };
        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);
    }
}
