using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;

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

/// <summary>
/// bUnit tests for the TreeView component covering core rendering,
/// expand/collapse behavior, ARIA attributes, and indentation.
/// </summary>
public class TreeViewTests : BunitContext
{
    private record TestNode(string Key, string Label, List<TestNode> Children);

    private static List<TestNode> SimpleRoots() => new()
    {
        new("a", "Alpha", new()
        {
            new("a1", "Alpha-1", new()),
            new("a2", "Alpha-2", new()
            {
                new("a2x", "Alpha-2-X", new())
            })
        }),
        new("b", "Beta", new()),
    };

    private IRenderedComponent<TreeView<TestNode>> RenderTreeView(
        List<TestNode>? items = null,
        RenderFragment? emptyContent = null,
        int indentPx = 24,
        Func<TestNode, bool>? initiallyExpanded = null,
        bool selectable = false,
        object? selectedKey = null,
        Action<object?>? onSelectedKeyChanged = null,
        string? selectedCssClass = null,
        string? storageKey = null,
        RenderFragment<TestNode>? contextMenu = null)
    {
        return Render<TreeView<TestNode>>(parameters =>
        {
            parameters
                .Add(p => p.Items, items ?? SimpleRoots())
                .Add(p => p.ChildrenSelector, n => n.Children)
                .Add(p => p.HasChildrenSelector, n => n.Children.Count > 0)
                .Add(p => p.KeySelector, n => n.Key)
                .Add(p => p.NodeContent, node => builder =>
                {
                    builder.AddMarkupContent(0, $"<span class=\"node-label\">{node.Label}</span>");
                })
                .Add(p => p.IndentPx, indentPx)
                .Add(p => p.EmptyContent, emptyContent)
                .Add(p => p.InitiallyExpanded, initiallyExpanded)
                .Add(p => p.Selectable, selectable)
                .Add(p => p.SelectedKey, selectedKey);

            if (onSelectedKeyChanged != null)
            {
                parameters.Add(p => p.SelectedKeyChanged, onSelectedKeyChanged);
            }

            if (selectedCssClass != null)
            {
                parameters.Add(p => p.SelectedCssClass, selectedCssClass);
            }

            if (storageKey != null)
            {
                parameters.Add(p => p.StorageKey, storageKey);
            }

            if (contextMenu != null)
            {
                parameters.Add(p => p.ContextMenu, contextMenu);
            }
        });
    }

    [Fact]
    public void RendersRootLevelItems_WithCorrectLabels()
    {
        var cut = RenderTreeView();

        var labels = cut.FindAll(".node-label");
        // Only root-level items visible (children collapsed)
        Assert.Equal(2, labels.Count);
        Assert.Equal("Alpha", labels[0].TextContent);
        Assert.Equal("Beta", labels[1].TextContent);
    }

    [Fact]
    public void RendersEmptyContent_WhenItemsEmpty()
    {
        var cut = RenderTreeView(
            items: new List<TestNode>(),
            emptyContent: builder =>
            {
                builder.AddMarkupContent(0, "<p class=\"empty-msg\">Nothing here</p>");
            });

        var msg = cut.Find(".empty-msg");
        Assert.Equal("Nothing here", msg.TextContent);
        Assert.Throws<Bunit.ElementNotFoundException>(() => cut.Find("ul[role='tree']"));
    }

    [Fact]
    public void LeafNodes_HaveNoToggle()
    {
        var cut = RenderTreeView();

        // Beta is a leaf (index 1 in the li list)
        var treeItems = cut.FindAll("li[role='treeitem']");
        var betaLi = treeItems[1]; // Beta is second root
        Assert.Throws<Bunit.ElementNotFoundException>(() => betaLi.QuerySelector(".tv-toggle")
            ?? throw new Bunit.ElementNotFoundException(".tv-toggle"));
        // Should have spacer instead
        Assert.NotNull(betaLi.QuerySelector(".tv-spacer"));
    }

