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);

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

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

    [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"]);
    }

    [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");
    }

    [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);
    }

    [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);
    }

    [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);
    }

    [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();
    }

    [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();
    }

    [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" });
    }

    [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();
    }

    [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" });
    }

    [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);
    }
}
