using Bunit;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;

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

/// <summary>
/// bUnit tests for <see cref="ExecutionTree"/> (Audit Log ParentExecutionId
/// feature, Task 10). The component takes the FLAT
/// <see cref="ExecutionTreeNode"/> list the repository returns, assembles it
/// into a tree by joining <see cref="ExecutionTreeNode.ParentExecutionId"/> to a
/// parent node's <see cref="ExecutionTreeNode.ExecutionId"/>, and renders it
/// recursively. Tests pin: single-node tree, multi-level assembly, stub-node
/// presentation, the arrived-from highlight, node-click navigation, node
/// double-click raising/bubbling <see cref="ExecutionTreeNode.ExecutionId"/> via
/// <c>OnNodeActivated</c>, and cycle-safety (a corrupt flat list must not
/// infinite-loop).
/// </summary>
public class ExecutionTreeTests : BunitContext
{
    private static ExecutionTreeNode Node(
        Guid executionId,
        Guid? parentExecutionId,
        int rowCount = 2,
        string? site = "plant-a",
        string? instance = "boiler-3")
        => new(
            executionId,
            parentExecutionId,
            rowCount,
            rowCount == 0 ? Array.Empty<string>() : new[] { "ApiOutbound" },
            rowCount == 0 ? Array.Empty<string>() : new[] { "Delivered" },
            rowCount == 0 ? null : site,
            rowCount == 0 ? null : instance,
            rowCount == 0 ? null : new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc),
            rowCount == 0 ? null : new DateTime(2026, 5, 20, 12, 0, 5, DateTimeKind.Utc));

    [Fact]
    public void SingleNode_RendersOneTreeNode()
    {
        var id = Guid.Parse("11111111-1111-1111-1111-111111111111");
        var nodes = new List<ExecutionTreeNode> { Node(id, null) };

        var cut = Render<ExecutionTree>(p => p
            .Add(c => c.Nodes, nodes)
            .Add(c => c.ArrivedFromExecutionId, id));

        Assert.Contains($"data-test=\"tree-node-{id}\"", cut.Markup);
    }

    [Fact]
    public void MultiLevel_AssemblesTree_FromFlatList()
    {
        // root → child → grandchild — a deliberately shuffled flat list so the
        // component must reconstruct parent/child links rather than rely on
        // input ordering.
        var root = Guid.Parse("aaaaaaaa-0000-0000-0000-000000000000");
        var child = Guid.Parse("bbbbbbbb-0000-0000-0000-000000000000");
        var grandchild = Guid.Parse("cccccccc-0000-0000-0000-000000000000");
        var nodes = new List<ExecutionTreeNode>
        {
            Node(grandchild, child),
            Node(root, null),
            Node(child, root),
        };

        var cut = Render<ExecutionTree>(p => p
            .Add(c => c.Nodes, nodes)
            .Add(c => c.ArrivedFromExecutionId, child));

        // All three executions render as nodes.
        Assert.Contains($"data-test=\"tree-node-{root}\"", cut.Markup);
        Assert.Contains($"data-test=\"tree-node-{child}\"", cut.Markup);
        Assert.Contains($"data-test=\"tree-node-{grandchild}\"", cut.Markup);

        // The root must appear before the child, and the child before the
        // grandchild — recursive depth-first rendering preserves ancestry.
        var rootIdx = cut.Markup.IndexOf($"tree-node-{root}", StringComparison.Ordinal);
        var childIdx = cut.Markup.IndexOf($"tree-node-{child}", StringComparison.Ordinal);
        var grandIdx = cut.Markup.IndexOf($"tree-node-{grandchild}", StringComparison.Ordinal);
        Assert.True(rootIdx < childIdx, "root must render before child");
        Assert.True(childIdx < grandIdx, "child must render before grandchild");
    }

    [Fact]
    public void StubNode_RendersStubMarker()
    {
        // A stub parent (RowCount = 0) referenced by a real child must still
        // render, visibly marked as "no audited actions".
        var stubParent = Guid.Parse("dddddddd-0000-0000-0000-000000000000");
        var child = Guid.Parse("eeeeeeee-0000-0000-0000-000000000000");
        var nodes = new List<ExecutionTreeNode>
        {
            Node(stubParent, null, rowCount: 0),
            Node(child, stubParent),
        };

        var cut = Render<ExecutionTree>(p => p
            .Add(c => c.Nodes, nodes)
            .Add(c => c.ArrivedFromExecutionId, child));

        Assert.Contains($"data-test=\"tree-node-{stubParent}\"", cut.Markup);
        Assert.Contains($"data-test=\"stub-node-{stubParent}\"", cut.Markup);
        Assert.Contains("no audited actions", cut.Markup);
    }

    [Fact]
    public void ArrivedFromNode_IsVisuallyHighlighted()
    {
        var root = Guid.Parse("aaaaaaaa-1111-1111-1111-111111111111");
        var child = Guid.Parse("bbbbbbbb-1111-1111-1111-111111111111");
        var nodes = new List<ExecutionTreeNode>
        {
            Node(root, null),
            Node(child, root),
        };

        var cut = Render<ExecutionTree>(p => p
            .Add(c => c.Nodes, nodes)
            .Add(c => c.ArrivedFromExecutionId, child));

        // The arrived-from node carries the highlight marker; a non-arrived
        // sibling does not.
        var arrived = cut.Find($"[data-test=\"tree-node-{child}\"]");
        Assert.Contains("execution-tree-node--current", arrived.GetAttribute("class"));

        var other = cut.Find($"[data-test=\"tree-node-{root}\"]");
        Assert.DoesNotContain("execution-tree-node--current", other.GetAttribute("class") ?? string.Empty);
    }

    [Fact]
    public void NodeLink_PointsTo_AuditLogFilteredByThatExecution()
    {
        // Each node's id is a real <a href> deep link — clicking it lands on
        // the Audit Log filtered to that execution's rows. A genuine anchor
        // (rather than an @onclick navigate) keeps the link middle-click /
        // open-in-new-tab friendly, matching the rest of the Audit UI.
        var root = Guid.Parse("aaaaaaaa-2222-2222-2222-222222222222");
        var child = Guid.Parse("bbbbbbbb-2222-2222-2222-222222222222");
        var nodes = new List<ExecutionTreeNode>
        {
            Node(root, null),
            Node(child, root),
        };

        var cut = Render<ExecutionTree>(p => p
            .Add(c => c.Nodes, nodes)
            .Add(c => c.ArrivedFromExecutionId, root));

        var childLink = cut.Find($"[data-test=\"tree-node-link-{child}\"]");
        Assert.Equal($"/audit/log?executionId={child}", childLink.GetAttribute("href"));

        var rootLink = cut.Find($"[data-test=\"tree-node-link-{root}\"]");
        Assert.Equal($"/audit/log?executionId={root}", rootLink.GetAttribute("href"));
    }

    [Fact]
    public void EmptyNodeList_RendersNothingWithoutThrowing()
    {
        var cut = Render<ExecutionTree>(p => p
            .Add(c => c.Nodes, (IReadOnlyList<ExecutionTreeNode>)Array.Empty<ExecutionTreeNode>())
            .Add(c => c.ArrivedFromExecutionId, Guid.NewGuid()));

        Assert.DoesNotContain("data-test=\"tree-node-", cut.Markup);
    }

    [Fact]
    public void CyclicFlatList_TerminatesWithoutInfiniteLoop()
    {
        // Defensive: a corrupt flat list where A→B and B→A must not hang the
        // renderer. Each execution is rendered at most once.
        var a = Guid.Parse("a0000000-0000-0000-0000-000000000000");
        var b = Guid.Parse("b0000000-0000-0000-0000-000000000000");
        var nodes = new List<ExecutionTreeNode>
        {
            Node(a, b),
            Node(b, a),
        };

        var cut = Render<ExecutionTree>(p => p
            .Add(c => c.Nodes, nodes)
            .Add(c => c.ArrivedFromExecutionId, a));

        // Both render exactly once — no runaway recursion.
        Assert.Equal(1, CountOccurrences(cut.Markup, $"data-test=\"tree-node-{a}\""));
        Assert.Equal(1, CountOccurrences(cut.Markup, $"data-test=\"tree-node-{b}\""));
    }

    [Fact]
    public void ToggleExpand_CollapsesAndReExpandsChildSubtree()
    {
        // root → child → grandchild. Clicking the root's toggle collapses its
        // subtree (the child node disappears); clicking it again re-expands.
        var root = Guid.Parse("aaaaaaaa-3333-3333-3333-333333333333");
        var child = Guid.Parse("bbbbbbbb-3333-3333-3333-333333333333");
        var grandchild = Guid.Parse("cccccccc-3333-3333-3333-333333333333");
        var nodes = new List<ExecutionTreeNode>
        {
            Node(root, null),
            Node(child, root),
            Node(grandchild, child),
        };

        var cut = Render<ExecutionTree>(p => p
            .Add(c => c.Nodes, nodes)
            .Add(c => c.ArrivedFromExecutionId, root));

        // All nodes start expanded — the whole chain is visible on arrival.
        Assert.Contains($"data-test=\"tree-node-{child}\"", cut.Markup);
        Assert.Contains($"data-test=\"tree-node-{grandchild}\"", cut.Markup);

        var toggle = cut.Find($"[data-test=\"tree-toggle-{root}\"]");
        Assert.Equal("true", toggle.GetAttribute("aria-expanded"));

        // Collapse: the child (and its descendants) must disappear.
        toggle.Click();
        Assert.DoesNotContain($"data-test=\"tree-node-{child}\"", cut.Markup);
        Assert.DoesNotContain($"data-test=\"tree-node-{grandchild}\"", cut.Markup);
        Assert.Equal(
            "false",
            cut.Find($"[data-test=\"tree-toggle-{root}\"]").GetAttribute("aria-expanded"));

        // Re-expand: the child subtree reappears.
        cut.Find($"[data-test=\"tree-toggle-{root}\"]").Click();
        Assert.Contains($"data-test=\"tree-node-{child}\"", cut.Markup);
        Assert.Contains($"data-test=\"tree-node-{grandchild}\"", cut.Markup);
        Assert.Equal(
            "true",
            cut.Find($"[data-test=\"tree-toggle-{root}\"]").GetAttribute("aria-expanded"));
    }

    [Fact]
    public void DoubleClickingNode_RaisesOnNodeActivated_WithExecutionId()
    {
        // Double-clicking a node's body raises OnNodeActivated carrying that
        // node's ExecutionId — the affordance a later task uses to open the
        // node detail modal.
        var root = Guid.Parse("aaaaaaaa-4444-4444-4444-444444444444");
        var child = Guid.Parse("bbbbbbbb-4444-4444-4444-444444444444");
        var nodes = new List<ExecutionTreeNode>
        {
            Node(root, null),
            Node(child, root),
        };

        Guid? activated = null;
        var cut = Render<ExecutionTree>(p => p
            .Add(c => c.Nodes, nodes)
            .Add(c => c.ArrivedFromExecutionId, root)
            .Add(c => c.OnNodeActivated, (Guid id) => activated = id));

        var rootBody = cut.Find($"[data-test=\"tree-node-{root}\"] .execution-tree-body");
        rootBody.DoubleClick();

        Assert.Equal(root, activated);
    }

    [Fact]
    public void DoubleClickingNestedNode_BubblesOnNodeActivated_ToRoot()
    {
        // root → child → grandchild. Double-clicking a deeply nested node's
        // body invokes the SAME root-supplied callback — the EventCallback is
        // threaded unchanged down every recursive ExecutionTree instance.
        var root = Guid.Parse("aaaaaaaa-5555-5555-5555-555555555555");
        var child = Guid.Parse("bbbbbbbb-5555-5555-5555-555555555555");
        var grandchild = Guid.Parse("cccccccc-5555-5555-5555-555555555555");
        var nodes = new List<ExecutionTreeNode>
        {
            Node(root, null),
            Node(child, root),
            Node(grandchild, child),
        };

        Guid? activated = null;
        var cut = Render<ExecutionTree>(p => p
            .Add(c => c.Nodes, nodes)
            .Add(c => c.ArrivedFromExecutionId, root)
            .Add(c => c.OnNodeActivated, (Guid id) => activated = id));

        // Double-click the grandchild (two recursion levels deep).
        cut.Find($"[data-test=\"tree-node-{grandchild}\"] .execution-tree-body").DoubleClick();
        Assert.Equal(grandchild, activated);

        // And the child (one level deep) — both reach the root's callback.
        cut.Find($"[data-test=\"tree-node-{child}\"] .execution-tree-body").DoubleClick();
        Assert.Equal(child, activated);
    }

    private static int CountOccurrences(string haystack, string needle)
    {
        int count = 0, idx = 0;
        while ((idx = haystack.IndexOf(needle, idx, StringComparison.Ordinal)) >= 0)
        {
            count++;
            idx += needle.Length;
        }
        return count;
    }
}