    [Fact]
    public void BranchNodes_ShowCollapsedToggle()
    {
        var cut = RenderTreeView();

        var alphaLi = cut.FindAll("li[role='treeitem']")[0];
        Assert.Equal("false", alphaLi.GetAttribute("aria-expanded"));
        var toggle = alphaLi.QuerySelector(".tv-toggle");
        Assert.NotNull(toggle);
        // V2 spec: toggle uses Bootstrap Icons chevron-right; CSS rotates on aria-expanded.
        Assert.NotNull(toggle!.QuerySelector("i.bi.bi-chevron-right"));
    }

    [Fact]
    public void CollapsedBranch_ChildrenNotInDom()
    {
        var cut = RenderTreeView();

        // Alpha is collapsed by default, children should not be in DOM
        var groups = cut.FindAll("ul[role='group']");
        Assert.Empty(groups);
    }

    [Fact]
    public void ClickToggle_ExpandsNode_ShowsChildren()
    {
        var cut = RenderTreeView();

        // Click Alpha's toggle
        var toggle = cut.Find(".tv-toggle");
        toggle.Click();

        // Alpha should now be expanded
        var alphaLi = cut.FindAll("li[role='treeitem']")[0];
        Assert.Equal("true", alphaLi.GetAttribute("aria-expanded"));

        // Children should appear
        var labels = cut.FindAll(".node-label");
        Assert.Contains(labels, l => l.TextContent == "Alpha-1");
        Assert.Contains(labels, l => l.TextContent == "Alpha-2");
    }

    [Fact]
    public void ClickExpandedToggle_Collapses_HidesChildren()
    {
        var cut = RenderTreeView();

        // Expand Alpha
        var toggle = cut.Find(".tv-toggle");
        toggle.Click();

        // Verify children visible
        Assert.Contains(cut.FindAll(".node-label"), l => l.TextContent == "Alpha-1");

        // Collapse Alpha - find the toggle again (DOM changed)
        var toggleAgain = cut.Find(".tv-toggle");
        toggleAgain.Click();

        // Children gone
        var labels = cut.FindAll(".node-label");
        Assert.DoesNotContain(labels, l => l.TextContent == "Alpha-1");
        Assert.Empty(cut.FindAll("ul[role='group']"));
    }

    [Fact]
    public void DeepNesting_ExpandParentThenChild_ShowsGrandchildren()
    {
        var cut = RenderTreeView();

        // Expand Alpha
        cut.Find(".tv-toggle").Click();

        // Now find Alpha-2's toggle (Alpha-2 is a branch)
        var toggles = cut.FindAll(".tv-toggle");
        // toggles[0] = Alpha (now expanded, shows minus), toggles[1] = Alpha-2
        Assert.True(toggles.Count >= 2);
        toggles[1].Click();

        // Alpha-2-X should be visible
        var labels = cut.FindAll(".node-label");
        Assert.Contains(labels, l => l.TextContent == "Alpha-2-X");
    }

    [Fact]
    public void InitiallyExpanded_ExpandsMatchingNodes()
    {
        var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a" || n.Key == "a2");

        // Alpha and Alpha-2 should be expanded, so Alpha-2-X should be visible
        var labels = cut.FindAll(".node-label");
        Assert.Contains(labels, l => l.TextContent == "Alpha-1");
        Assert.Contains(labels, l => l.TextContent == "Alpha-2");
        Assert.Contains(labels, l => l.TextContent == "Alpha-2-X");
    }

    [Fact]
    public void RootUl_HasRoleTree()
    {
        var cut = RenderTreeView();

        var rootUl = cut.Find("ul[role='tree']");
        Assert.NotNull(rootUl);
    }

    [Fact]
    public void NodeLi_HasRoleTreeitem()
    {
        var cut = RenderTreeView();

        var items = cut.FindAll("li[role='treeitem']");
        Assert.Equal(2, items.Count); // Two root nodes
    }

    [Fact]
    public void ExpandedBranch_HasAriaExpandedTrue()
    {
        var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");

        var alphaLi = cut.FindAll("li[role='treeitem']")[0];
        Assert.Equal("true", alphaLi.GetAttribute("aria-expanded"));
    }

    [Fact]
    public void ChildGroup_HasRoleGroup()
    {
        var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");

        var groups = cut.FindAll("ul[role='group']");
        Assert.Single(groups);
    }

