using System.Security.Claims;
using ZB.MOM.WW.ScadaBridge.Security;
using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using ZB.MOM.WW.ScadaBridge.TemplateEngine;
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services;
using TemplatesPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Design.Templates;

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

/// <summary>
/// bUnit rendering tests for the Templates page that verify the folder/template
/// tree builds the expected DOM for the main shape categories: empty state,
/// folder-containing-template nesting, and composition leaves under their owner.
/// </summary>
public class TemplatesPageTests : BunitContext
{
    private readonly ITemplateEngineRepository _repo = Substitute.For<ITemplateEngineRepository>();
    private readonly IAuditService _audit = Substitute.For<IAuditService>();

    public TemplatesPageTests()
    {
        // The page's TemplateService / TemplateFolderService are constructed via DI
        // from the repository and audit service, mirroring real Host wiring.
        Services.AddSingleton(_repo);
        Services.AddSingleton(_audit);
        Services.AddScoped<TemplateService>();
        Services.AddScoped<TemplateFolderService>();
        // The Templates page injects IDialogService for the new-folder prompt
        // and delete confirmations. The host is rendered in MainLayout, not
        // here, but the DI registration still has to satisfy the [Inject].
        Services.AddScoped<IDialogService, DialogService>();
        AddTestAuth();

        // The TreeView inside the page persists expansion state via JS interop
        // against sessionStorage (`templates-tree` key). bUnit requires explicit
        // stubs for all JS interop calls, otherwise rendering throws.
        JSInterop.Setup<string?>("treeviewStorage.load", _ => true).SetResult(null);
        JSInterop.SetupVoid("treeviewStorage.save", _ => true);
    }

    private void AddTestAuth()
    {
        // The page resolves the current user via the "Username" claim in
        // GetCurrentUserAsync(); supply a stub so OnInitializedAsync doesn't crash.
        var claims = new[]
        {
            new Claim(JwtTokenService.UsernameClaimType, "tester"),
            new Claim(JwtTokenService.RoleClaimType, "Designer")
        };
        var identity = new ClaimsIdentity(claims, "TestAuth");
        var user = new ClaimsPrincipal(identity);
        Services.AddSingleton<AuthenticationStateProvider>(new TestAuthStateProvider(user));
        Services.AddAuthorizationCore();
    }

