using MeterVault.Core.Analysis.Virtual;
namespace MeterVault.Core.Tests.Analysis;
///
/// Nested virtual meters are evaluated dependencies first, a loop is reported with the path through it and
/// withheld from evaluation, and a request expands to the physical meters it finally reads (D-27).
///
public sealed class DependencyGraphTests
{
private static DependencyGraph Graph(params (int Meter, int[] Dependencies)[] meters) =>
DependencyGraph.Build(meters.ToDictionary(m => m.Meter, m => (IReadOnlyList)m.Dependencies));
[Fact]
public void Nested_virtual_meters_are_ordered_dependencies_first_and_expand_to_their_physical_leaves()
{
// 10 = m1 + m2, 11 = m10 - m3, 12 = m11 + m10
var graph = Graph((12, [11, 10]), (11, [10, 3]), (10, [1, 2]));
Assert.Equal([10, 11, 12], graph.EvaluationOrder);
Assert.Equal([1, 2, 3], graph.PhysicalLeaves(12));
Assert.Equal([1, 2], graph.PhysicalLeaves(10));
Assert.Equal([7], graph.PhysicalLeaves(7));
Assert.Equal([12, 10, 1], graph.PathTo(12, 1)); // the shortest way, not 12 → 11 → 10 → 1
Assert.Null(graph.PathTo(10, 3));
Assert.Empty(graph.Cycles);
}
[Fact]
public void Evaluation_order_for_a_request_holds_only_what_it_reaches()
{
var graph = Graph((12, [11]), (11, [10]), (10, [1]), (20, [2]));
Assert.Equal([10, 11], graph.EvaluationOrderFor([11]));
Assert.Equal([20], graph.EvaluationOrderFor([20, 1]));
}
[Fact]
public void A_loop_is_reported_with_its_path_and_blocks_everything_that_depends_on_it()
{
// 10 → 11 → 12 → 10 is a loop; 13 reads it; 14 is independent.
var graph = Graph((10, [11]), (11, [12, 1]), (12, [10]), (13, [10, 2]), (14, [1]));
var cycle = Assert.Single(graph.Cycles);
Assert.Equal([10, 11, 12, 10], cycle);
Assert.Equal([11, 12, 10, 11], graph.CycleFor(11));
Assert.Equal([13, 10, 11, 12, 10], graph.CycleFor(13));
Assert.False(graph.IsEvaluable(13));
Assert.True(graph.IsEvaluable(14));
Assert.True(graph.IsEvaluable(1));
Assert.Null(graph.CycleFor(14));
Assert.Equal([14], graph.EvaluationOrder);
}
[Fact]
public void A_meter_referring_to_itself_is_a_loop_of_one()
{
var graph = Graph((10, [10, 1]));
Assert.Equal([10, 10], Assert.Single(graph.Cycles));
Assert.Empty(graph.EvaluationOrder);
Assert.Equal([1], graph.PhysicalLeaves(10));
}
[Fact]
public void Dependents_are_the_virtual_meters_a_deletion_would_break()
{
var graph = Graph((10, [1, 2]), (11, [10, 3]), (12, [3]));
Assert.Equal([10, 11], graph.Dependents(1));
Assert.Equal([11, 12], graph.Dependents(3));
Assert.Equal([11], graph.Dependents(10));
Assert.Empty(graph.Dependents(11));
}
[Fact]
public void A_legacy_virtual_meter_without_a_calculation_is_not_mistaken_for_a_physical_leaf()
{
var catalog = new MeterCatalog(VirtualFixtures.SeededElectricity()); // Summe Solar (6) has no definition
var graph = DependencyGraph.FromCatalog(catalog);
Assert.True(graph.IsVirtual(6));
Assert.Empty(graph.PhysicalLeaves(6));
Assert.False(graph.IsVirtual(4));
}
[Fact]
public void A_chain_of_five_thousand_virtual_meters_is_walked_without_recursion()
{
var chain = Enumerable.Range(1, 5000).Select(i => (Meter: 10_000 + i, Dependencies: new[] { i == 1 ? 1 : 10_000 + i - 1 })).ToArray();
var graph = Graph(chain);
Assert.Equal(5000, graph.EvaluationOrder.Count);
Assert.Equal(10_001, graph.EvaluationOrder[0]);
Assert.Equal([1], graph.PhysicalLeaves(15_000));
Assert.Equal(5001, graph.PathTo(15_000, 1)!.Count);
var looped = Graph([.. chain.Skip(1), (10_001, [15_000])]);
Assert.Equal(5001, Assert.Single(looped.Cycles).Count);
}
[Fact]
public void Paths_format_as_meter_ids_joined_by_the_separator()
{
Assert.Equal("12>7>12", DependencyGraph.FormatPath([12, 7, 12]));
}
}