📄 ui/tests/GraphEdgeTests/CreateCurveGeometryShould.cs
using System;
using System.Collections.Immutable;
using System.Linq;
using Avalonia;
using Avalonia.Headless.XUnit;
using Avalonia.Media;
using Cfa.Ui.Graph;
using Microsoft.CodeAnalysis;
using Shouldly;

namespace Cfa.Ui.Tests.GraphEdgeTests;

public sealed class CreateCurveGeometryShould
{
    [AvaloniaFact]
    public void KeepTheLoopStrokeAndArrowOutsideSmallAndLargeNodes()
    {
        // Arrange
        var original = TestGraph.Create();
        foreach (var kind in Enum.GetValues<LinkKind>())
        foreach (var appearances in new[] { 1u, uint.MaxValue })
        {
            var analysis = original with
            {
                CallsTo = original.CallsTo.ToImmutableDictionary(
                    entry => entry.Key,
                    entry =>
                        (IImmutableList<CallAnalysis>)
                            entry.Value.Select(call => call with { LinkKind = kind }).ToImmutableList(),
                    SymbolEqualityComparer.Default
                ),
                Appearances = original.Appearances.ToImmutableDictionary(
                    entry => entry.Key,
                    _ => appearances,
                    SymbolEqualityComparer.Default
                ),
            };

            // Act
            var scene = GraphLayout.Build(analysis);
            var loop = scene.Edges.Single(edge => edge.IsLoopback);
            var curve = loop.CreateCurveGeometry();
            var arrow = loop.CreateArrowGeometry();

            // Assert
            var node = loop.Source.Bounds;
            // The curve lies inside its control-point hull; the stroke adds half its width.
            new[] { loop.Start, loop.Control1, loop.Control2, loop.End }.ShouldAllBe(point =>
                point.X - loop.Thickness / 2 >= node.Right
            );
            arrow.Bounds.Left.ShouldBeGreaterThanOrEqualTo(node.Right);
            curve.Bounds.Width.ShouldBeGreaterThan(0);
            arrow.Bounds.Width.ShouldBeGreaterThan(0);
            scene.Bounds.Contains(curve.GetRenderBounds(new Pen(Brushes.SkyBlue, loop.Thickness))).ShouldBeTrue();
            scene.Bounds.Contains(arrow.Bounds).ShouldBeTrue();
            for (var y = node.Top + 0.1; y < node.Bottom; y += 0.5)
            {
                var inside = new Point(node.Right - 0.1, y);
                curve.StrokeContains(new Pen(Brushes.SkyBlue, loop.Thickness), inside).ShouldBeFalse();
                arrow.FillContains(inside).ShouldBeFalse();
            }
        }
    }
}