using Theriapolis.Core;
using Theriapolis.Core.Util;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Polylines;
using Xunit;
using Xunit.Abstractions;

namespace Theriapolis.Tests.Worldgen;

/// <summary>
/// Road network connectivity tests:
/// 1. Every non-POI settlement has a road endpoint within reach.
/// 2. Every bridge has road geometry on both sides (not truncated).
/// 3. No duplicate road polylines connect the same settlement pair.
/// </summary>
public sealed class RoadConnectivityTests : IClassFixture<WorldCache>
{
    private readonly WorldCache _cache;
    private readonly ITestOutputHelper _out;

    public RoadConnectivityTests(WorldCache cache, ITestOutputHelper output)
    {
        _cache = cache;
        _out   = output;
    }

    // ── 1. Settlement connectivity ───────────────────────────────────────────

    /// <summary>
    /// Every non-POI settlement must have at least one road polyline endpoint
    /// within 2 tiles of its center. This catches settlements that are
    /// topologically in the network but have no visible road reaching them.
    /// </summary>
    [Theory]
    [InlineData(0xCAFEBABEUL)]
    [InlineData(0xDEADBEEFUL)]
    [InlineData(0x12345678UL)]
    public void AllSettlements_HaveRoadEndpointNearby(ulong seed)
    {
        var ctx   = _cache.Get(seed);
        var world = ctx.World;
        float maxDist = C.WORLD_TILE_PIXELS * 2.5f; // 2.5 tiles
        float maxDistSq = maxDist * maxDist;

        var disconnected = new List<string>();

        foreach (var settle in world.Settlements)
        {
            if (settle.IsPoi) continue;

            var center = new Vec2(settle.WorldPixelX, settle.WorldPixelY);
            bool found = false;

            foreach (var road in world.Roads)
            {
                if (road.Points.Count < 2) continue;
                if (Vec2.DistSq(road.Points[0], center) < maxDistSq ||
                    Vec2.DistSq(road.Points[^1], center) < maxDistSq)
                {
                    found = true;
                    break;
                }
            }

            if (!found)
            {
                // Find closest endpoint for diagnostic output
                float bestDist = float.MaxValue;
                foreach (var road in world.Roads)
                {
                    if (road.Points.Count < 2) continue;
                    bestDist = MathF.Min(bestDist, Vec2.Dist(road.Points[0], center));
                    bestDist = MathF.Min(bestDist, Vec2.Dist(road.Points[^1], center));
                }

                disconnected.Add(
                    $"  {settle.Name ?? "?"} (Tier {settle.Tier}, tile {settle.TileX},{settle.TileY}) " +
                    $"— nearest endpoint {bestDist:F0}px away ({bestDist / C.WORLD_TILE_PIXELS:F1} tiles)");
            }
        }

        if (disconnected.Count > 0)
        {
            _out.WriteLine($"Disconnected settlements ({disconnected.Count}):");
            foreach (var line in disconnected) _out.WriteLine(line);
        }

        Assert.Empty(disconnected);
    }

    // ── 2. Bridge–road continuity ────────────────────────────────────────────

    /// <summary>
    /// Every bridge must reference a valid road, and the road must have enough
    /// geometry to visually support the bridge (not just 1-2 segments total).
    /// Bridges at road polyline termini are acceptable — the "other side" is
    /// covered by a different polyline from SplitByExistingFeature.
    /// </summary>
    [Theory]
    [InlineData(0xCAFEBABEUL)]
    [InlineData(0xDEADBEEFUL)]
    [InlineData(0x12345678UL)]
    public void AllBridges_ReferenceValidRoads(ulong seed)
    {
        var ctx   = _cache.Get(seed);
        var world = ctx.World;

        // Index roads by Id for fast lookup
        var roadsById = new Dictionary<int, Polyline>();
        foreach (var road in world.Roads)
            roadsById.TryAdd(road.Id, road);

        var broken = new List<string>();

        foreach (var bridge in world.Bridges)
        {
            if (!roadsById.TryGetValue(bridge.RoadId, out var road))
            {
                broken.Add($"  Bridge at ({bridge.WorldPixelX:F0},{bridge.WorldPixelY:F0}) references missing road Id={bridge.RoadId}");
                continue;
            }

            // A road with < 5 segments is too short to meaningfully support a
            // bridge — the deck would be the entire road.
            if (road.Points.Count < 5)
            {
                broken.Add(
                    $"  Bridge at ({bridge.WorldPixelX:F0},{bridge.WorldPixelY:F0}) on road {road.Id} " +
                    $"({road.RoadClassification}) — road only has {road.Points.Count} points");
            }
        }

        if (broken.Count > 0)
        {
            _out.WriteLine($"Invalid bridges ({broken.Count}/{world.Bridges.Count}):");
            foreach (var line in broken) _out.WriteLine(line);
        }

        Assert.Empty(broken);
    }

    // ── 3. No geometrically redundant roads ─────────────────────────────────

