using System;
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using Xunit;
using Xunit.Abstractions;

namespace CounterDrone.Core.Tests
{
    public class DefensePlannerTests
    {
        private readonly ITestOutputHelper _output;

        public DefensePlannerTests(ITestOutputHelper output) { _output = output; }
        private static readonly List<AmmunitionSpec> TestAmmo = DefaultAmmunition.GetAll();

        private static FireUnit MakeGroundUnit(string id, float posX, int munitions = 16)
            => new()
            {
                Id = id, Type = PlatformType.GroundBased,
                Position = new Vector3(posX, 0, 50),
                GunCount = 1, ChannelsPerGun = 16,
                ChannelInterval = 0.1f,
                MuzzleVelocity = 800, TotalMunitions = munitions, Cooldown = 5f,
                AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
            };

        private static FireUnit MakeAirUnit(string id, float posX)
            => new()
            {
                Id = id, Type = PlatformType.AirBased,
                Position = new Vector3(posX, 2500, -2000),
                GunCount = 1, ChannelsPerGun = 16,
                CruiseSpeed = 55, ReleaseAltitude = 1500,
                TotalMunitions = 16, Cooldown = 5f,
                AmmoTypes = new() { AerosolType.InertGas },
            };

        private static DroneGroup MakeThreat(PowerType power = PowerType.Piston, float speed = 120f,
            float startX = 0, float endX = 10000, float alt = 500)
        {
            var t = new DroneGroup
            {
                GroupId = "default",
                Target = new TargetConfig
                {
                    TargetType = (int)TargetType.Piston, PowerType = (int)power,
                    Quantity = 1, TypicalSpeed = speed, TypicalAltitude = alt,
                },
                Waypoints = new List<Waypoint>
                {
                    new() { PosX = startX, PosY = alt, PosZ = 100, Speed = speed },
                    new() { PosX = endX, PosY = alt, PosZ = 100, Speed = speed },
                },
            };
            return t;
        }

        private PlannerResult Plan(List<FireUnit> units, DroneGroup threat,
            CombatScene? env = null)
            => new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(units, new() { threat }, env ?? new CombatScene());

        // ═══════════════════════════════════════
        // 威胁排序
        // ═══════════════════════════════════════

        [Fact] public void Priority_FasterThreat_RanksHigher()
        {
            var a = MakeThreat(speed: 60); a.ArrivalTime = 50; a.ThreatIndex = 2f;
            var b = MakeThreat(speed: 500); b.ArrivalTime = 50; b.ThreatIndex = 16f;
            Assert.True(b.Priority > a.Priority);
        }

        [Fact] public void Priority_EarlierThreat_RanksHigher()
        {
            var a = MakeThreat(); a.ArrivalTime = 20; a.ThreatIndex = 2f;
            var b = MakeThreat(); b.ArrivalTime = 100; b.ThreatIndex = 2f;
            Assert.True(a.Priority > b.Priority);
        }

        // ═══════════════════════════════════════
        // 弹药匹配
        // ═══════════════════════════════════════

        [Fact] public void Ammo_Piston_MatchesInertGas()
            => Assert.Equal(AerosolType.InertGas,
                Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat(PowerType.Piston)).Best.Assignments[0].AmmoType);