    [Fact]
    public void Children_IndentedByIndentPxPerDepth()
    {
        var cut = RenderTreeView(indentPx: 30, initiallyExpanded: n => n.Key == "a" || n.Key == "a2");

        var rows = cut.FindAll(".tv-row");
        // Root nodes at depth 0: padding-left: 0px
        // Children at depth 1: padding-left: 30px
        // Grandchildren at depth 2: padding-left: 60px

        // Find Alpha row (depth 0)
        var alphaRow = rows[0];
        Assert.Contains("padding-left: 0px", alphaRow.GetAttribute("style"));

        // Find Alpha-1 row (depth 1)
        var alpha1Row = rows[1];
        Assert.Contains("padding-left: 30px", alpha1Row.GetAttribute("style"));

        // Find Alpha-2-X row (depth 2) - it's after Alpha-2 at index 3
        var alpha2xRow = rows[3];
        Assert.Contains("padding-left: 60px", alpha2xRow.GetAttribute("style"));
    }

    [Fact]
    public void Selection_Disabled_ClickDoesNotFireCallback()
    {
        object? selected = null;
        var cut = RenderTreeView(selectable: false, onSelectedKeyChanged: k => selected = k);

        cut.Find(".tv-content").Click();

        Assert.Null(selected);
    }

    [Fact]
    public void Selection_Enabled_ClickContentFiresCallback()
    {
        object? selected = null;
        var cut = RenderTreeView(selectable: true, onSelectedKeyChanged: k => selected = k);

        cut.Find(".tv-content").Click();

        Assert.Equal("a", selected);
    }

    [Fact]
    public void Selection_ClickToggle_DoesNotChangeSelection()
    {
        object? selected = null;
        var cut = RenderTreeView(selectable: true, onSelectedKeyChanged: k => selected = k);

        cut.Find(".tv-toggle").Click();

        Assert.Null(selected);
    }

    [Fact]
    public void Selection_SelectedNode_HasCssClass()
    {
        var cut = RenderTreeView(selectable: true, selectedKey: "a");

        var alphaRow = cut.FindAll(".tv-row")[0];
        Assert.Contains("bg-primary", alphaRow.GetAttribute("class"));
    }

    [Fact]
    public void Selection_CustomCssClass_Applied()
    {
        var cut = RenderTreeView(selectable: true, selectedKey: "a", selectedCssClass: "my-highlight");

        var alphaRow = cut.FindAll(".tv-row")[0];
        Assert.Contains("my-highlight", alphaRow.GetAttribute("class"));
    }

    [Fact]
    public void Selection_AriaSelected_SetOnSelectedNode()
    {
        var cut = RenderTreeView(selectable: true, selectedKey: "a");

        var alphaLi = cut.FindAll("li[role='treeitem']")[0];
        Assert.Equal("true", alphaLi.GetAttribute("aria-selected"));
    }

    [Fact]
    public void SessionStorage_NullKey_NoJsInteropCalls()
    {
        var cut = RenderTreeView();

        // Expand Alpha
        cut.Find(".tv-toggle").Click();

        // No JS interop calls should have been made
        Assert.Empty(JSInterop.Invocations);
    }

    [Fact]
    public void SessionStorage_Set_ExpandWritesToStorage()
    {
        JSInterop.Setup<string?>("treeviewStorage.load", _ => true).SetResult(null);
        JSInterop.SetupVoid("treeviewStorage.save", _ => true);

        var cut = RenderTreeView(storageKey: "test-tree");

        // Expand Alpha
        cut.Find(".tv-toggle").Click();

        // Verify save was called
        var saveInvocations = JSInterop.Invocations
            .Where(i => i.Identifier == "treeviewStorage.save")
            .ToList();
        Assert.Single(saveInvocations);
    }

