📄 ui/tests/PhysicsLayoutTests/AvoidCrossingsShould.cs
using System;
using System.Linq;
using Avalonia;
using Cfa.Ui.Graph;
using Shouldly;
using Xunit;

namespace Cfa.Ui.Tests.PhysicsLayoutTests;

public sealed class AvoidCrossingsShould
{
    [Fact]
    public void UntangleCrossedBranchesDeterministically()
    {
        // Arrange
        PhysicsNode[] nodes = [new(-1, 10), new(0, 10), new(0, 10), new(1, 10), new(2, 10)];
        Point[] starts = [new(0, -200), new(-100, 0), new(100, 0), new(100, 200), new(-100, 200)];
        var options = new PhysicsOptions { SpringStrength = 0.001, EdgeNodeStrength = 0 };
        var layout = new PhysicsLayout(nodes, options, starts);
        var repeat = new PhysicsLayout(nodes, options, starts);

        // Act
        layout.Settle();
        repeat.Settle();

        // Assert
        Crosses(layout.Positions[1], layout.Positions[3], layout.Positions[2], layout.Positions[4]).ShouldBeFalse();
        layout.Positions.ShouldBe(repeat.Positions);
        layout.Positions.ShouldAllBe(point => double.IsFinite(point.X) && double.IsFinite(point.Y));
    }

    [Fact]
    public void PushAnUnrelatedNodeOffAnEdgeEvenWhenExactlyAligned()
    {
        // Arrange
        PhysicsNode[] nodes = [new(-1, 10), new(0, 10), new(0, 10)];
        Point[] starts = [default, new(400, 0), new(200, 0)];
        var layout = new PhysicsLayout(nodes, new PhysicsOptions { EdgeCrossingStrength = 0 }, starts);

        // Act
        layout.Tick();

        // Assert
        Math.Abs(layout.Positions[2].Y).ShouldBeGreaterThan(0);
    }

    [Fact]
    public void LeaveSharedEndpointsAlone()
    {
        // Arrange
        PhysicsNode[] nodes = [new(-1, 10), new(0, 10), new(0, 10)];
        Point[] starts = [default, new(200, 0), new(0, 200)];
        var enabled = new PhysicsLayout(nodes, new PhysicsOptions { EdgeNodeStrength = 0 }, starts);
        var disabled = new PhysicsLayout(
            nodes,
            new PhysicsOptions { EdgeCrossingStrength = 0, EdgeNodeStrength = 0 },
            starts
        );

        // Act
        enabled.Tick();
        disabled.Tick();

        // Assert
        enabled.Positions.ShouldBe(disabled.Positions);
    }

    [Theory]
    [InlineData(20, true)]
    [InlineData(60, false)]
    public void ApplyNodeAvoidanceOnlyWithinTheClearance(double distance, bool shouldDiffer)
    {
        // Arrange
        PhysicsNode[] nodes = [new(-1, 10), new(0, 10), new(0, 10)];
        Point[] starts = [default, new(400, 0), new(200, distance)];
        var enabled = new PhysicsLayout(nodes, new PhysicsOptions { EdgeCrossingStrength = 0 }, starts);
        var disabled = new PhysicsLayout(
            nodes,
            new PhysicsOptions { EdgeCrossingStrength = 0, EdgeNodeStrength = 0 },
            starts
        );

        // Act
        enabled.Tick();
        disabled.Tick();

        // Assert
        enabled.Positions.SequenceEqual(disabled.Positions).ShouldBe(!shouldDiffer);
    }

    [Theory]
    [InlineData(20, true)]
    [InlineData(80, false)]
    public void RepelNearlyCrossingIndependentEdgesWithoutAnIntersection(double distance, bool shouldDiffer)
    {
        // Arrange
        PhysicsNode[] nodes = [new(-1, 1), new(0, 1), new(0, 1), new(1, 1), new(2, 1)];
        Point[] starts = [new(-300, 200), new(0, 0), new(100, distance), new(200, 0), new(100, 200)];
        var enabled = new PhysicsLayout(nodes, new PhysicsOptions { EdgeNodeStrength = 0 }, starts);
        var disabled = new PhysicsLayout(
            nodes,
            new PhysicsOptions { EdgeNodeStrength = 0, EdgeCrossingStrength = 0 },
            starts
        );

        // Act
        enabled.Tick();
        disabled.Tick();

        // Assert
        enabled.Positions.SequenceEqual(disabled.Positions).ShouldBe(!shouldDiffer);
    }

    private static bool Crosses(Point a, Point b, Point c, Point d)
    {
        static double Side(Point p, Point q, Point r) => (q.X - p.X) * (r.Y - p.Y) - (q.Y - p.Y) * (r.X - p.X);
        return Side(a, b, c) * Side(a, b, d) < 0 && Side(c, d, a) * Side(c, d, b) < 0;
    }
}