using FluentAssertions;
using Microsoft.Extensions.Options;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Enums;
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<RoutePoint> 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(RoutePointType.Start);
        graph.Points.Last().PointType.Should().Be(RoutePointType.End);
        graph.Points.Skip(1).Take(graph.Points.Count - 2)
            .Should().OnlyContain(p => p.PointType == RoutePointType.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 == RoutePointType.Start).Should().Be(1);
        graph.Points.Count(p => p.PointType == RoutePointType.End).Should().Be(1);
        graph.Points.Count(p => p.PointType == RoutePointType.Action).Should().Be(8);
        graph.Points.Should().Contain(p => p.PointType == RoutePointType.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<RoutePoint> { new() { Latitude = 47.46, Longitude = 37.64 } };

        Action act = () => sut.Build(input);

        act.Should().Throw<ArgumentException>().WithMessage("*at least 2 points*");
    }

    [Fact]
    public void Build_NullInput_Throws()
    {
        var sut = MakeBuilder();

        Action act = () => sut.Build(null!);

        act.Should().Throw<ArgumentNullException>();
    }
}