    [Fact]
    public void SessionStorage_RestoresExpandedOnMount()
    {
        JSInterop.Setup<string?>("treeviewStorage.load", _ => true).SetResult("[\"a\"]");
        JSInterop.SetupVoid("treeviewStorage.save", _ => true);

        var cut = RenderTreeView(storageKey: "test-tree");

        // Alpha's children should be visible because "a" was restored from storage
        var labels = cut.FindAll(".node-label");
        Assert.Contains(labels, l => l.TextContent == "Alpha-1");
        Assert.Contains(labels, l => l.TextContent == "Alpha-2");
    }

    [Fact]
    public void SessionStorage_TakesPrecedenceOverInitiallyExpanded()
    {
        // Storage returns empty array — meaning user explicitly collapsed everything
        JSInterop.Setup<string?>("treeviewStorage.load", _ => true).SetResult("[]");
        JSInterop.SetupVoid("treeviewStorage.save", _ => true);

        var cut = RenderTreeView(
            storageKey: "test-tree",
            initiallyExpanded: n => n.Key == "a");

        // Alpha should NOT be expanded — storage (empty) wins over InitiallyExpanded
        var labels = cut.FindAll(".node-label");
        Assert.DoesNotContain(labels, l => l.TextContent == "Alpha-1");
    }

    [Fact]
    public void ExpandAll_ExpandsAllBranches()
    {
        var cut = RenderTreeView();

        // Everything collapsed initially
        Assert.Equal(2, cut.FindAll(".node-label").Count);

        cut.InvokeAsync(() => cut.Instance.ExpandAll());

        var labels = cut.FindAll(".node-label");
        Assert.Contains(labels, l => l.TextContent == "Alpha-1");
        Assert.Contains(labels, l => l.TextContent == "Alpha-2");
        Assert.Contains(labels, l => l.TextContent == "Alpha-2-X");
    }

    [Fact]
    public void CollapseAll_CollapsesAllBranches()
    {
        var cut = RenderTreeView(initiallyExpanded: _ => true);

        // Verify deep content is visible
        var labels = cut.FindAll(".node-label");
        Assert.Contains(labels, l => l.TextContent == "Alpha-2-X");

        cut.InvokeAsync(() => cut.Instance.CollapseAll());

        // Only roots should be visible
        labels = cut.FindAll(".node-label");
        Assert.Equal(2, labels.Count);
        Assert.DoesNotContain(labels, l => l.TextContent == "Alpha-1");
    }

    [Fact]
    public void RevealNode_ExpandsAncestors()
    {
        var cut = RenderTreeView();

        // Everything collapsed initially
        Assert.Equal(2, cut.FindAll(".node-label").Count);

        cut.InvokeAsync(() => cut.Instance.RevealNode("a2x"));

        // Alpha-2-X should now be visible (Alpha and Alpha-2 expanded)
        var labels = cut.FindAll(".node-label");
        Assert.Contains(labels, l => l.TextContent == "Alpha-2-X");
        Assert.Contains(labels, l => l.TextContent == "Alpha-1"); // sibling also visible since Alpha is expanded
        Assert.Contains(labels, l => l.TextContent == "Alpha-2");
    }

    [Fact]
    public void RevealNode_WithSelect_SelectsNode()
    {
        object? selected = null;
        var cut = RenderTreeView(selectable: true, onSelectedKeyChanged: k => selected = k);

        cut.InvokeAsync(() => cut.Instance.RevealNode("a2x", select: true));

        Assert.Equal("a2x", selected);
    }

    [Fact]
    public void RevealNode_UnknownKey_NoOp()
    {
        var cut = RenderTreeView();

        cut.InvokeAsync(() => cut.Instance.RevealNode("nonexistent"));

        // Alpha should still be collapsed
        var labels = cut.FindAll(".node-label");
        Assert.Equal(2, labels.Count);
    }

    // ── External filtering tests (R8) ──────────────────────────────────