    [Fact]
    public void Renders_EmptyState_WhenNoTemplatesOrFolders()
    {
        _repo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Template>>(new List<Template>()));
        _repo.GetAllFoldersAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<TemplateFolder>>(new List<TemplateFolder>()));

        var cut = Render<TemplatesPage>();

        Assert.Contains("No templates yet", cut.Markup);
    }

    [Fact]
    public void Renders_FolderAndTemplate_AtCorrectNesting()
    {
        var folder = new TemplateFolder("Dev") { Id = 1 };
        var template = new Template("TestMachine") { Id = 5, FolderId = 1 };
        _repo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Template>>(new List<Template> { template }));
        _repo.GetAllFoldersAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<TemplateFolder>>(new List<TemplateFolder> { folder }));

        var cut = Render<TemplatesPage>();

        // The folder is rendered collapsed; assert the folder label is present,
        // then expand it and assert the nested template label appears.
        Assert.Contains("Dev", cut.Markup);

        var folderToggle = cut.FindAll("li[role='treeitem']")
            .FirstOrDefault(li => li.TextContent.Contains("Dev"))
            ?.QuerySelector(".tv-toggle");
        Assert.NotNull(folderToggle);
        folderToggle!.Click();

        Assert.Contains("TestMachine", cut.Markup);
    }

    [Fact]
    public void Renders_CompositionChildren_UnderOwningTemplate()
    {
        var template = new Template("TestMachine") { Id = 5 };
        template.Compositions.Add(
            new TemplateComposition("DelmiaReceiver") { Id = 10, ComposedTemplateId = 99 });
        var composed = new Template("Other") { Id = 99 };

        _repo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Template>>(new List<Template> { template, composed }));
        _repo.GetAllFoldersAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<TemplateFolder>>(new List<TemplateFolder>()));

        var cut = Render<TemplatesPage>();

        // The owning template must be expanded for its composition leaves to be
        // in the DOM — composition children only render under an expanded parent.
        var ownerToggle = cut.FindAll("li[role='treeitem']")
            .FirstOrDefault(li => li.TextContent.Contains("TestMachine"))
            ?.QuerySelector(".tv-toggle");
        Assert.NotNull(ownerToggle);
        ownerToggle!.Click();

        Assert.Contains("DelmiaReceiver", cut.Markup);
        // The composition glyph appears via Bootstrap Icons; the composed template name
        // is intentionally not rendered on the tree (V7 spec).
        Assert.Contains("bi-arrow-return-right", cut.Markup);
    }

    [Fact]
    public void Renders_InheritedComposition_UnderDerivedComposedMember()
    {
        // followup #9: LeakTest is composed onto the BASE ReactorSide. LeftReactorSide is
        // DERIVED from ReactorSide and composed into CvdReactor, so the tree must surface
        // the INHERITED LeakTest slot under LeftReactorSide — the composition row lives on
        // ReactorSide (no IsInherited placeholder on the child), so the builder must use
        // the effective (own + inherited) composition set, mirroring the resolver.
        var leakTest = new Template("LeakTest") { Id = 50 };
        var reactorSide = new Template("ReactorSide") { Id = 7 };
        reactorSide.Compositions.Add(
            new TemplateComposition("LeakTest") { Id = 100, TemplateId = 7, ComposedTemplateId = 50 });
        var leftReactorSide = new Template("LeftReactorSide") { Id = 8, ParentTemplateId = 7, IsDerived = true };
        var cvdReactor = new Template("CvdReactor") { Id = 1 };
        cvdReactor.Compositions.Add(
            new TemplateComposition("LeftReactorSide") { Id = 200, TemplateId = 1, ComposedTemplateId = 8 });

        _repo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Template>>(
                new List<Template> { cvdReactor, reactorSide, leftReactorSide, leakTest }));
        _repo.GetAllFoldersAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<TemplateFolder>>(new List<TemplateFolder>()));

        var cut = Render<TemplatesPage>();

        // Expand CvdReactor to reveal its composed member LeftReactorSide.
        cut.FindAll("li[role='treeitem']")
            .First(li => li.QuerySelector(".tv-label")?.TextContent.Trim() == "CvdReactor")
            .QuerySelector(".tv-toggle")!.Click();

        // LeftReactorSide must be expandable: before the fix it was a flat leaf with no
        // toggle because its own Compositions are empty (LeakTest is inherited, not owned).
        var leftLi = cut.FindAll("li[role='treeitem']")
            .First(li => li.QuerySelector(".tv-label")?.TextContent.Trim() == "LeftReactorSide");
        var leftToggle = leftLi.QuerySelector(".tv-toggle");
        Assert.NotNull(leftToggle);
        leftToggle!.Click();

        // The inherited LeakTest slot now renders under LeftReactorSide, badged "inherited".
        var leftLiAfter = cut.FindAll("li[role='treeitem']")
            .First(li => li.QuerySelector(".tv-label")?.TextContent.Trim() == "LeftReactorSide");
        Assert.Contains("LeakTest", leftLiAfter.TextContent);
        Assert.Contains("inherited", leftLiAfter.TextContent);
    }

    [Fact]
    public void SearchBox_IsPresentAndBound_ToTemplateFolderTreeFilter()
    {
        // Seed two templates in the same folder so we can confirm the filter narrows
        // the visible set and that clearing the input restores the full tree.
        var folder = new TemplateFolder("Controllers") { Id = 1 };
        var alpha = new Template("AlphaDevice") { Id = 10, FolderId = 1 };
        var beta = new Template("BetaDevice") { Id = 20, FolderId = 1 };

        _repo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Template>>(new List<Template> { alpha, beta }));
        _repo.GetAllFoldersAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<TemplateFolder>>(new List<TemplateFolder> { folder }));

        var cut = Render<TemplatesPage>();

        // 1. A search input must exist in the rendered output.
        var search = cut.Find("input[type='text'][placeholder*='earch']");
        Assert.NotNull(search);

        // 2. Typing a substring hides non-matching templates. The TemplateFolderTree
        //    filter auto-expands ancestors of matches via its _initiallyExpanded hook,
        //    so "AlphaDevice" appears without a manual toggle click. "BetaDevice" is
        //    absent from the filtered tree entirely.
        search.Input("Alpha");

        Assert.Contains("AlphaDevice", cut.Markup);
        Assert.DoesNotContain("BetaDevice", cut.Markup);

        // 3. Clearing the input restores the full tree. The folder may now be collapsed
        //    (prior expansion state is stored per-key; after filter-clear the tree uses
        //    saved state). Expand manually to verify both templates are reachable.
        search.Input("");

        // Both templates must be in the tree; expand the folder if needed.
        var folderToggleAfter = cut.FindAll("li[role='treeitem']")
            .FirstOrDefault(li => li.TextContent.Contains("Controllers"))
            ?.QuerySelector(".tv-toggle");

        // Only click if the folder is not yet expanded (aria-expanded='false').
        var folderLi = cut.FindAll("li[role='treeitem']")
            .FirstOrDefault(li => li.TextContent.Contains("Controllers"));
        if (folderLi?.GetAttribute("aria-expanded") != "true")
            folderToggleAfter?.Click();

        Assert.Contains("AlphaDevice", cut.Markup);
        Assert.Contains("BetaDevice", cut.Markup);
    }

    // ========================================================================
    // M9-T23b: folder sibling reorder menu items + root context menu
    // ========================================================================

    // Seeds two root sibling folders (Alpha @ SortOrder 0, Beta @ SortOrder 1) and
    // wires the repo so TemplateFolderService.ReorderFolderAsync runs end-to-end.
    private (TemplateFolder alpha, TemplateFolder beta) SeedTwoRootSiblings()
    {
        var alpha = new TemplateFolder("Alpha") { Id = 1, ParentFolderId = null, SortOrder = 0 };
        var beta = new TemplateFolder("Beta") { Id = 2, ParentFolderId = null, SortOrder = 1 };
        var all = new List<TemplateFolder> { alpha, beta };

        _repo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Template>>(new List<Template>()));
        _repo.GetAllFoldersAsync(Arg.Any<CancellationToken>())
            .Returns(_ => Task.FromResult<IReadOnlyList<TemplateFolder>>(all));
        _repo.GetFolderByIdAsync(1, Arg.Any<CancellationToken>()).Returns(Task.FromResult<TemplateFolder?>(alpha));
        _repo.GetFolderByIdAsync(2, Arg.Any<CancellationToken>()).Returns(Task.FromResult<TemplateFolder?>(beta));

        return (alpha, beta);
    }

    // Right-clicks the folder row whose label contains the given text, opening the
    // TreeView context menu, then returns the rendered context-menu container.
    private static AngleSharp.Dom.IElement OpenFolderContextMenu(IRenderedComponent<TemplatesPage> cut, string folderLabel)
    {
        var row = cut.FindAll("li[role='treeitem']")
            .First(li => li.TextContent.Contains(folderLabel))
            .QuerySelector(".tv-row")!;
        row.ContextMenu();
        return cut.Find(".dropdown-menu.show");
    }

    [Fact]
    public void FolderContextMenu_ExposesMoveUpAndMoveDown()
    {
        SeedTwoRootSiblings();

        var cut = Render<TemplatesPage>();

        var menu = OpenFolderContextMenu(cut, "Beta");
        var labels = menu.QuerySelectorAll("button.dropdown-item").Select(b => b.TextContent.Trim()).ToList();

        Assert.Contains("Move up", labels);
        Assert.Contains("Move down", labels);
    }

    [Fact]
    public void FolderContextMenu_MoveDown_DispatchesReorderDown_AndReloads()
    {
        var (alpha, beta) = SeedTwoRootSiblings();

        var cut = Render<TemplatesPage>();

        // Move Alpha (first sibling) down -> swaps SortOrder with Beta (the next sibling).
        var menu = OpenFolderContextMenu(cut, "Alpha");
        menu.QuerySelectorAll("button.dropdown-item")
            .First(b => b.TextContent.Trim() == "Move down")
            .Click();

        // Reorder was dispatched the same way other folder commands are — via
        // TemplateFolderService, which resolves + persists both swapped siblings.
        _repo.Received().GetFolderByIdAsync(1, Arg.Any<CancellationToken>());
        _repo.Received().UpdateFolderAsync(alpha, Arg.Any<CancellationToken>());
        _repo.Received().UpdateFolderAsync(beta, Arg.Any<CancellationToken>());
        _repo.Received().SaveChangesAsync(Arg.Any<CancellationToken>());

        // Down on Alpha swapped sort orders: Alpha now after Beta.
        Assert.Equal(1, alpha.SortOrder);
        Assert.Equal(0, beta.SortOrder);

        // Tree reloaded after the mutation: LoadTemplatesAsync re-fetched (initial
        // load + post-reorder reload). GetAllTemplatesAsync is only touched by the
        // page's load path, never by ReorderFolderAsync, so 2 calls == one reload.
        _repo.Received(2).GetAllTemplatesAsync(Arg.Any<CancellationToken>());
    }

    [Fact]
    public void FolderContextMenu_MoveUp_DispatchesReorderUp_AndReloads()
    {
        var (alpha, beta) = SeedTwoRootSiblings();

        var cut = Render<TemplatesPage>();

        // Move Beta (second sibling) up -> swaps SortOrder with Alpha (the previous sibling).
        var menu = OpenFolderContextMenu(cut, "Beta");
        menu.QuerySelectorAll("button.dropdown-item")
            .First(b => b.TextContent.Trim() == "Move up")
            .Click();

        _repo.Received().GetFolderByIdAsync(2, Arg.Any<CancellationToken>());
        _repo.Received().UpdateFolderAsync(alpha, Arg.Any<CancellationToken>());
        _repo.Received().UpdateFolderAsync(beta, Arg.Any<CancellationToken>());

        // Up on Beta swapped sort orders: Beta now before Alpha.
        Assert.Equal(0, beta.SortOrder);
        Assert.Equal(1, alpha.SortOrder);

        // Tree reloaded after the mutation (one reload == GetAllTemplatesAsync twice).
        _repo.Received(2).GetAllTemplatesAsync(Arg.Any<CancellationToken>());
    }

    [Fact]
    public void FolderContextMenu_MoveUp_IsDisabled_OnFirstSibling()
    {
        // Alpha is the first sibling (SortOrder 0) — Move up must be disabled.
        SeedTwoRootSiblings();

        var cut = Render<TemplatesPage>();

        var menu = OpenFolderContextMenu(cut, "Alpha");
        var moveUpBtn = menu.QuerySelectorAll("button.dropdown-item")
            .First(b => b.TextContent.Trim() == "Move up");

        Assert.True(moveUpBtn.HasAttribute("disabled"), "Move up should be disabled for the first sibling");
    }

    [Fact]
    public void FolderContextMenu_MoveDown_IsDisabled_OnLastSibling()
    {
        // Beta is the last sibling (SortOrder 1) — Move down must be disabled.
        SeedTwoRootSiblings();

        var cut = Render<TemplatesPage>();

        var menu = OpenFolderContextMenu(cut, "Beta");
        var moveDownBtn = menu.QuerySelectorAll("button.dropdown-item")
            .First(b => b.TextContent.Trim() == "Move down");

        Assert.True(moveDownBtn.HasAttribute("disabled"), "Move down should be disabled for the last sibling");
    }

    [Fact]
    public void RootContextMenu_OffersNewFolderAndNewTemplate()
    {
        _repo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Template>>(new List<Template>()));
        _repo.GetAllFoldersAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<TemplateFolder>>(new List<TemplateFolder>()));

        var cut = Render<TemplatesPage>();

        // Right-click the root tree zone to open the root context menu.
        cut.Find(".tv-root-zone").ContextMenu();
        var menu = cut.Find(".tv-root-menu");
        var labels = menu.QuerySelectorAll("button.dropdown-item").Select(b => b.TextContent.Trim()).ToList();

        Assert.Contains("New Folder", labels);
        Assert.Contains("New Template", labels);
    }

    [Fact]
    public void RootContextMenu_NewTemplate_NavigatesToRootCreate()
    {
        _repo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Template>>(new List<Template>()));
        _repo.GetAllFoldersAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<TemplateFolder>>(new List<TemplateFolder>()));

        var nav = Services.GetRequiredService<NavigationManager>();

        var cut = Render<TemplatesPage>();

        cut.Find(".tv-root-zone").ContextMenu();
        cut.Find(".tv-root-menu")
            .QuerySelectorAll("button.dropdown-item")
            .First(b => b.TextContent.Trim() == "New Template")
            .Click();

        // Root-level New Template navigates with no folderId query — a root create.
        Assert.EndsWith("/design/templates/create", nav.Uri);
    }

}

internal sealed class TestAuthStateProvider : AuthenticationStateProvider
{
    private readonly ClaimsPrincipal _user;
    public TestAuthStateProvider(ClaimsPrincipal user) => _user = user;
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
        => Task.FromResult(new AuthenticationState(_user));
}
