using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;

namespace ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests;

/// <summary>
///     Verifies cycle detection + topological sort on the virtual-tag dependency
///     graph. Publish-time correctness depends on these being right — a missed cycle
///     would deadlock cascade evaluation; a wrong topological order would miscompute
///     chained virtual tags.
/// </summary>
[Trait("Category", "Unit")]
public sealed class DependencyGraphTests
{
    private static IReadOnlySet<string> Set(params string[] items) =>
        new HashSet<string>(items, StringComparer.Ordinal);

    /// <summary>Verifies that an empty dependency graph produces an empty topological sort and no cycles.</summary>

    [Fact]
    public void Empty_graph_produces_empty_sort_and_no_cycles()
    {
        var g = new DependencyGraph();
        g.TopologicalSort().ShouldBeEmpty();
        g.DetectCycles().ShouldBeEmpty();
    }

    /// <summary>Verifies that a single node with no dependencies sorts correctly.</summary>

    [Fact]
    public void Single_node_with_no_deps()
    {
        var g = new DependencyGraph();
        g.Add("A", Set());
        g.TopologicalSort().ShouldBe(new[] { "A" });
        g.DetectCycles().ShouldBeEmpty();
    }

    /// <summary>Verifies that topological sort places dependencies before their dependents.</summary>

    [Fact]
    public void Topological_order_places_dependencies_before_dependents()
    {
        var g = new DependencyGraph();
        g.Add("B", Set("A"));       // B depends on A
        g.Add("C", Set("B", "A"));  // C depends on B + A
        g.Add("A", Set());          // A is a leaf

        var order = g.TopologicalSort();
        var idx = order.Select((x, i) => (x, i)).ToDictionary(p => p.x, p => p.i);
        idx["A"].ShouldBeLessThan(idx["B"]);
        idx["B"].ShouldBeLessThan(idx["C"]);
    }

    /// <summary>Verifies that a self-referencing node is detected as a cycle.</summary>

    [Fact]
    public void Self_loop_detected_as_cycle()
    {
        var g = new DependencyGraph();
        g.Add("A", Set("A"));
        var cycles = g.DetectCycles();
        cycles.Count.ShouldBe(1);
        cycles[0].ShouldContain("A");
    }

    /// <summary>Verifies that a two-node cycle is detected.</summary>

    [Fact]
    public void Two_node_cycle_detected()
    {
        var g = new DependencyGraph();
        g.Add("A", Set("B"));
        g.Add("B", Set("A"));
        var cycles = g.DetectCycles();
        cycles.Count.ShouldBe(1);
        cycles[0].Count.ShouldBe(2);
    }

    /// <summary>Verifies that a three-node cycle is detected.</summary>

    [Fact]
    public void Three_node_cycle_detected()
    {
        var g = new DependencyGraph();
        g.Add("A", Set("B"));
        g.Add("B", Set("C"));
        g.Add("C", Set("A"));
        var cycles = g.DetectCycles();
        cycles.Count.ShouldBe(1);
        cycles[0].Count.ShouldBe(3);
    }

    /// <summary>Verifies that multiple disjoint cycles are all reported.</summary>

    [Fact]
    public void Multiple_disjoint_cycles_all_reported()
    {
        var g = new DependencyGraph();
        // Cycle 1: A -> B -> A
        g.Add("A", Set("B"));
        g.Add("B", Set("A"));
        // Cycle 2: X -> Y -> Z -> X
        g.Add("X", Set("Y"));
        g.Add("Y", Set("Z"));
        g.Add("Z", Set("X"));
        // Clean leaf: M
        g.Add("M", Set());

        var cycles = g.DetectCycles();
        cycles.Count.ShouldBe(2);
    }

    /// <summary>Verifies that topological sort throws DependencyCycleException when a cycle is present.</summary>

    [Fact]
    public void Topological_sort_throws_DependencyCycleException_on_cycle()
    {
        var g = new DependencyGraph();
        g.Add("A", Set("B"));
        g.Add("B", Set("A"));
        Should.Throw<DependencyCycleException>(() => g.TopologicalSort())
            .Cycles.ShouldNotBeEmpty();
    }

    /// <summary>Verifies that DirectDependents returns only direct dependents, not transitive ones.</summary>

    [Fact]
    public void DirectDependents_returns_direct_only()
    {
        var g = new DependencyGraph();
        g.Add("B", Set("A"));
        g.Add("C", Set("B"));
        g.DirectDependents("A").ShouldBe(new[] { "B" });
        g.DirectDependents("B").ShouldBe(new[] { "C" });
        g.DirectDependents("C").ShouldBeEmpty();
    }

    /// <summary>Verifies that TransitiveDependentsInOrder returns the topological closure of dependents.</summary>

    [Fact]
    public void TransitiveDependentsInOrder_returns_topological_closure()
    {
        var g = new DependencyGraph();
        g.Add("B", Set("A"));
        g.Add("C", Set("B"));
        g.Add("D", Set("C"));
        var closure = g.TransitiveDependentsInOrder("A");
        closure.ShouldBe(new[] { "B", "C", "D" });
    }