    [Fact]
    public void Filtering_ReducedItems_HidesRemovedRoots()
    {
        var fullItems = SimpleRoots();
        var cut = RenderTreeView(items: fullItems);

        // Both roots visible
        var labels = cut.FindAll(".node-label");
        Assert.Equal(2, labels.Count);

        // Re-render with only Alpha (Beta removed)
        var alphaOnly = new List<TestNode> { fullItems[0] };
        cut.Render(parameters =>
        {
            parameters
                .Add(p => p.Items, alphaOnly)
                .Add(p => p.ChildrenSelector, (Func<TestNode, IReadOnlyList<TestNode>>)(n => n.Children))
                .Add(p => p.HasChildrenSelector, (Func<TestNode, bool>)(n => n.Children.Count > 0))
                .Add(p => p.KeySelector, (Func<TestNode, object>)(n => n.Key))
                .Add(p => p.NodeContent, (RenderFragment<TestNode>)(node => builder =>
                {
                    builder.AddMarkupContent(0, $"<span class=\"node-label\">{node.Label}</span>");
                }));
        });

        labels = cut.FindAll(".node-label");
        Assert.Single(labels);
        Assert.Equal("Alpha", labels[0].TextContent);
    }

    [Fact]
    public void Filtering_ExpansionStatePreserved()
    {
        var fullItems = SimpleRoots();
        var cut = RenderTreeView(items: fullItems);

        // Expand Alpha
        cut.Find(".tv-toggle").Click();
        Assert.Contains(cut.FindAll(".node-label"), l => l.TextContent == "Alpha-1");

        // Re-render with only Alpha
        var alphaOnly = new List<TestNode> { fullItems[0] };
        cut.Render(parameters =>
        {
            parameters
                .Add(p => p.Items, alphaOnly)
                .Add(p => p.ChildrenSelector, (Func<TestNode, IReadOnlyList<TestNode>>)(n => n.Children))
                .Add(p => p.HasChildrenSelector, (Func<TestNode, bool>)(n => n.Children.Count > 0))
                .Add(p => p.KeySelector, (Func<TestNode, object>)(n => n.Key))
                .Add(p => p.NodeContent, (RenderFragment<TestNode>)(node => builder =>
                {
                    builder.AddMarkupContent(0, $"<span class=\"node-label\">{node.Label}</span>");
                }));
        });

        // Alpha-1 still visible (expansion state preserved)
        Assert.Contains(cut.FindAll(".node-label"), l => l.TextContent == "Alpha-1");

        // Re-render with full list again
        cut.Render(parameters =>
        {
            parameters
                .Add(p => p.Items, fullItems)
                .Add(p => p.ChildrenSelector, (Func<TestNode, IReadOnlyList<TestNode>>)(n => n.Children))
                .Add(p => p.HasChildrenSelector, (Func<TestNode, bool>)(n => n.Children.Count > 0))
                .Add(p => p.KeySelector, (Func<TestNode, object>)(n => n.Key))
                .Add(p => p.NodeContent, (RenderFragment<TestNode>)(node => builder =>
                {
                    builder.AddMarkupContent(0, $"<span class=\"node-label\">{node.Label}</span>");
                }));
        });

        // Alpha-1 still visible after restoration
        Assert.Contains(cut.FindAll(".node-label"), l => l.TextContent == "Alpha-1");
    }

    [Fact]
    public void Filtering_SelectionCleared_WhenNodeDisappears()
    {
        var fullItems = SimpleRoots();
        object? lastSelected = "b"; // track the last value passed to callback
        var cut = RenderTreeView(
            items: fullItems,
            selectable: true,
            selectedKey: "b",
            onSelectedKeyChanged: k => lastSelected = k);

        // Re-render with only Alpha (Beta disappears)
        var alphaOnly = new List<TestNode> { fullItems[0] };
        cut.Render(parameters =>
        {
            parameters
                .Add(p => p.Items, alphaOnly)
                .Add(p => p.ChildrenSelector, (Func<TestNode, IReadOnlyList<TestNode>>)(n => n.Children))
                .Add(p => p.HasChildrenSelector, (Func<TestNode, bool>)(n => n.Children.Count > 0))
                .Add(p => p.KeySelector, (Func<TestNode, object>)(n => n.Key))
                .Add(p => p.NodeContent, (RenderFragment<TestNode>)(node => builder =>
                {
                    builder.AddMarkupContent(0, $"<span class=\"node-label\">{node.Label}</span>");
                }))
                .Add(p => p.Selectable, true)
                .Add(p => p.SelectedKey, (object?)"b")
                .Add(p => p.SelectedKeyChanged, (Action<object?>)(k => lastSelected = k));
        });

        // SelectedKeyChanged should have been called with null
        Assert.Null(lastSelected);
    }

