using FluentAssertions; using Microsoft.Extensions.Options; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.DTO; using SatelliteProvider.Common.Utils; using SatelliteProvider.Services.RouteManagement; using SatelliteProvider.Tests.Fixtures; namespace SatelliteProvider.Tests; public class RoutePointGraphBuilderTests { private static readonly ProcessingConfig DefaultProcessingConfig = new(); private static RoutePointGraphBuilder MakeBuilder() => new(Options.Create(new ProcessingConfig())); private static List ToRoutePoints(IEnumerable<(double Lat, double Lon)> points) => points.Select(p => new RoutePoint { Latitude = p.Lat, Longitude = p.Lon }).ToList(); [Fact] public void Build_TwoUserPoints_FirstIsStart_LastIsEnd_BetweenAreIntermediate() { var sut = MakeBuilder(); var input = ToRoutePoints(TestCoordinates.Route.Route01Points); var graph = sut.Build(input); graph.Points.First().PointType.Should().Be("start"); graph.Points.Last().PointType.Should().Be("end"); graph.Points.Skip(1).Take(graph.Points.Count - 2) .Should().OnlyContain(p => p.PointType == "intermediate"); } [Fact] public void Build_ConsecutivePointsRespectMaxSpacing() { var sut = MakeBuilder(); var input = ToRoutePoints(TestCoordinates.Route.Route01Points); var graph = sut.Build(input); for (int i = 1; i < graph.Points.Count; i++) { var prev = graph.Points[i - 1]; var cur = graph.Points[i]; var distance = GeoUtils.CalculateDistance( new GeoPoint(prev.Latitude, prev.Longitude), new GeoPoint(cur.Latitude, cur.Longitude)); distance.Should().BeLessThanOrEqualTo(DefaultProcessingConfig.MaxRoutePointSpacingMeters + 0.5, $"point {i - 1}→{i} must be ≤{DefaultProcessingConfig.MaxRoutePointSpacingMeters}m"); } } [Fact] public void Build_TenPointRoute_HasOneStartOneEndAndEightAction() { var sut = MakeBuilder(); var input = ToRoutePoints(TestCoordinates.Route.Route04Points); var graph = sut.Build(input); graph.Points.Count(p => p.PointType == "start").Should().Be(1); graph.Points.Count(p => p.PointType == "end").Should().Be(1); graph.Points.Count(p => p.PointType == "action").Should().Be(8); graph.Points.Should().Contain(p => p.PointType == "intermediate"); } [Fact] public void Build_TotalDistanceEqualsSumOfHaversineSegments() { var sut = MakeBuilder(); var input = ToRoutePoints(TestCoordinates.Route.Route01Points); var graph = sut.Build(input); var summed = 0.0; for (int i = 1; i < graph.Points.Count; i++) { var prev = graph.Points[i - 1]; var cur = graph.Points[i]; summed += GeoUtils.CalculateDistance( new GeoPoint(prev.Latitude, prev.Longitude), new GeoPoint(cur.Latitude, cur.Longitude)); } graph.TotalDistanceMeters.Should().BeApproximately(summed, 1.0); } [Fact] public void Build_SequenceNumbersAreContiguousAndStartAtZero() { var sut = MakeBuilder(); var input = ToRoutePoints(TestCoordinates.Route.Route04Points); var graph = sut.Build(input); graph.Points.Select(p => p.SequenceNumber) .Should().Equal(Enumerable.Range(0, graph.Points.Count)); } [Fact] public void Build_FirstPointHasNullDistanceFromPrevious() { var sut = MakeBuilder(); var input = ToRoutePoints(TestCoordinates.Route.Route01Points); var graph = sut.Build(input); graph.Points.First().DistanceFromPrevious.Should().BeNull(); graph.Points.Skip(1).Should().OnlyContain(p => p.DistanceFromPrevious.HasValue); } [Fact] public void Build_FewerThanTwoPoints_Throws() { var sut = MakeBuilder(); var input = new List { new() { Latitude = 47.46, Longitude = 37.64 } }; Action act = () => sut.Build(input); act.Should().Throw().WithMessage("*at least 2 points*"); } [Fact] public void Build_NullInput_Throws() { var sut = MakeBuilder(); Action act = () => sut.Build(null!); act.Should().Throw(); } }