    /// <summary>
    /// When multiple road polylines connect the same settlement pair (expected
    /// from SplitByExistingFeature), they should cover DIFFERENT geographic
    /// stretches. If two same-pair polylines have endpoints close to each other,
    /// they're geometrically redundant — one should have been merged or subsumed
    /// during cleanup.
    /// </summary>
    [Theory]
    [InlineData(0xCAFEBABEUL)]
    [InlineData(0xDEADBEEFUL)]
    [InlineData(0x12345678UL)]
    public void NoDuplicateRoads_BetweenSameSettlementPair(ulong seed)
    {
        var ctx   = _cache.Get(seed);
        var world = ctx.World;
        float overlapDist = C.POLYLINE_MERGE_DIST; // 80px — if endpoints are this close, they overlap
        float overlapDistSq = overlapDist * overlapDist;

        // Group roads by their unordered settlement pair + classification
        var groups = new Dictionary<(int, int, RoadType), List<Polyline>>();
        foreach (var road in world.Roads)
        {
            if (road.FromSettlementId < 0 || road.ToSettlementId < 0) continue;
            int a = Math.Min(road.FromSettlementId, road.ToSettlementId);
            int b = Math.Max(road.FromSettlementId, road.ToSettlementId);
            var key = (a, b, road.RoadClassification);

            if (!groups.TryGetValue(key, out var list))
                groups[key] = list = new List<Polyline>();
            list.Add(road);
        }

        var duplicates = new List<string>();
        foreach (var (key, roads) in groups)
        {
            if (roads.Count < 2) continue;

            // Check every pair for geometric overlap.
            // Skip consecutive-ID pairs: those are split segments from a single
            // A* edge (SplitByExistingFeature), not duplicate routes. They share
            // a junction endpoint by design.
            for (int i = 0; i < roads.Count; i++)
            for (int j = i + 1; j < roads.Count; j++)
            {
                // Consecutive IDs come from the same edge's split — not redundant
                if (Math.Abs(roads[i].Id - roads[j].Id) == 1) continue;

                var ptsA = roads[i].Points;
                var ptsB = roads[j].Points;
                if (ptsA.Count < 2 || ptsB.Count < 2) continue;

                // Check if A's start is near any of B's endpoints AND
                // A's end is near any of B's endpoints.
                bool startOverlap =
                    Vec2.DistSq(ptsA[0], ptsB[0]) < overlapDistSq ||
                    Vec2.DistSq(ptsA[0], ptsB[^1]) < overlapDistSq;
                bool endOverlap =
                    Vec2.DistSq(ptsA[^1], ptsB[0]) < overlapDistSq ||
                    Vec2.DistSq(ptsA[^1], ptsB[^1]) < overlapDistSq;

                if (startOverlap && endOverlap)
                {
                    var settleA = world.Settlements.FirstOrDefault(s => s.Id == key.Item1);
                    var settleB = world.Settlements.FirstOrDefault(s => s.Id == key.Item2);
                    duplicates.Add(
                        $"  {settleA?.Name ?? $"#{key.Item1}"} <-> {settleB?.Name ?? $"#{key.Item2}"}: " +
                        $"{key.Item3} ids {roads[i].Id} & {roads[j].Id} " +
                        $"({ptsA.Count} pts vs {ptsB.Count} pts, endpoints within {overlapDist:F0}px)");
                }
            }
        }

        if (duplicates.Count > 0)
        {
            _out.WriteLine($"Geometrically redundant road pairs ({duplicates.Count}):");
            foreach (var line in duplicates) _out.WriteLine(line);
        }

        Assert.Empty(duplicates);
    }

    // ── 4. Road segments near settlements aren't excessively fanning ─────────

    /// <summary>
    /// At each settlement, count the number of distinct road polyline endpoints
    /// within 3 tiles. Settlements shouldn't have more road endpoints than their
    /// degree in the MST + shortcuts would produce. A reasonable upper bound is
    /// 12 for any single settlement (even a capital in a dense network).
    /// </summary>
    [Theory]
    [InlineData(0xCAFEBABEUL)]
    [InlineData(0xDEADBEEFUL)]
    [InlineData(0x12345678UL)]
    public void NoSettlement_HasExcessiveRoadFanout(ulong seed)
    {
        var ctx   = _cache.Get(seed);
        var world = ctx.World;
        float radiusSq = (C.WORLD_TILE_PIXELS * 3f) * (C.WORLD_TILE_PIXELS * 3f);
        const int maxEndpoints = 12;

        var excessive = new List<string>();

        foreach (var settle in world.Settlements)
        {
            if (settle.IsPoi) continue;

            var center = new Vec2(settle.WorldPixelX, settle.WorldPixelY);
            int endpointCount = 0;

            foreach (var road in world.Roads)
            {
                if (road.Points.Count < 2) continue;
                if (Vec2.DistSq(road.Points[0], center) < radiusSq) endpointCount++;
                if (Vec2.DistSq(road.Points[^1], center) < radiusSq) endpointCount++;
            }

            if (endpointCount > maxEndpoints)
            {
                excessive.Add(
                    $"  {settle.Name ?? "?"} (Tier {settle.Tier}, tile {settle.TileX},{settle.TileY}) " +
                    $"— {endpointCount} road endpoints within 3 tiles");
            }
        }

        if (excessive.Count > 0)
        {
            _out.WriteLine($"Settlements with excessive fan-out ({excessive.Count}):");
            foreach (var line in excessive) _out.WriteLine(line);
        }

        Assert.Empty(excessive);
    }
}