    // ── Context menu tests ──────────────────────────────────────────────

    [Fact]
    public void ContextMenu_Null_NoMenuRendered()
    {
        var cut = RenderTreeView();

        // Right-click Alpha
        var row = cut.Find(".tv-row");
        row.TriggerEvent("oncontextmenu", new MouseEventArgs { ClientX = 100, ClientY = 200 });

        // No dropdown-menu should appear
        Assert.Throws<Bunit.ElementNotFoundException>(() => cut.Find(".dropdown-menu"));
    }

    [Fact]
    public void ContextMenu_RightClickShowsMenu()
    {
        var cut = RenderTreeView(contextMenu: node => builder =>
        {
            builder.AddMarkupContent(0, $"<button class=\"ctx-btn\">{node.Label}</button>");
        });

        // Right-click Alpha
        var row = cut.Find(".tv-row");
        row.TriggerEvent("oncontextmenu", new MouseEventArgs { ClientX = 100, ClientY = 200 });

        // Dropdown menu should contain the button for Alpha
        var menu = cut.Find(".dropdown-menu");
        Assert.NotNull(menu);
        var btn = menu.QuerySelector(".ctx-btn");
        Assert.NotNull(btn);
        Assert.Equal("Alpha", btn!.TextContent);
    }

    [Fact]
    public void ContextMenu_EscapeKeyDismissesMenu()
    {
        var cut = RenderTreeView(contextMenu: node => builder =>
        {
            builder.AddMarkupContent(0, $"<button class=\"ctx-btn\">{node.Label}</button>");
        });

        var row = cut.Find(".tv-row");
        row.TriggerEvent("oncontextmenu", new MouseEventArgs { ClientX = 100, ClientY = 200 });

        Assert.NotNull(cut.Find(".dropdown-menu"));

        // Press Escape on the menu — R15 spec requires it to dismiss.
        var menu = cut.Find(".dropdown-menu");
        menu.TriggerEvent("onkeydown", new KeyboardEventArgs { Key = "Escape" });

        Assert.Empty(cut.FindAll(".dropdown-menu"));
    }

    [Fact]
    public void ContextMenu_NonEscapeKey_DoesNotDismiss()
    {
        var cut = RenderTreeView(contextMenu: node => builder =>
        {
            builder.AddMarkupContent(0, $"<button class=\"ctx-btn\">{node.Label}</button>");
        });

        var row = cut.Find(".tv-row");
        row.TriggerEvent("oncontextmenu", new MouseEventArgs { ClientX = 100, ClientY = 200 });

        var menu = cut.Find(".dropdown-menu");
        menu.TriggerEvent("onkeydown", new KeyboardEventArgs { Key = "ArrowDown" });

        Assert.NotEmpty(cut.FindAll(".dropdown-menu"));
    }

    [Fact]
    public void ContextMenu_RightClickDifferentNode_ReplacesMenu()
    {
        var cut = RenderTreeView(
            initiallyExpanded: n => n.Key == "a",
            contextMenu: node => builder =>
            {
                builder.AddMarkupContent(0, $"<button class=\"ctx-btn\">{node.Label}</button>");
            });

        // Right-click Alpha
        var rows = cut.FindAll(".tv-row");
        rows[0].TriggerEvent("oncontextmenu", new MouseEventArgs { ClientX = 100, ClientY = 200 });

        // Now right-click Alpha-1
        rows = cut.FindAll(".tv-row");
        rows[1].TriggerEvent("oncontextmenu", new MouseEventArgs { ClientX = 150, ClientY = 250 });

        // Should be only one dropdown-menu, showing Alpha-1
        var menus = cut.FindAll(".dropdown-menu");
        Assert.Single(menus);
        var btn = menus[0].QuerySelector(".ctx-btn");
        Assert.NotNull(btn);
        Assert.Equal("Alpha-1", btn!.TextContent);
    }
}