    /// <summary>Verifies that readding a node overwrites its prior dependencies.</summary>

    [Fact]
    public void Readding_a_node_overwrites_prior_dependencies()
    {
        var g = new DependencyGraph();
        g.Add("X", Set("A"));
        g.DirectDependencies("X").ShouldBe(new[] { "A" });
        // Re-add with different deps (simulates script edit + republish).
        g.Add("X", Set("B", "C"));
        g.DirectDependencies("X").OrderBy(s => s).ShouldBe(new[] { "B", "C" });
        // A should no longer list X as a dependent.
        g.DirectDependents("A").ShouldBeEmpty();
    }

    /// <summary>Verifies that leaf dependencies not registered as nodes are treated as implicit upstream nodes.</summary>

    [Fact]
    public void Leaf_dependencies_not_registered_as_nodes_are_treated_as_implicit()
    {
        // A is referenced but never Add'd as a node — it's an upstream driver tag.
        var g = new DependencyGraph();
        g.Add("B", Set("A"));
        g.TopologicalSort().ShouldBe(new[] { "B" });
        g.DirectDependents("A").ShouldBe(new[] { "B" });
    }

    // ----- Core.VirtualTags-013: DependencyCycleException message must not present SCC as edge path -----

    /// <summary>Verifies that DependencyCycleException message describes cycle members and not a fabricated edge path.</summary>

    [Fact]
    public void DependencyCycleException_message_describes_cycle_members_not_a_fabricated_edge_path()
    {
        // Regression for Core.VirtualTags-013: Tarjan returns SCC members in stack-pop
        // order, NOT in edge-traversal order. The exception message must not render the
        // members as "A -> B -> C -> A" — that misleads operators into looking for an
        // edge that may not be in the config. Instead the message uses a set-form
        // ("members: A, B, C") or a labelled traversal.
        var g = new DependencyGraph();
        g.Add("A", Set("B"));
        g.Add("B", Set("A"));
        var ex = Should.Throw<DependencyCycleException>(() => g.TopologicalSort());

        // The arrow ("->") notation as used previously (string.Join(" -> ", c) + " -> " + c[0])
        // implies an ordered edge path. After the fix, the message must NOT contain the
        // closing edge `-> A` (i.e. " -> " + first-member) on its own — the formatting
        // must clearly mark the list as cycle MEMBERS rather than an edge sequence.
        ex.Message.ShouldContain("cycle");
        ex.Message.ShouldContain("A");
        ex.Message.ShouldContain("B");
        // Verify the message uses a member-list framing ("members:" or "members of cycle"
        // or commas) rather than the misleading edge-path framing.
        ex.Message.ShouldContain("member", Case.Insensitive,
            "message should label entries as cycle members, not present them as an edge path");
    }

    // ----- Core.VirtualTags-009: empty-set allocation on miss -----

    /// <summary>Verifies that querying direct dependencies for an unregistered node returns a shared empty set.</summary>

    [Fact]
    public void DirectDependencies_miss_returns_shared_empty_set_instance()
    {
        // Regression for Core.VirtualTags-009: calling DirectDependencies for an
        // unregistered node should NOT allocate a fresh HashSet each time. The miss
        // path returns a shared empty set so the change-cascade hot path doesn't
        // churn the GC.
        var g = new DependencyGraph();
        var a = g.DirectDependencies("Unknown1");
        var b = g.DirectDependencies("Unknown2");
        a.ShouldBeEmpty();
        b.ShouldBeEmpty();
        ReferenceEquals(a, b).ShouldBeTrue("miss path must return the shared empty-set instance");
    }

    /// <summary>Verifies that querying direct dependents for an unregistered node returns a shared empty set.</summary>

    [Fact]
    public void DirectDependents_miss_returns_shared_empty_set_instance()
    {
        // Same regression as above for DirectDependents — called from inside the
        // CascadeAsync DFS and TopologicalSort Kahn loop, so the miss-path allocation
        // is on every change-cascade event.
        var g = new DependencyGraph();
        var a = g.DirectDependents("LeafA");
        var b = g.DirectDependents("LeafB");
        a.ShouldBeEmpty();
        b.ShouldBeEmpty();
        ReferenceEquals(a, b).ShouldBeTrue("miss path must return the shared empty-set instance");
    }

    /// <summary>Verifies that deep dependency graphs do not cause stack overflow.</summary>

    [Fact]
    public void Deep_graph_no_stack_overflow()
    {
        // Iterative Tarjan's + Kahn's — 10k deep chain must complete without blowing the stack.
        var g = new DependencyGraph();
        for (var i = 1; i < 10_000; i++)
            g.Add($"N{i}", Set($"N{i - 1}"));
        var order = g.TopologicalSort();
        order.Count.ShouldBe(9_999);
    }
}
