using TraceCad.Core.Geometry;
using TraceCad.Core.Model;
using Xunit;

namespace TraceCad.Tests;

public sealed class GeometryTests
{
    [Fact]
    public void LineLengthUsesMillimetres()
    {
        var line = new LineEntity(Guid.NewGuid(), Layer.Cut.Name, new Point2(0, 0), new Point2(3, 4));

        Assert.Equal(5.0, line.Length, 9);
    }

    [Fact]
    public void ParallelConstraintPreservesTargetLengthAndMatchesReferenceDirection()
    {
        var reference = new LineEntity(Guid.NewGuid(), Layer.Cut.Name, new Point2(0, 0), new Point2(10, 0));
        var target = new LineEntity(Guid.NewGuid(), Layer.Cut.Name, new Point2(0, 5), new Point2(0, 9));

        var success = ConstraintGeometry.TryMakeParallel(reference, target, out var replacement);

        Assert.True(success);
        Assert.Equal(target.Length, replacement.Length, 9);
        Assert.Equal(0.0, replacement.Start.Y - replacement.End.Y, 9);
    }

    [Fact]
    public void ParallelDistanceMeasuresPerpendicularSpacing()
    {
        var first = new LineEntity(Guid.NewGuid(), Layer.Cut.Name, new Point2(0, 0), new Point2(10, 0));
        var second = new LineEntity(Guid.NewGuid(), Layer.Cut.Name, new Point2(3, 7), new Point2(13, 7));

        var success = ConstraintGeometry.TryMeasureParallelDistance(first, second, out var distance);

        Assert.True(success);
        Assert.Equal(7.0, distance, 9);
    }

    [Fact]
    public void TangentConstraintMovesLineOntoNearestArcEndpointTangent()
    {
        var arc = new ArcEntity(Guid.NewGuid(), Layer.Cut.Name, new Point2(0, 0), 10, 0, 90, false);
        var line = new LineEntity(Guid.NewGuid(), Layer.Cut.Name, new Point2(10.2, 0.1), new Point2(16.2, 2.1));

        var success = ConstraintGeometry.TryMakeLineTangentToArc(line, arc, out var replacement);

        Assert.True(success);
        Assert.Equal(arc.StartPoint.X, replacement.Start.X, 9);
        Assert.Equal(arc.StartPoint.Y, replacement.Start.Y, 9);
        var direction = (replacement.End - replacement.Start).Normalized();
        var tangent = GeometryHelpers.TangentAtAngle(arc.StartAngleDeg, arc.IsClockwise);
        Assert.True(Math.Abs(direction.Dot(tangent)) > 0.999);
    }

    [Fact]
    public void SolverAppliesDrivingRadiusDimension()
    {
        var document = SketchDocument.CreateDefault();
        var arcId = Guid.NewGuid();
        document.AddEntity(new ArcEntity(arcId, Layer.Cut.Name, new Point2(0, 0), 10, 0, 90, false));
        document.AddConstraint(new SketchConstraint(Guid.NewGuid(), ConstraintType.Radius, new[] { arcId }, 25));

        SketchConstraintSolver.Solve(document);

        var arc = Assert.IsType<ArcEntity>(document.Entities.Single(entity => entity.Id == arcId));
        Assert.Equal(25.0, arc.Radius, 9);
    }

    [Fact]
    public void SolverAppliesDrivingParallelDistanceDimension()
    {
        var document = SketchDocument.CreateDefault();
        var firstId = Guid.NewGuid();
        var secondId = Guid.NewGuid();
        document.AddEntity(new LineEntity(firstId, Layer.Cut.Name, new Point2(0, 0), new Point2(10, 0)));
        document.AddEntity(new LineEntity(secondId, Layer.Cut.Name, new Point2(0, 2), new Point2(10, 2)));
        document.AddConstraint(new SketchConstraint(Guid.NewGuid(), ConstraintType.Distance, new[] { firstId, secondId }, 7));

        SketchConstraintSolver.Solve(document);

        var first = Assert.IsType<LineEntity>(document.Entities.Single(entity => entity.Id == firstId));
        var second = Assert.IsType<LineEntity>(document.Entities.Single(entity => entity.Id == secondId));
        Assert.True(ConstraintGeometry.TryMeasureParallelDistance(first, second, out var distance));
        Assert.Equal(7.0, distance, 9);
    }

    [Fact]
    public void CircleFromThreePointsComputesCenterAndRadius()
    {
        var success = GeometryHelpers.TryCreateCircleFromThreePoints(
            new Point2(1, 0),
            new Point2(0, 1),
            new Point2(-1, 0),
            out var circle);

        Assert.True(success);
        Assert.Equal(0.0, circle.Center.X, 9);
        Assert.Equal(0.0, circle.Center.Y, 9);
        Assert.Equal(1.0, circle.Radius, 9);
    }

    [Fact]
    public void CircleFromCollinearPointsFails()
    {
        var success = GeometryHelpers.TryCreateCircleFromThreePoints(
            new Point2(0, 0),
            new Point2(1, 1),
            new Point2(2, 2),
            out _);

        Assert.False(success);
    }

    [Fact]
    public void ArcFromThreePointsPreservesPointOnCounterClockwiseSweep()
    {
        var success = GeometryHelpers.TryCreateArcFromThreePoints(
            new Point2(1, 0),
            new Point2(0, 1),
            new Point2(-1, 0),
            out var arc);

        Assert.True(success);
        Assert.False(arc.IsClockwise);
        Assert.Equal(0.0, arc.StartAngleDeg, 9);
        Assert.Equal(180.0, arc.EndAngleDeg, 9);
        Assert.Equal(1.0, arc.Radius, 9);
    }

    [Fact]
    public void ArcFromCollinearPointsFails()
    {
        var success = GeometryHelpers.TryCreateArcFromThreePoints(
            new Point2(0, 0),
            new Point2(5, 0),
            new Point2(10, 0),
            out _);

        Assert.False(success);
    }

    [Fact]
    public void ArcFromStartTangentAndEndPointMatchesRequestedTangent()
    {
        var success = GeometryHelpers.TryCreateArcFromStartTangentAndEndPoint(
            new Point2(0, 0),
            new Vector2(1, 0),
            new Point2(10, 10),
            new Point2(8, 2),
            out var arc);

        Assert.True(success);
        var tangent = GeometryHelpers.TangentAtAngle(arc.StartAngleDeg, arc.IsClockwise);
        Assert.True(tangent.Dot(new Vector2(1, 0)) > 0.999);
    }

    [Fact]
    public void ArcFromStartTangentFailsWhenEndPointLiesOnTangentLine()
    {
        var success = GeometryHelpers.TryCreateArcFromStartTangentAndEndPoint(
            new Point2(0, 0),
            new Vector2(1, 0),
            new Point2(10, 0),
            null,
            out _);

        Assert.False(success);
    }

    [Theory]
    [InlineData(360, 0)]
    [InlineData(-90, 270)]
    [InlineData(725, 5)]
    public void AngleNormalizationWrapsToPositiveDegrees(double input, double expected)
    {
        Assert.Equal(expected, GeometryHelpers.NormalizeAngleDeg(input), 9);
    }
}