        [Fact] public void Ammo_Jet_MatchesActiveMaterial()
            => Assert.Equal(AerosolType.ActiveMaterial,
                Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat(PowerType.Jet, 500)).Best.Assignments[0].AmmoType);

        [Fact] public void Ammo_Electric_MatchesInertGas()
            => Assert.Equal(AerosolType.InertGas,
                Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat(PowerType.Electric)).Best.Assignments[0].AmmoType);

        // ═══════════════════════════════════════
        // 地基弹道：每发 shellTime 因目标位置不同而不同
        // ═══════════════════════════════════════

        [Fact]
        public void Ground_FireTime_VariesPerUnitPosition()
        {
            // 两个单元不同位置 → 飞行时间不同 → 发射时间不同
            var u0 = MakeGroundUnit("u0", 5000);
            var u1 = MakeGroundUnit("u1", 8000);
            var threat = MakeThreat(speed: 120);

            var r0 = Plan(new() { u0 }, threat);
            var r1 = Plan(new() { u1 }, threat);

            float t0 = r0.Best.MergedSchedule[0].FireTime;
            float t1 = r1.Best.MergedSchedule[0].FireTime;
            Assert.NotEqual(t0, t1); // 位置不同 → 弹道不同 → 发射时刻不同
        }

        [Fact]
        public void MultiLane_3Drones_3Lanes_ExactOutput()
        {
            var threat = MakeThreat(speed: 200);
            threat.Target.Quantity = 3;
            threat.Route = new RoutePlan { FormationMode = (int)Models.FormationMode.Formation, LateralSpacing = 50 };
            threat.Waypoints.Clear();
            threat.Waypoints.Add(new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 });
            threat.Waypoints.Add(new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 });

            var result = Plan(new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5000), MakeGroundUnit("u2", 5000) }, threat);

            var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList();
            float first = events[0].FireTime, last = events[^1].FireTime;

            // 输出到控制台
            _output.WriteLine($"=== 3无人机 3车道 Planner输出 ===");
            _output.WriteLine($"总弹药: {events.Count}发, 跨度: {last-first:F2}s");
            foreach (var e in events)
                _output.WriteLine($"  FireTime={e.FireTime:F2}s Y={e.TargetY:F0} PlatIdx={e.PlatformIndex}");

            var dump = string.Join("\n", events.Select(e => $"  FireTime={e.FireTime:F2}s Y={e.TargetY:F0} PlatIdx={e.PlatformIndex}"));
            // 3 车道，弹药数取决于损伤模型
            Assert.True(events.Count >= 21, $"应≥21发, 实际{events.Count}\n{dump}");
            Assert.True(last - first > 0, $"跨度>0\n{dump}");
            // 验证每个车道有独立的 TargetZ
            var zs = events.Select(e => e.TargetZ).Distinct().ToList();
            Assert.True(zs.Count >= 2, $"应有至少2个不同Z值(distinct lanes)\n{dump}");
        }

        [Fact]
        public void Ground_AllFireTimesPositive() => Assert.All(
            Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat()).Best.MergedSchedule,
            fe => Assert.True(fe.FireTime > 0));

        [Fact]
        public void Ground_AllBeforeArrival()
        {
            var threat = MakeThreat();
            float arrival = threat.GetArrivalTime();
            Assert.All(Plan(new() { MakeGroundUnit("u0", 5000) }, threat).Best.MergedSchedule,
                fe => Assert.True(fe.FireTime < arrival, $"FireTime={fe.FireTime:F1} >= arrival={arrival:F1}"));
        }

        [Fact]
        public void Ground_MuzzleVelocity_InFireEvent()
            => Assert.All(Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat()).Best.MergedSchedule,
                fe => Assert.Equal(800f, fe.MuzzleVelocity));

        [Fact]
        public void Ground_TargetX_OnRoute()
        {
            var threat = MakeThreat(startX: 0, endX: 10000);
            Assert.All(Plan(new() { MakeGroundUnit("u0", 5000) }, threat).Best.MergedSchedule,
                fe => Assert.True(fe.TargetX >= 0 && fe.TargetX <= 10000, $"TargetX={fe.TargetX:F0}"));
        }

        // ═══════════════════════════════════════
        // 空基弹道：平台飞一次，所有弹药同飞行时间
        // ═══════════════════════════════════════

        [Fact]
        public void AirBased_PlatformFliesOnce_SameFlightTime()
        {
            // 同一平台多通道：所有弹药飞行时间相同（平台停在投放点）
            var unit = MakeAirUnit("u0", 3000);
            var result = Plan(new() { unit }, MakeThreat(speed: 120));
            var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList();
            Assert.True(events.Count >= 3);

            float expectedGap = Kinematics.CloudCoverInterval(40f, 120f, 1f);
            for (int i = 1; i < events.Count; i++)
            {
                float gap = events[i].FireTime - events[i - 1].FireTime;
                Assert.True(Math.Abs(gap - expectedGap) < 0.3f,
                    $"gap[{i}]={gap:F3}, expected {expectedGap:F2}±0.3");
            }
        }

        [Fact]
        public void AirBased_FireTime_BeforeArrival()
        {
            var threat = MakeThreat(speed: 120);
            float arrival = threat.GetArrivalTime();
            Assert.All(Plan(new() { MakeAirUnit("u0", 3000) }, threat).Best.MergedSchedule,
                fe => Assert.True(fe.FireTime < arrival - 40f,
                    $"空基FireTime={fe.FireTime:F1} 应远早于 arrival={arrival:F1}"));
        }

        [Fact]
        public void AirBased_MuzzleVelocity_Zero()
            => Assert.All(Plan(new() { MakeAirUnit("u0", 3000) }, MakeThreat()).Best.MergedSchedule,
                fe => Assert.Equal(0f, fe.MuzzleVelocity));

        [Fact]
        public void AirBased_TargetX_OnRoute()
        {
            var threat = MakeThreat(startX: 0, endX: 10000);
            Assert.All(Plan(new() { MakeAirUnit("u0", 3000) }, threat).Best.MergedSchedule,
                fe => Assert.True(fe.TargetX >= 0 && fe.TargetX <= 10000, $"TargetX={fe.TargetX:F0}"));
        }

        // ═══════════════════════════════════════
        // 分配逻辑
        // ═══════════════════════════════════════

        [Fact]
        public void Allocation_RespectsTotalMunitions()
        {
            var unit = MakeGroundUnit("u0", 5000, munitions: 2); // 只能打 2 发
            var result = Plan(new() { unit }, MakeThreat(speed: 200));
            int totalFired = result.Best.Assignments.Sum(a => a.RoundsFired);
            Assert.True(totalFired <= 2, $"fired={totalFired} > 2 munitions");
        }

        [Fact]
        public void Allocation_MultiUnit_PoolsChannels()
        {
            var result = new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(
                new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5100) },
                new() { MakeThreat(speed: 200) }, new CombatScene());
            Assert.True(result.Best.ThreatsEngaged == 1);
            Assert.True(result.Best.MergedSchedule.Count > 1);
        }

        [Fact]
        public void Allocation_IncompatibleAmmo_NotEngaged()
        {
            var unit = new FireUnit
            {
                Id = "u0", Type = PlatformType.GroundBased,
                Position = new Vector3(5000, 0, 50),
                GunCount = 1, ChannelsPerGun = 16, MuzzleVelocity = 800,
                TotalMunitions = 16,
                AmmoTypes = new() { AerosolType.ActiveMaterial }, // 无 InertGas
            };
            var result = Plan(new() { unit }, MakeThreat(PowerType.Piston));
            Assert.Equal(0, result.Best.ThreatsEngaged);
        }

        // ═══════════════════════════════════════
        // 边界
        // ═══════════════════════════════════════

        [Fact] public void Edge_NoUnits_Unengaged() => Assert.Equal(1, new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(new(), new() { MakeThreat() }, new CombatScene()).Best.ThreatsUnengaged);
        [Fact] public void Edge_NoThreats_Empty() => Assert.Equal(0, new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(new() { MakeGroundUnit("u0", 5000) }, new(), new CombatScene()).Best.ThreatsEngaged);

        [Fact]
        public void Edge_OutOfRange_Ground()
        {
            var unit = MakeGroundUnit("u0", 100000); // 极远
            var result = Plan(new() { unit }, MakeThreat());
            Assert.Equal(0, result.Best.ThreatsEngaged);
        }

        [Fact]
        public void Edge_OutOfRange_Air()
        {
            var unit = new FireUnit
            {
                Id = "u0", Type = PlatformType.AirBased,
                Position = new Vector3(100000, 2500, 100000), // 极远
                GunCount = 1, ChannelsPerGun = 4, CruiseSpeed = 10, ReleaseAltitude = 1500,
                TotalMunitions = 4, AmmoTypes = new() { AerosolType.InertGas },
            };
            var result = Plan(new() { unit }, MakeThreat());
            Assert.Equal(0, result.Best.ThreatsEngaged);
        }

        // ═══════════════════════════════════════
        // 多威胁
        // ═══════════════════════════════════════

        [Fact]
        public void MultiThreat_BothEngaged()
        {
            var a = MakeThreat(PowerType.Piston, 120); a.GroupId = "g0";
            var b = MakeThreat(PowerType.Jet, 300); b.GroupId = "g1";
            var result = new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(
                new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5100) },
                new() { a, b }, new CombatScene());
            Assert.Equal(2, result.Best.ThreatsEngaged);
        }

        // ═══════════════════════════════════════
        // 临界方案
        // ═══════════════════════════════════════

        [Fact]
        public void Critical_HasAssignments()
        {
            var result = Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat());
            Assert.True(result.Critical.Assignments.Count > 0);
            Assert.True(result.Critical.OverallProbability >= 0.4f);
        }

        // ═══════════════════════════════════════
        // P1 风偏补偿：云团生成后会被风吹偏 windVec × expansionTime，
        // Planner 须逆风预置抛撒点，使云团漂移回无人机穿越点。
        //   tx,tz = 无人机穿越点（航路中点，不变）
        //   cloudGenX/Z = 抛撒点（FireEvent.TargetX/Z，含风偏补偿）
        // ═══════════════════════════════════════

        [Fact]
        public void WindOffset_NoWind_NoDirectionalBias()
        {
            // 无风时，无论 WindDirection 取何值，抛撒点都应相同
            // （风偏补偿 = windVec × expansionTime，windVec=0 时补偿为 0）
            var threat = MakeThreat(startX: 0, endX: 10000);

            // 风向取不同值，但风速都是 0
            var envN = new CombatScene { WindSpeed = 0, WindDirection = (int)WindDirection.N };
            var envE = new CombatScene { WindSpeed = 0, WindDirection = (int)WindDirection.E };

            var rN = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, envN);
            var rE = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, envE);

            float xN = rN.Best.MergedSchedule[0].TargetX;
            float xE = rE.Best.MergedSchedule[0].TargetX;
            Assert.True(Math.Abs(xN - xE) < 1f,
                $"无风时风向不应影响抛撒点：N={xN:F1}, E={xE:F1}");
        }

        [Fact]
        public void WindOffset_EastWind_TargetShiftedUpwind()
        {
            // 东风(WindDirection.E)：WindToVector 返回 (speed, 0, 0)，云团被吹向 +X。
            // 抛撒点应逆风预置 → TargetX < 航路中点 X=5000
            var threat = MakeThreat(startX: 0, endX: 10000);
            var env = new CombatScene { WindSpeed = 10f, WindDirection = (int)WindDirection.E };

            var calmResult = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, new CombatScene { WindSpeed = 0 });
            var windyResult = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, env);

            float calmX = calmResult.Best.MergedSchedule[0].TargetX;
            float windyX = windyResult.Best.MergedSchedule[0].TargetX;
            Assert.True(windyX < calmX,
                $"东风下抛撒点 X={windyX:F1} 应小于无风 X={calmX:F1}（逆风预置补偿）");
            // 偏移量级合理性：expansionTime≈30s，风速 10m/s → 偏移约 300m
            Assert.True(calmX - windyX > 100f,
                $"偏移量 {calmX - windyX:F1}m 应有显著量级（期望 ~300m）");
        }

        [Fact]
        public void WindOffset_WestWind_TargetShiftedOpposite()
        {
            // 西风：风矢量 (-speed, 0, 0)，云团被吹向 -X。
            // 抛撒点应在 +X 方向（TargetX > 中点），与东风对称
            var threat = MakeThreat(startX: 0, endX: 10000);
            var env = new CombatScene { WindSpeed = 10f, WindDirection = (int)WindDirection.W };

            var calmResult = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, new CombatScene { WindSpeed = 0 });
            var windyResult = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, env);

            float calmX = calmResult.Best.MergedSchedule[0].TargetX;
            float windyX = windyResult.Best.MergedSchedule[0].TargetX;
            Assert.True(windyX > calmX,
                $"西风下抛撒点 X={windyX:F1} 应大于无风 X={calmX:F1}（与东风相反方向）");
        }

        [Fact]
        public void WindOffset_NorthWind_ShiftsZ()
        {
            // 北风：WindToVector 返回 (0, 0, speed)，云团被吹向 +Z。
            // 抛撒点应在 -Z 方向（TargetZ 更小）
            var threat = MakeThreat(startX: 0, endX: 10000);
            threat.Waypoints[0].PosZ = 100;
            threat.Waypoints[1].PosZ = 100;
            var env = new CombatScene { WindSpeed = 10f, WindDirection = (int)WindDirection.N };

            var calmResult = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, new CombatScene { WindSpeed = 0 });
            var windyResult = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, env);

            float calmZ = calmResult.Best.MergedSchedule[0].TargetZ;
            float windyZ = windyResult.Best.MergedSchedule[0].TargetZ;
            Assert.True(windyZ < calmZ,
                $"北风下抛撒点 Z={windyZ:F1} 应小于无风 Z={calmZ:F1}（Z 方向逆风补偿）");
        }

        // ═══════════════════════════════════════
        // 航路感知布局：云团沿真实航路方向分布，不写死 X 轴。
        // Z 向航路（南北飞）的多发云团，X 坐标应集中在航路 X 附近（非发散）。
        // ═══════════════════════════════════════

        [Fact]
        public void RouteAware_ZDirectionRoute_CloudsOnRoute()
        {
            // Z 向航路：从 (5000,0) 飞向 (5000,10000)，即沿 +Z 方向
            // 之前 offset 写死 X 轴时，多发云团会沿 X 发散到 5000±N×spacing，偏离航路
            // 改用 RouteGeometry 后，offset 沿航路切向（+Z），云团应集中在 X=5000
            var threat = new DroneGroup
            {
                GroupId = "default",
                Target = new TargetConfig
                {
                    TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
                    Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
                },
                Waypoints = new List<Waypoint>
                {
                    new() { PosX = 5000, PosY = 500, PosZ = 0,     Speed = 200 },
                    new() { PosX = 5000, PosY = 500, PosZ = 10000, Speed = 200 },
                },
            };
            // 火力单元放在航路起点附近（X=5000）
            var unit = MakeGroundUnit("u0", 5000);
            var result = Plan(new() { unit }, threat, new CombatScene());

            // 所有抛撒点的 X 应集中在 5000 附近（±少量风偏，此处无风）
            // 若 offset 仍写死 X 轴，多发会沿 X 散开到 5000±N×40
            Assert.All(result.Best.MergedSchedule, fe =>
                Assert.True(Math.Abs(fe.TargetX - 5000) < 50f,
                    $"Z 向航路下抛撒点 X={fe.TargetX:F1} 应在航路 X=5000 附近（±50m），而非沿 X 发散"));
        }
    }
}
