using Flyshot.ControllerClientCompat;
using Flyshot.Core.Config;
using Flyshot.Core.Domain;
using Flyshot.Core.Planning;
using Flyshot.Core.Planning.Sampling;
using Flyshot.Runtime.Common;
using Flyshot.Runtime.Fanuc;
using Flyshot.Runtime.Fanuc.Protocol;
using System.Globalization;
using System.Reflection;

namespace Flyshot.Core.Tests;

/// <summary>
/// 验证最小运行时编排链路会把规划结果交给控制器运行时，而不是停留在兼容层内存状态。
/// </summary>
public sealed class RuntimeOrchestrationTests
{
    /// <summary>
    /// 验证 FANUC 最小运行时执行轨迹后会更新状态快照与最终关节位置。
    /// </summary>
    [Fact]
    public void FanucControllerRuntime_ExecuteTrajectory_UpdatesSnapshotAndFinalJointPositions()
    {
        var runtime = new FanucControllerRuntime();
        var robot = TestRobotFactory.CreateRobotProfile();
        runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
        runtime.SetActiveController(sim: true);
        runtime.Connect("192.168.10.101");
        runtime.EnableRobot(bufferSize: 2);

        var result = new TrajectoryResult(
            programName: "demo",
            method: PlanningMethod.Icsp,
            isValid: true,
            duration: TimeSpan.FromSeconds(1.2),
            shotEvents: Array.Empty<ShotEvent>(),
            triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
            artifacts: Array.Empty<TrajectoryArtifact>(),
            failureReason: null,
            usedCache: false,
            originalWaypointCount: 4,
            plannedWaypointCount: 4);

        runtime.ExecuteTrajectory(result, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);

        var snapshot = runtime.GetSnapshot();
        Assert.Equal("Connected", snapshot.ConnectionState);
        Assert.False(snapshot.IsInMotion);
        Assert.Equal([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], snapshot.JointPositions);
    }

    /// <summary>
    /// 验证真机运行时会把 TCP 10010 状态通道健康度映射为可诊断连接状态。
    /// </summary>
    [Theory]
    [InlineData(FanucStateConnectionState.Connected, false, "Connected")]
    [InlineData(FanucStateConnectionState.Connected, true, "StateTimeout")]
    [InlineData(FanucStateConnectionState.TimedOut, true, "StateTimeout")]
    [InlineData(FanucStateConnectionState.Reconnecting, true, "Reconnecting")]
    [InlineData(FanucStateConnectionState.Disconnected, false, "Disconnected")]
    public void FanucControllerRuntime_ResolveRealConnectionState_ReflectsStateChannelHealth(
        FanucStateConnectionState state,
        bool isFrameStale,
        string expected)
    {
        var status = new FanucStateClientStatus(
            state,
            isFrameStale,
            lastFrameAt: null,
            reconnectAttemptCount: 0,
            lastErrorMessage: null);

        var actual = FanucControllerRuntime.ResolveRealConnectionState(status);

        Assert.Equal(expected, actual);
    }

    /// <summary>
    /// 验证只有已连接且未陈旧的 TCP 10010 帧会被 runtime 当作当前机器人状态使用。
    /// </summary>
    [Theory]
    [InlineData(FanucStateConnectionState.Connected, false, true)]
    [InlineData(FanucStateConnectionState.Connected, true, false)]
    [InlineData(FanucStateConnectionState.Reconnecting, false, false)]
    [InlineData(FanucStateConnectionState.TimedOut, false, false)]
    [InlineData(FanucStateConnectionState.Disconnected, false, false)]
    public void FanucControllerRuntime_ShouldUseStateFrame_RequiresConnectedFreshState(
        FanucStateConnectionState state,
        bool isFrameStale,
        bool expected)
    {
        var status = new FanucStateClientStatus(
            state,
            isFrameStale,
            lastFrameAt: null,
            reconnectAttemptCount: 0,
            lastErrorMessage: null);

        var actual = FanucControllerRuntime.ShouldUseStateFrame(status);

        Assert.Equal(expected, actual);
    }

    /// <summary>
    /// 验证普通轨迹会先进入 ICSP 规划，并沿用 ICSP 对示教点数量的约束。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_RejectsThreeTeachPoints()
    {
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var robot = TestRobotFactory.CreateRobotProfile();

        void Act() =>
            orchestrator.PlanOrdinaryTrajectory(
                robot,
                [
                    [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                    [0.5, 0.0, 0.0, 0.0, 0.0, 0.0],
                    [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
                ]);

        Assert.Throws<ArgumentException>(Act);
    }

    /// <summary>
    /// 验证已上传飞拍轨迹会经过 self-adapt-icsp 并生成拍照触发时间轴。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_BuildsShotTimeline()
    {
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var robot = TestRobotFactory.CreateRobotProfile();
        var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();

        var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded);

        Assert.True(bundle.Result.IsValid);
        Assert.Single(bundle.Result.ShotEvents);
        Assert.Single(bundle.Result.TriggerTimeline);
    }

    /// <summary>
    /// 验证飞拍规划会把规划限速倍率纳入速度/加速度/Jerk 限制，而不是复用运行时下发倍率。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_AppliesPlanningSpeedScaleToLimits()
    {
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var robot = TestRobotFactory.CreateRobotProfile();
        var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();

        var fullSpeed = orchestrator.PlanUploadedFlyshot(robot, uploaded, planningSpeedScale: 1.0);
        var halfSpeed = orchestrator.PlanUploadedFlyshot(robot, uploaded, planningSpeedScale: 0.5);

        Assert.True(
            halfSpeed.Result.Duration.TotalSeconds > fullSpeed.Result.Duration.TotalSeconds * 1.9,
            $"半速规划时长应接近全速的 2 倍，实际 full={fullSpeed.Result.Duration.TotalSeconds}, half={halfSpeed.Result.Duration.TotalSeconds}");
    }

    /// <summary>
    /// 验证飞拍生成轨迹时会把 speed_ratio 折算进有效规划倍率，使倍率乘积相同的配置生成等价轨迹。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_UsesSpeedRatioAsPlanningScaleFactor()
    {
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var robot = TestRobotFactory.CreateRobotProfile();
        var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
        var planningSideSlowdown = CreateFlyshotSettings(planningSpeedScale: 0.45, speedRatio: 1.0);
        var runtimeSideSlowdown = CreateFlyshotSettings(planningSpeedScale: 0.9, speedRatio: 0.5);

        var planningSideBundle = orchestrator.PlanUploadedFlyshot(robot, uploaded, settings: planningSideSlowdown);
        var runtimeSideBundle = orchestrator.PlanUploadedFlyshot(robot, uploaded, settings: runtimeSideSlowdown);

        Assert.Equal(planningSideBundle.Result.Duration.TotalSeconds, runtimeSideBundle.Result.Duration.TotalSeconds, precision: 6);
        Assert.Equal(planningSideBundle.ExecutionTrajectory.WaypointTimes.Count, runtimeSideBundle.ExecutionTrajectory.WaypointTimes.Count);
        for (var index = 0; index < planningSideBundle.ExecutionTrajectory.WaypointTimes.Count; index++)
        {
            Assert.Equal(
                planningSideBundle.ExecutionTrajectory.WaypointTimes[index],
                runtimeSideBundle.ExecutionTrajectory.WaypointTimes[index],
                precision: 6);
        }

        Assert.Equal(planningSideBundle.Result.DenseJointTrajectory!.Count, runtimeSideBundle.Result.DenseJointTrajectory!.Count);
        for (var rowIndex = 0; rowIndex < planningSideBundle.Result.DenseJointTrajectory.Count; rowIndex++)
        {
            var planningSideRow = planningSideBundle.Result.DenseJointTrajectory[rowIndex];
            var runtimeSideRow = runtimeSideBundle.Result.DenseJointTrajectory[rowIndex];
            Assert.Equal(planningSideRow.Count, runtimeSideRow.Count);
            for (var columnIndex = 0; columnIndex < planningSideRow.Count; columnIndex++)
            {
                Assert.Equal(planningSideRow[columnIndex], runtimeSideRow[columnIndex], precision: 6);
            }
        }
    }

    /// <summary>
    /// 验证普通轨迹生成也会把 speed_ratio 折算进规划倍率，避免把限速留到发送阶段处理。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_UsesSpeedRatioAsPlanningScaleFactor()
    {
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var robot = TestRobotFactory.CreateRobotProfile();
        var waypoints = new[]
        {
            new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
            new[] { 0.001, 0.0, 0.0, 0.0, 0.0, 0.0 },
            new[] { 0.002, 0.0, 0.0, 0.0, 0.0, 0.0 },
            new[] { 0.003, 0.0, 0.0, 0.0, 0.0, 0.0 }
        };

        var planningSideBundle = orchestrator.PlanOrdinaryTrajectory(
            robot,
            waypoints,
            planningSpeedScale: 0.45);
        var runtimeSideBundle = orchestrator.PlanOrdinaryTrajectory(
            robot,
            waypoints,
            planningSpeedScale: 0.9,
            speedRatio: 0.5);

        Assert.Equal(planningSideBundle.Result.Duration.TotalSeconds, runtimeSideBundle.Result.Duration.TotalSeconds, precision: 6);
        Assert.Equal(planningSideBundle.Result.DenseJointTrajectory!.Count, runtimeSideBundle.Result.DenseJointTrajectory!.Count);
        for (var rowIndex = 0; rowIndex < planningSideBundle.Result.DenseJointTrajectory.Count; rowIndex++)
        {
            var planningSideRow = planningSideBundle.Result.DenseJointTrajectory[rowIndex];
            var runtimeSideRow = runtimeSideBundle.Result.DenseJointTrajectory[rowIndex];
            for (var columnIndex = 0; columnIndex < planningSideRow.Count; columnIndex++)
            {
                Assert.Equal(planningSideRow[columnIndex], runtimeSideRow[columnIndex], precision: 6);
            }
        }
    }

    /// <summary>
    /// 验证飞拍缓存键包含规划限速倍率，避免降速验证时误用 100% 速度下的规划结果。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_CacheKeyIncludesPlanningSpeedScale()
    {
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var robot = TestRobotFactory.CreateRobotProfile();
        var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
        var options = new FlyshotExecutionOptions(useCache: true);

        var fullSpeed = orchestrator.PlanUploadedFlyshot(robot, uploaded, options, planningSpeedScale: 1.0);
        var halfSpeed = orchestrator.PlanUploadedFlyshot(robot, uploaded, options, planningSpeedScale: 0.5);

        Assert.False(halfSpeed.Result.UsedCache);
        Assert.True(halfSpeed.Result.Duration > fullSpeed.Result.Duration);
    }

    /// <summary>
    /// 验证飞拍编排会使用 RobotConfig.json 中的 IO 保持周期。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_UsesRobotSettingsForHoldCycles()
    {
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var robot = TestRobotFactory.CreateRobotProfile();
        var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
        var settings = new CompatibilityRobotSettings(
            useDo: true,
            ioAddresses: [7, 8],
            ioKeepCycles: 4,
            triggerSampleIndexOffsetCycles: 7,
            accLimitScale: 1.0,
            jerkLimitScale: 1.0,
            adaptIcspTryNum: 5);

        var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded, settings: settings);

        var doEvent = Assert.Single(bundle.Result.TriggerTimeline);
        Assert.Equal(4, doEvent.HoldCycles);
    }

    /// <summary>
    /// 验证 RobotConfig.json 关闭 use_do 时仍保留 ShotEvent 诊断信息，但不生成伺服 DO 事件。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_SuppressesDoTimeline_WhenUseDoIsFalse()
    {
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var robot = TestRobotFactory.CreateRobotProfile();
        var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
        var settings = new CompatibilityRobotSettings(
            useDo: false,
            ioAddresses: [7, 8],
            ioKeepCycles: 4,
            triggerSampleIndexOffsetCycles: 7,
            accLimitScale: 1.0,
            jerkLimitScale: 1.0,
            adaptIcspTryNum: 5);

        var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded, settings: settings);

        Assert.Single(bundle.Result.ShotEvents);
        Assert.Empty(bundle.Result.TriggerTimeline);
    }

    /// <summary>
    /// 验证普通轨迹规划后会生成稠密关节采样序列。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_ReturnsDenseJointTrajectory()
    {
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var robot = TestRobotFactory.CreateRobotProfile();

        var bundle = orchestrator.PlanOrdinaryTrajectory(
            robot,
            [
                [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                [0.1, 0.0, 0.0, 0.0, 0.0, 0.0],
                [0.2, 0.0, 0.0, 0.0, 0.0, 0.0],
                [0.3, 0.0, 0.0, 0.0, 0.0, 0.0]
            ]);

        Assert.NotNull(bundle.Result.DenseJointTrajectory);
        Assert.NotEmpty(bundle.Result.DenseJointTrajectory);

        // 验证时间单调递增。
        var times = bundle.Result.DenseJointTrajectory.Select(static row => row[0]).ToArray();
        for (var i = 1; i < times.Length; i++)
        {
            Assert.True(times[i] > times[i - 1], $"采样时间点应在索引 {i} 处单调递增。");
        }

        // 验证每行包含时间 + 6 个关节值。
        Assert.All(bundle.Result.DenseJointTrajectory, row => Assert.Equal(7, row.Count));
    }

    /// <summary>
    /// 验证飞拍轨迹规划后的稠密采样时间轴与伺服周期一致。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_DenseTrajectoryUsesServoPeriod()
    {
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var robot = TestRobotFactory.CreateRobotProfile();
        var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();

        var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded);

        Assert.NotNull(bundle.Result.DenseJointTrajectory);
        Assert.True(bundle.Result.DenseJointTrajectory.Count > 1);

        // 采样周期应为 8ms（伺服周期）。
        var firstDt = bundle.Result.DenseJointTrajectory[1][0] - bundle.Result.DenseJointTrajectory[0][0];
        Assert.Equal(0.008, firstDt, precision: 3);
    }

    /// <summary>
    /// 验证可直接使用宿主输出目录 Config/RobotConfig.json 中的真实 UTTC_MS11 示教点，
    /// 在 1x 规划速度下生成一条完整飞拍轨迹。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_FromHostRuntimeRobotConfig_GeneratesCompleteTrajectoryAtOneX()
    {
        var fixture = LoadUttcMs11RuntimeFixture();
        var orchestrator = new ControllerClientTrajectoryOrchestrator();

        var bundle = orchestrator.PlanUploadedFlyshot(
            fixture.Robot,
            fixture.Uploaded,
            settings: EnableSmoothStartStopTiming(fixture.Settings),
            planningSpeedScale: 1.0);

        Assert.Equal("UTTC_MS11", bundle.Result.ProgramName);
        Assert.NotNull(bundle.Result.DenseJointTrajectory);
        Assert.True(bundle.Result.DenseJointTrajectory.Count > fixture.Uploaded.Waypoints.Count);
        Assert.True(bundle.Result.Duration.TotalSeconds > 0.0);
        Assert.Equal(fixture.Uploaded.ShotFlags.Count(static flag => flag), bundle.Result.ShotEvents.Count);
        Assert.Equal(fixture.Uploaded.ShotFlags.Count(static flag => flag), bundle.Result.TriggerTimeline.Count);

        var firstDt = bundle.Result.DenseJointTrajectory[1][0] - bundle.Result.DenseJointTrajectory[0][0];
        Assert.Equal(fixture.Robot.ServoPeriod.TotalSeconds, firstDt, precision: 6);

        AssertJointRadiansEqual(fixture.Uploaded.Waypoints[0], bundle.Result.DenseJointTrajectory[0].Skip(1).ToArray());
        AssertJointRadiansEqual(fixture.Uploaded.Waypoints[^1], bundle.Result.DenseJointTrajectory[^1].Skip(1).ToArray());
    }

    /// <summary>
    /// 验证现场 UTTC_MS11 配置使用通用规划速度倍率拉长时间轴，并关闭二次平滑时间重映射。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_FromRuntimeLegacyFitConfig_UsesPlanningScaleWithoutSecondRemap()
    {
        var fixture = LoadUttcMs11RuntimeFixture();
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var legacyFitSettings = DisableSmoothStartStopTiming(fixture.Settings);

        var bundle = orchestrator.PlanUploadedFlyshot(
            fixture.Robot,
            fixture.Uploaded,
            settings: legacyFitSettings);
        var baselineBundle = orchestrator.PlanUploadedFlyshot(
            fixture.Robot,
            fixture.Uploaded,
            settings: EnableSmoothStartStopTiming(fixture.Settings),
            planningSpeedScale: 1.0);
        var executionDense = TrajectorySampler.SampleJointTrajectory(
            bundle.ExecutionTrajectory,
            samplePeriod: fixture.Robot.ServoPeriod.TotalSeconds);

        Assert.Equal(0.74227, legacyFitSettings.PlanningSpeedScale, precision: 6);
        Assert.False(legacyFitSettings.SmoothStartStopTiming);
        Assert.Equal(fixture.Uploaded.Waypoints.Count, bundle.PlannedTrajectory.WaypointTimes.Count);
        Assert.Equal(
            baselineBundle.PlannedTrajectory.WaypointTimes[^1] / legacyFitSettings.PlanningSpeedScale,
            bundle.PlannedTrajectory.WaypointTimes[^1],
            precision: 6);
        Assert.Equal(7.403046, bundle.PlannedTrajectory.WaypointTimes[^1], precision: 3);

        // 关闭二次时间重映射时，稠密点应和最终执行时间轴自洽；若离散限幅额外拉长，也要反映在 ExecutionTrajectory 上。
        Assert.Equal(executionDense.Count, bundle.Result.DenseJointTrajectory!.Count);
        Assert.Equal(executionDense[1][1], bundle.Result.DenseJointTrajectory[1][1], precision: 12);
        Assert.True(bundle.ExecutionTrajectory.WaypointTimes[^1] >= bundle.PlannedTrajectory.WaypointTimes[^1]);
        Assert.True(
            Math.Abs(bundle.ExecutionTrajectory.WaypointTimes[^1] - bundle.Result.Duration.TotalSeconds) < 1e-6,
            $"执行时长应保留最终执行时间轴终点，execution={bundle.ExecutionTrajectory.WaypointTimes[^1]}, result={bundle.Result.Duration.TotalSeconds}");
    }

    /// <summary>
    /// 使用运行时 RobotConfig.json 中的真实 UTTC_MS11 示教点，导出首尾 10 点整形前后的逐点对比数据，
    /// 便于和现场报警时间段逐项核对关节、速度、加速度与跃度。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_FromHostRuntimeRobotConfig_ExportsEdgeComparisonAtOneX()
    {
        const int edgePointCount = 10;
        var fixture = LoadUttcMs11RuntimeFixture();
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var bundle = orchestrator.PlanUploadedFlyshot(
            fixture.Robot,
            fixture.Uploaded,
            settings: fixture.Settings,
            planningSpeedScale: 1.0);
        var rawDense = TrajectorySampler.SampleJointTrajectory(
            bundle.PlannedTrajectory,
            samplePeriod: fixture.Robot.ServoPeriod.TotalSeconds);
        var shapedDense = bundle.Result.DenseJointTrajectory!;

        var outputDir = Path.Combine(
            fixture.ConfigRoot,
            "Data",
            "UTTC_MS11_EdgeShapeDebug_1x",
            DateTime.Now.ToString("yyyyMMdd_HHmmss_fff", CultureInfo.InvariantCulture));
        Directory.CreateDirectory(outputDir);

        var leadingPath = Path.Combine(outputDir, "Leading10Comparison.csv");
        var trailingPath = Path.Combine(outputDir, "Trailing10Comparison.csv");
        var summaryPath = Path.Combine(outputDir, "Summary.txt");

        WriteEdgeComparisonCsv(leadingPath, rawDense, shapedDense, 0, Math.Min(edgePointCount, shapedDense.Count));
        WriteEdgeComparisonCsv(
            trailingPath,
            rawDense,
            shapedDense,
            Math.Max(0, shapedDense.Count - edgePointCount),
            Math.Min(edgePointCount, shapedDense.Count));

        var summaryLines = new List<string>
        {
            $"program=UTTC_MS11",
            $"planning_speed_scale=1.000000",
            $"smooth_start_stop_timing=true",
            $"servo_period_seconds={fixture.Robot.ServoPeriod.TotalSeconds.ToString("F6", CultureInfo.InvariantCulture)}",
            $"dense_point_count={shapedDense.Count}",
            $"leading_first_step_raw_deg={ComputeStepDegrees(rawDense, 1, 0).ToString("F6", CultureInfo.InvariantCulture)}",
            $"leading_first_step_shaped_deg={ComputeStepDegrees(shapedDense, 1, 0).ToString("F6", CultureInfo.InvariantCulture)}",
            $"trailing_last_step_raw_deg={ComputeStepDegrees(rawDense, rawDense.Count - 1, 0).ToString("F6", CultureInfo.InvariantCulture)}",
            $"trailing_last_step_shaped_deg={ComputeStepDegrees(shapedDense, shapedDense.Count - 1, 0).ToString("F6", CultureInfo.InvariantCulture)}",
            $"leading_window_max_abs_raw_jerk_deg_s3={ComputeWindowMaxAbsJerkDegrees(rawDense, 0, Math.Min(edgePointCount, rawDense.Count)).ToString("F6", CultureInfo.InvariantCulture)}",
            $"leading_window_max_abs_shaped_jerk_deg_s3={ComputeWindowMaxAbsJerkDegrees(shapedDense, 0, Math.Min(edgePointCount, shapedDense.Count)).ToString("F6", CultureInfo.InvariantCulture)}",
            $"trailing_window_max_abs_raw_jerk_deg_s3={ComputeWindowMaxAbsJerkDegrees(rawDense, Math.Max(0, rawDense.Count - edgePointCount), Math.Min(edgePointCount, rawDense.Count)).ToString("F6", CultureInfo.InvariantCulture)}",
            $"trailing_window_max_abs_shaped_jerk_deg_s3={ComputeWindowMaxAbsJerkDegrees(shapedDense, Math.Max(0, shapedDense.Count - edgePointCount), Math.Min(edgePointCount, shapedDense.Count)).ToString("F6", CultureInfo.InvariantCulture)}",
            $"leading_csv={leadingPath}",
            $"trailing_csv={trailingPath}"
        };

        File.WriteAllLines(summaryPath, summaryLines);

        Assert.True(File.Exists(leadingPath));
        Assert.True(File.Exists(trailingPath));
        Assert.True(File.Exists(summaryPath));
    }

    /// <summary>
    /// 验证运行时 UTTC_MS11 在 1x 下的平滑起停会降低首尾窗口最大跃度，而不是只把第一步压小。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_FromHostRuntimeRobotConfig_ReducesEdgeWindowJerkAtOneX()
    {
        const int edgePointCount = 10;
        var fixture = LoadUttcMs11RuntimeFixture();
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var bundle = orchestrator.PlanUploadedFlyshot(
            fixture.Robot,
            fixture.Uploaded,
            settings: EnableSmoothStartStopTiming(fixture.Settings),
            planningSpeedScale: 1.0);
        var rawDense = TrajectorySampler.SampleJointTrajectory(
            bundle.PlannedTrajectory,
            samplePeriod: fixture.Robot.ServoPeriod.TotalSeconds);
        var shapedDense = bundle.Result.DenseJointTrajectory!;

        var rawLeadingMaxJerk = ComputeWindowMaxAbsJerkDegrees(rawDense, 0, Math.Min(edgePointCount, rawDense.Count));
        var shapedLeadingMaxJerk = ComputeWindowMaxAbsJerkDegrees(shapedDense, 0, Math.Min(edgePointCount, shapedDense.Count));
        var rawTrailingMaxJerk = ComputeWindowMaxAbsJerkDegrees(rawDense, Math.Max(0, rawDense.Count - edgePointCount), Math.Min(edgePointCount, rawDense.Count));
        var shapedTrailingMaxJerk = ComputeWindowMaxAbsJerkDegrees(shapedDense, Math.Max(0, shapedDense.Count - edgePointCount), Math.Min(edgePointCount, shapedDense.Count));

        Assert.True(
            shapedLeadingMaxJerk < rawLeadingMaxJerk,
            $"首段最大跃度应下降，raw={rawLeadingMaxJerk:F6}, shaped={shapedLeadingMaxJerk:F6}");
        Assert.True(
            shapedTrailingMaxJerk < rawTrailingMaxJerk,
            $"尾段最大跃度应下降，raw={rawTrailingMaxJerk:F6}, shaped={shapedTrailingMaxJerk:F6}");
    }

    /// <summary>
    /// 验证运行时 UTTC_MS11 在 1x 下的平滑起停会缩小首尾第一步，
    /// 但不能把首尾直接压成静止平台，否则现场执行会变成先停住再起步。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_FromHostRuntimeRobotConfig_ReducesEdgeStepWithoutFlatteningToZeroAtOneX()
    {
        var fixture = LoadUttcMs11RuntimeFixture();
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var bundle = orchestrator.PlanUploadedFlyshot(
            fixture.Robot,
            fixture.Uploaded,
            settings: EnableSmoothStartStopTiming(fixture.Settings),
            planningSpeedScale: 1.0);
        var rawDense = TrajectorySampler.SampleJointTrajectory(
            bundle.PlannedTrajectory,
            samplePeriod: fixture.Robot.ServoPeriod.TotalSeconds);
        var shapedDense = bundle.Result.DenseJointTrajectory!;

        var rawLeadingFirstStep = Math.Abs(ComputeStepDegrees(rawDense, 1, 0));
        var shapedLeadingFirstStep = Math.Abs(ComputeStepDegrees(shapedDense, 1, 0));
        var rawTrailingLastStep = Math.Abs(ComputeStepDegrees(rawDense, rawDense.Count - 1, 0));
        var shapedTrailingLastStep = Math.Abs(ComputeStepDegrees(shapedDense, shapedDense.Count - 1, 0));

        Assert.True(
            shapedLeadingFirstStep < rawLeadingFirstStep,
            $"首段第一步应变小，raw={rawLeadingFirstStep:F6}, shaped={shapedLeadingFirstStep:F6}");
        Assert.True(
            shapedTrailingLastStep < rawTrailingLastStep,
            $"尾段最后一步应变小，raw={rawTrailingLastStep:F6}, shaped={shapedTrailingLastStep:F6}");
        Assert.True(
            shapedLeadingFirstStep > 1e-6,
            $"首段第一步不应被压成 0，actual={shapedLeadingFirstStep:F9}");
        Assert.True(
            shapedTrailingLastStep > 1e-6,
            $"尾段最后一步不应被压成 0，actual={shapedTrailingLastStep:F9}");
    }

    /// <summary>
    /// 对比真实可运行轨迹前 0.04s 反推出的局部三次边界特征与当前首段样条系数，
    /// 用于判断当前 clamped-zero 边界是否已经和现场可运行轨迹明显偏离。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_FromHostRuntimeRobotConfig_ExportsLeadingBoundaryComparison()
    {
        var fixture = LoadUttcMs11RuntimeFixture();
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var bundle = orchestrator.PlanUploadedFlyshot(
            fixture.Robot,
            fixture.Uploaded,
            settings: fixture.Settings,
            planningSpeedScale: 1.0);
        var applyMethod = typeof(ControllerClientTrajectoryOrchestrator).GetMethod(
            "ApplySmoothStartStopTiming",
            BindingFlags.Static | BindingFlags.NonPublic);
        Assert.NotNull(applyMethod);
        var executionTrajectory = Assert.IsType<PlannedTrajectory>(applyMethod!.Invoke(null, [bundle.PlannedTrajectory]));
        var spline = new CubicSplineInterpolator(
            executionTrajectory.WaypointTimes.ToArray(),
            executionTrajectory.PlannedWaypoints.Select(static waypoint => waypoint.Positions.ToArray()).ToArray());

        var comparisonPath = Path.Combine(
            fixture.ConfigRoot,
            "Data",
            "UTTC_MS11_EdgeShapeDebug_1x",
            DateTime.Now.ToString("yyyyMMdd_HHmmss_fff", CultureInfo.InvariantCulture),
            "LeadingBoundaryComparison.txt");
        Directory.CreateDirectory(Path.GetDirectoryName(comparisonPath)!);

        var realPath = Path.Combine(
            TestRobotFactory.GetReplacementRoot(),
            "..",
            "Rvbust",
            "前两个点正常 飞拍失败的运行",
            "真实可运行的飞拍轨迹点位JointDetialTraj.txt");
        var realRows = File.ReadLines(realPath)
            .TakeWhile(static line => !string.IsNullOrWhiteSpace(line))
            .Select(ParseSpaceSeparatedDoubles)
            .Take(4)
            .ToArray();
        Assert.Equal(4, realRows.Length);

        var fieldH = typeof(CubicSplineInterpolator).GetField("_h", BindingFlags.Instance | BindingFlags.NonPublic);
        var fieldA = typeof(CubicSplineInterpolator).GetField("_a", BindingFlags.Instance | BindingFlags.NonPublic);
        var fieldB = typeof(CubicSplineInterpolator).GetField("_b", BindingFlags.Instance | BindingFlags.NonPublic);
        var fieldC = typeof(CubicSplineInterpolator).GetField("_c", BindingFlags.Instance | BindingFlags.NonPublic);
        Assert.NotNull(fieldH);
        Assert.NotNull(fieldA);
        Assert.NotNull(fieldB);
        Assert.NotNull(fieldC);

        var segmentDurations = Assert.IsType<double[]>(fieldH!.GetValue(spline));
        var coeffA = Assert.IsType<double[][]>(fieldA!.GetValue(spline));
        var coeffB = Assert.IsType<double[][]>(fieldB!.GetValue(spline));
        var coeffC = Assert.IsType<double[][]>(fieldC!.GetValue(spline));

        var lines = new List<string>
        {
            $"real_path={Path.GetFullPath(realPath)}",
            $"current_segment0_h_seconds={segmentDurations[0].ToString("F9", CultureInfo.InvariantCulture)}",
            $"real_window_t0={realRows[0][0].ToString("F9", CultureInfo.InvariantCulture)}",
            $"real_window_t1={realRows[1][0].ToString("F9", CultureInfo.InvariantCulture)}",
            $"real_window_t2={realRows[2][0].ToString("F9", CultureInfo.InvariantCulture)}",
            $"real_window_t3={realRows[3][0].ToString("F9", CultureInfo.InvariantCulture)}"
        };

        for (var jointIndex = 0; jointIndex < 6; jointIndex++)
        {
            var currentStartVelocityDeg = RadiansToDegrees(coeffC[0][jointIndex]);
            var currentStartAccelerationDeg = RadiansToDegrees(2.0 * coeffB[0][jointIndex]);
            var currentJerkDeg = RadiansToDegrees(6.0 * coeffA[0][jointIndex]);
            var currentEndVelocityDeg = RadiansToDegrees(
                (3.0 * coeffA[0][jointIndex] * segmentDurations[0] * segmentDurations[0])
                + (2.0 * coeffB[0][jointIndex] * segmentDurations[0])
                + coeffC[0][jointIndex]);

            var localRealFit = FitCubicFromFirstFourRows(realRows, jointIndex + 1);
            var realStartVelocityDeg = localRealFit.Coefficient1;
            var realStartAccelerationDeg = 2.0 * localRealFit.Coefficient2;
            var realJerkDeg = 6.0 * localRealFit.Coefficient3;
            var realEndVelocityDeg = (3.0 * localRealFit.Coefficient3 * localRealFit.LastLocalTime * localRealFit.LastLocalTime)
                + (2.0 * localRealFit.Coefficient2 * localRealFit.LastLocalTime)
                + localRealFit.Coefficient1;

            lines.Add(
                $"J{jointIndex + 1}: current_start_v_deg_s={currentStartVelocityDeg:F9}, real_start_v_deg_s={realStartVelocityDeg:F9}, current_start_a_deg_s2={currentStartAccelerationDeg:F9}, real_start_a_deg_s2={realStartAccelerationDeg:F9}, current_jerk_deg_s3={currentJerkDeg:F9}, real_jerk_deg_s3={realJerkDeg:F9}, current_end_v_deg_s={currentEndVelocityDeg:F9}, real_end_v_deg_s={realEndVelocityDeg:F9}");
        }

        File.WriteAllLines(comparisonPath, lines);
        Assert.True(File.Exists(comparisonPath));
    }

    /// <summary>
    /// 验证飞拍规划结果会应用平滑起停时间轴，而不是直接复用原始线性时间轴采样。
    /// </summary>
    [Fact]
    public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_ShapesDenseTrajectoryEdges()
    {
        var orchestrator = new ControllerClientTrajectoryOrchestrator();
        var robot = TestRobotFactory.CreateRobotProfile();
        var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();

        var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded);
        var rawDense = TrajectorySampler.SampleJointTrajectory(
            bundle.PlannedTrajectory,
            samplePeriod: robot.ServoPeriod.TotalSeconds);

        Assert.NotNull(bundle.Result.DenseJointTrajectory);
        Assert.True(bundle.Result.DenseJointTrajectory.Count > 4);

        var dense = bundle.Result.DenseJointTrajectory;
        Assert.NotEqual(rawDense[1][1], dense[1][1]);
        Assert.NotEqual(rawDense[^2][1], dense[^2][1]);
        Assert.Equal(rawDense[0][1], dense[0][1], precision: 6);
        Assert.Equal(rawDense[^1][1], dense[^1][1], precision: 6);
        Assert.Equal(rawDense.Count, dense.Count);
    }

    /// <summary>
    /// 验证旧的首尾整形工具在独立调用时仍会改动首尾窗口，供对比和回归保留。
    /// </summary>
    [Fact]
    public void FlyshotTrajectoryEdgeShaper_ShapesLeadingAndTrailingStepsWithoutFlatteningMiddleSection()
    {
        var denseTrajectory = new IReadOnlyList<double>[]
        {
            [0.000, DegreesToRadians(60.546226), 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.008, DegreesToRadians(60.541482), 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.016, DegreesToRadians(60.536738), 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.024, DegreesToRadians(60.531994), 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.032, DegreesToRadians(60.527250), 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.040, DegreesToRadians(60.522506), 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.048, DegreesToRadians(60.517762), 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.056, DegreesToRadians(60.513018), 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.064, DegreesToRadians(60.508274), 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.072, DegreesToRadians(60.503530), 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.080, DegreesToRadians(60.498786), 0.0, 0.0, 0.0, 0.0, 0.0]
        };

        var shaped = FlyshotTrajectoryEdgeShaper.ShapeDenseJointTrajectory(denseTrajectory);

        Assert.Equal(denseTrajectory.Length, shaped.Count);
        Assert.Equal(denseTrajectory[0][0], shaped[0][0], precision: 6);
        Assert.Equal(denseTrajectory[^1][0], shaped[^1][0], precision: 6);
        Assert.Equal(denseTrajectory[0][1], shaped[0][1], precision: 12);
        Assert.Equal(denseTrajectory[^1][1], shaped[^1][1], precision: 12);

        var firstStepDegrees = Math.Abs(RadiansToDegrees(shaped[1][1] - shaped[0][1]));
        var lastStepDegrees = Math.Abs(RadiansToDegrees(shaped[^1][1] - shaped[^2][1]));
        var originalFirstStepDegrees = Math.Abs(RadiansToDegrees(denseTrajectory[1][1] - denseTrajectory[0][1]));
        var originalLastStepDegrees = Math.Abs(RadiansToDegrees(denseTrajectory[^1][1] - denseTrajectory[^2][1]));
        Assert.NotEqual(originalFirstStepDegrees, firstStepDegrees);
        Assert.NotEqual(originalLastStepDegrees, lastStepDegrees);

        var middleStepDegrees = Math.Abs(RadiansToDegrees(shaped[5][1] - shaped[4][1]));
        Assert.True(middleStepDegrees > 0.002, $"中段单步变化不应被整体压平，actual={middleStepDegrees:F6}deg");
    }

    /// <summary>
    /// 验证飞拍首尾整形只覆盖首尾各 10 个点，锚点之外的中段采样保持不变。
    /// </summary>
    [Fact]
    public void FlyshotTrajectoryEdgeShaper_ShapesOnlyLeadingAndTrailingTenPoints()
    {
        var denseTrajectory = Enumerable.Range(0, 31)
            .Select(index => (IReadOnlyList<double>)
            [
                index * 0.008,
                DegreesToRadians(60.0 - index),
                DegreesToRadians(index * 0.1),
                DegreesToRadians(-index * 0.5),
                DegreesToRadians(index * 0.02),
                DegreesToRadians(index * 0.2),
                DegreesToRadians(index * 0.05)
            ])
            .ToArray();

        var shaped = FlyshotTrajectoryEdgeShaper.ShapeDenseJointTrajectory(denseTrajectory);

        Assert.Equal(denseTrajectory.Length, shaped.Count);

        Assert.NotEqual(denseTrajectory[1][1], shaped[1][1]);
        Assert.NotEqual(denseTrajectory[9][1], shaped[9][1]);
        Assert.Equal(denseTrajectory[10][1], shaped[10][1], precision: 12);
        Assert.Equal(denseTrajectory[11][1], shaped[11][1], precision: 12);
        Assert.Equal(denseTrajectory[19][1], shaped[19][1], precision: 12);
        Assert.Equal(denseTrajectory[20][1], shaped[20][1], precision: 12);
        Assert.NotEqual(denseTrajectory[21][1], shaped[21][1]);
        Assert.NotEqual(denseTrajectory[29][1], shaped[29][1]);
        Assert.Equal(denseTrajectory[30][1], shaped[30][1], precision: 12);
    }

    /// <summary>
    /// 验证兼容服务执行普通轨迹时会进入规划链路，而不是直接把最后一个路点写入状态。
    /// </summary>
    [Fact]
    public void ControllerClientCompatService_ExecuteTrajectory_RejectsThreeTeachPointsAfterPlanningIsIntroduced()
    {
        var service = TestRobotFactory.CreateCompatService();
        service.SetUpRobot("FANUC_LR_Mate_200iD");
        service.SetActiveController(sim: true);
        service.Connect("192.168.10.101");
        service.EnableRobot(2);

        void Act() =>
            service.ExecuteTrajectory(
                [
                    [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                    [0.5, 0.0, 0.0, 0.0, 0.0, 0.0],
                    [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
                ]);

        Assert.Throws<ArgumentException>(Act);
    }

    /// <summary>
    /// 验证兼容服务执行普通轨迹时会把运行时 speed_ratio 前移到规划阶段。
    /// </summary>
    [Fact]
    public void ControllerClientCompatService_ExecuteTrajectory_PassesSpeedRatioIntoOrdinaryPlanning()
    {
        var configRoot = CreateTempConfigRoot();
        try
        {
            WriteRobotConfigWithDemoTrajectory(configRoot, planningSpeedScale: 0.9, speedRatio: 1.0);
            var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
            var runtime = new RecordingControllerRuntime();
            var service = new ControllerClientCompatService(
                options,
                new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
                runtime,
                new ControllerClientTrajectoryOrchestrator(),
                new RobotConfigLoader());
            var waypoints = new[]
            {
                new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
                new[] { 0.001, 0.0, 0.0, 0.0, 0.0, 0.0 },
                new[] { 0.002, 0.0, 0.0, 0.0, 0.0, 0.0 },
                new[] { 0.003, 0.0, 0.0, 0.0, 0.0, 0.0 }
            };

            service.SetUpRobot("FANUC_LR_Mate_200iD");
            service.SetActiveController(sim: false);
            service.Connect("192.168.10.101");
            service.EnableRobot(2);
            runtime.SetSpeedRatio(0.5);

            service.ExecuteTrajectory(waypoints);

            var robot = new ControllerClientCompatRobotCatalog(options, new RobotModelLoader())
                .LoadProfile("FANUC_LR_Mate_200iD", accLimitScale: 1.0, jerkLimitScale: 1.0);
            var expected = new ControllerClientTrajectoryOrchestrator()
                .PlanOrdinaryTrajectory(robot, waypoints, planningSpeedScale: 0.45);
            var actual = Assert.IsType<TrajectoryResult>(runtime.LastExecutedResult);
            Assert.Equal(expected.Result.Duration.TotalSeconds, actual.Duration.TotalSeconds, precision: 6);
            Assert.Equal(expected.Result.DenseJointTrajectory!.Count, actual.DenseJointTrajectory!.Count);
        }
        finally
        {
            Directory.Delete(configRoot, recursive: true);
        }
    }

    /// <summary>
    /// 验证 ExecuteFlyShotTraj(move_to_start=true) 会先执行稠密 PTP 到起点，并等待该段运动完成后再启动飞拍轨迹。
    /// </summary>
    [Fact]
    public void ControllerClientCompatService_ExecuteTrajectoryByName_MoveToStartWaitsBeforeFlyshot()
    {
        var configRoot = CreateTempConfigRoot();
        try
        {
            WriteRobotConfigWithDemoTrajectory(configRoot);
            var options = new ControllerClientCompatOptions
            {
                ConfigRoot = configRoot
            };
            var runtime = new DelayedCompletionControllerRuntime(
                initialJointPositions: [0.4, 0.0, 0.0, 0.0, 0.0, 0.0],
                firstMotionCompletionDelay: TimeSpan.FromMilliseconds(80));
            var service = new ControllerClientCompatService(
                options,
                new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
                runtime,
                new ControllerClientTrajectoryOrchestrator(),
                new RobotConfigLoader());

            service.SetUpRobot("FANUC_LR_Mate_200iD");
            service.SetActiveController(sim: false);
            service.Connect("192.168.10.101");
            service.EnableRobot(2);
            service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());

            service.ExecuteTrajectoryByName(
                "demo-flyshot",
                new FlyshotExecutionOptions(moveToStart: true, method: "icsp", saveTrajectory: false, useCache: false));

            Assert.True(runtime.ExecuteCalls.Count >= 2);
            Assert.NotNull(runtime.ExecuteCalls[0].Result.DenseJointTrajectory);
            Assert.True(runtime.ExecuteCalls[0].Result.DenseJointTrajectory!.Count > 1);
            Assert.False(runtime.SecondTrajectoryStartedBeforeFirstMotionCompleted);
        }
        finally
        {
            Directory.Delete(configRoot, recursive: true);
        }
    }

    /// <summary>
    /// 验证正式飞拍前若真实反馈仍未接近起点，则兼容层会拒绝继续执行。
    /// </summary>
    [Fact]
    public void ControllerClientCompatService_ExecuteTrajectoryByName_RejectsFlyshotWhenFeedbackIsNotNearStartAfterMoveToStart()
    {
        var configRoot = CreateTempConfigRoot();
        try
        {
            WriteRobotConfigWithDemoTrajectory(configRoot);
            var options = new ControllerClientCompatOptions
            {
                ConfigRoot = configRoot
            };
            var runtime = new StickyFeedbackControllerRuntime(
                initialJointPositions: [0.4, 0.0, 0.0, 0.0, 0.0, 0.0],
                firstMotionCompletionDelay: TimeSpan.FromMilliseconds(20));
            var service = new ControllerClientCompatService(
                options,
                new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
                runtime,
                new ControllerClientTrajectoryOrchestrator(),
                new RobotConfigLoader());

            service.SetUpRobot("FANUC_LR_Mate_200iD");
            service.SetActiveController(sim: false);
            service.Connect("192.168.10.101");
            service.EnableRobot(2);
            service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());

            var exception = Assert.Throws<InvalidOperationException>(() =>
                service.ExecuteTrajectoryByName(
                    "demo-flyshot",
                    new FlyshotExecutionOptions(moveToStart: true, method: "icsp", saveTrajectory: false, useCache: false)));

            Assert.Contains("not near flyshot start", exception.Message, StringComparison.OrdinalIgnoreCase);
            Assert.Single(runtime.ExecuteCalls);
        }
        finally
        {
            Directory.Delete(configRoot, recursive: true);
        }
    }

    /// <summary>
    /// 验证 ExecuteFlyShotTraj(wait=true) 会等待正式飞拍轨迹完成后再返回。
    /// </summary>
    [Fact]
    public void ControllerClientCompatService_ExecuteTrajectoryByName_WaitTrueWaitsForFlyshotCompletion()
    {
        var configRoot = CreateTempConfigRoot();
        try
        {
            WriteRobotConfigWithDemoTrajectory(configRoot);
            var options = new ControllerClientCompatOptions
            {
                ConfigRoot = configRoot
            };
            var runtime = new DelayedCompletionControllerRuntime(
                initialJointPositions: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                firstMotionCompletionDelay: TimeSpan.FromMilliseconds(80));
            var service = new ControllerClientCompatService(
                options,
                new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
                runtime,
                new ControllerClientTrajectoryOrchestrator(),
                new RobotConfigLoader());

            service.SetUpRobot("FANUC_LR_Mate_200iD");
            service.SetActiveController(sim: false);
            service.Connect("192.168.10.101");
            service.EnableRobot(2);
            service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());

            service.ExecuteTrajectoryByName(
                "demo-flyshot",
                new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false, wait: true));

            Assert.Single(runtime.ExecuteCalls);
            Assert.False(runtime.GetSnapshot().IsInMotion);
        }
        finally
        {
            Directory.Delete(configRoot, recursive: true);
        }
    }

    /// <summary>
    /// 验证飞拍执行阻塞在运行时时，状态页元数据快照仍能通过短锁快速返回。
    /// </summary>
    [Fact]
    public async Task ControllerClientCompatService_GetStatusSnapshotMetadata_DoesNotWaitForRunningFlyshot()
    {
        var configRoot = CreateTempConfigRoot();
        try
        {
            WriteRobotConfigWithDemoTrajectory(configRoot);
            var options = new ControllerClientCompatOptions
            {
                ConfigRoot = configRoot
            };
            var runtime = new BlockingExecutionControllerRuntime([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
            var service = new ControllerClientCompatService(
                options,
                new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
                runtime,
                new ControllerClientTrajectoryOrchestrator(),
                new RobotConfigLoader());

            service.SetUpRobot("FANUC_LR_Mate_200iD");
            service.SetActiveController(sim: false);
            service.Connect("192.168.10.101");
            service.EnableRobot(2);
            service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());

            var executing = Task.Run(() => service.ExecuteTrajectoryByName(
                "demo-flyshot",
                new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false, wait: true)));
            Assert.True(runtime.WaitForExecutionStarted(TimeSpan.FromSeconds(2)));

            var metadataTask = Task.Run(() => service.GetStatusSnapshotMetadata());
            var completed = await Task.WhenAny(metadataTask, Task.Delay(TimeSpan.FromMilliseconds(150)));

            runtime.ReleaseExecution();
            await executing;

            Assert.Same(metadataTask, completed);
            var metadata = await metadataTask;
            Assert.True(metadata.IsSetup);
            Assert.Equal("FANUC_LR_Mate_200iD", metadata.RobotName);
            Assert.Equal(["demo-flyshot"], metadata.UploadedTrajectories);
        }
        finally
        {
            Directory.Delete(configRoot, recursive: true);
        }
    }

    /// <summary>
    /// 验证两个飞拍执行命令必须串行进入 runtime，避免 J519 队列被并发执行覆盖。
    /// </summary>
    [Fact]
    public async Task ControllerClientCompatService_ExecuteTrajectoryByName_SerializesConcurrentExecutionCommands()
    {
        var configRoot = CreateTempConfigRoot();
        try
        {
            WriteRobotConfigWithDemoTrajectory(configRoot);
            var options = new ControllerClientCompatOptions
            {
                ConfigRoot = configRoot
            };
            var runtime = new BlockingExecutionControllerRuntime([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
            var service = new ControllerClientCompatService(
                options,
                new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
                runtime,
                new ControllerClientTrajectoryOrchestrator(),
                new RobotConfigLoader());

            service.SetUpRobot("FANUC_LR_Mate_200iD");
            service.SetActiveController(sim: false);
            service.Connect("192.168.10.101");
            service.EnableRobot(2);
            service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());

            var first = Task.Run(() => service.ExecuteTrajectoryByName(
                "demo-flyshot",
                new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false, wait: true)));
            Assert.True(runtime.WaitForExecutionStarted(TimeSpan.FromSeconds(2)));

            var second = Task.Run(() => service.ExecuteTrajectoryByName(
                "demo-flyshot",
                new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false, wait: true)));
            await Task.Delay(TimeSpan.FromMilliseconds(100));

            Assert.Equal(1, runtime.ExecuteCallCount);

            runtime.ReleaseExecution();
            await first;
            Assert.True(runtime.WaitForExecutionStarted(TimeSpan.FromSeconds(2), expectedCallCount: 2));
            runtime.ReleaseExecution();
            await second;

            Assert.Equal(2, runtime.ExecuteCallCount);
        }
        finally
        {
            Directory.Delete(configRoot, recursive: true);
        }
    }

    /// <summary>
    /// 验证飞拍链路在进入运行时前就会准备最终发送队列，而不是把 speedRatio 重采样留给运行时临场处理。
    /// </summary>
    [Fact]
    public void ControllerClientCompatService_ExecuteTrajectoryByName_PreparesFinalSendQueueBeforeRuntime()
    {
        var configRoot = CreateTempConfigRoot();
        try
        {
            WriteRobotConfigWithDemoTrajectory(configRoot);
            var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
            var runtime = new RecordingControllerRuntime();
            var service = new ControllerClientCompatService(
                options,
                new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
                runtime,
                new ControllerClientTrajectoryOrchestrator(),
                new RobotConfigLoader());

            service.SetUpRobot("FANUC_LR_Mate_200iD");
            service.SetActiveController(sim: false);
            service.Connect("192.168.10.101");
            service.EnableRobot(2);
            service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
            runtime.SetSpeedRatio(0.5);

            service.ExecuteTrajectoryByName(
                "demo-flyshot",
                new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false));

            var result = Assert.IsType<TrajectoryResult>(runtime.LastExecutedResult);
            var preparedExecution = Assert.IsType<FlyshotPreparedExecution>(result.PreparedFlyshotExecution);
            Assert.NotEmpty(preparedExecution.Samples);
            Assert.Equal(preparedExecution.Samples.Count, preparedExecution.TimingRows.Count);
            Assert.Equal(result.TriggerTimeline.Count, preparedExecution.TriggerBindings.Count);
        }
        finally
        {
            Directory.Delete(configRoot, recursive: true);
        }
    }

    /// <summary>
    /// 验证兼容服务初始化机器人时会把 RobotConfig.json 中的 acc_limit / jerk_limit 传给模型加载器。
    /// </summary>
    [Fact]
    public void ControllerClientCompatService_SetUpRobot_AppliesRobotConfigLimitScales()
    {
        var configRoot = CreateTempConfigRoot();
        try
        {
            File.WriteAllText(
                Path.Combine(configRoot, "RobotConfig.json"),
                """
                {
                  "robot": {
                    "use_do": true,
                    "io_addr": [7, 8],
                    "io_keep_cycles": 4,
                    "acc_limit": 0.5,
                    "jerk_limit": 0.25,
                    "adapt_icsp_try_num": 3,
                    "speed_ratio": 0.65
                  },
                  "flying_shots": {}
                }
                """);

            var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
            var runtime = new RecordingControllerRuntime();
            var service = new ControllerClientCompatService(
                options,
                new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
                runtime,
                new ControllerClientTrajectoryOrchestrator(),
                new RobotConfigLoader());

            service.SetUpRobot("FANUC_LR_Mate_200iD");

            var profile = Assert.IsType<RobotProfile>(runtime.LastRobotProfile);
            Assert.Equal(14.905, profile.JointLimits[2].AccelerationLimit, precision: 3);
            Assert.Equal(62.115, profile.JointLimits[2].JerkLimit, precision: 3);
            Assert.Equal(0.65, runtime.GetSpeedRatio(), precision: 6);
        }
        finally
        {
            Directory.Delete(configRoot, recursive: true);
        }
    }

    /// <summary>
    /// 验证 IsFlyshotTrajectoryValid(saveTrajectory=true) 会把规划后的结果点位导出到 Config/Data/name。
    /// </summary>
    [Fact]
    public void ControllerClientCompatService_IsFlyshotTrajectoryValid_SaveTrajectoryExportsPlannedData()
    {
        var configRoot = CreateTempConfigRoot();
        try
        {
            WriteRobotConfigWithDemoTrajectory(configRoot);
            var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
            var service = new ControllerClientCompatService(
                options,
                new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
                new RecordingControllerRuntime(),
                new ControllerClientTrajectoryOrchestrator(),
                new RobotConfigLoader());

            service.SetUpRobot("FANUC_LR_Mate_200iD");

            var valid = service.IsFlyshotTrajectoryValid(
                out var duration,
                "demo-flyshot",
                method: "icsp",
                saveTrajectory: true);

            var outputDir = Path.Combine(configRoot, "Data", "demo-flyshot");
            Assert.True(valid);
            Assert.True(duration > TimeSpan.Zero);
            Assert.True(File.Exists(Path.Combine(outputDir, "JointTraj.txt")));
            Assert.True(File.Exists(Path.Combine(outputDir, "JointDetialTraj.txt")));
            Assert.True(File.Exists(Path.Combine(outputDir, "CartTraj.txt")));
            Assert.True(File.Exists(Path.Combine(outputDir, "CartDetialTraj.txt")));
            Assert.True(File.Exists(Path.Combine(outputDir, "ShotEvents.json")));
            Assert.NotEmpty(File.ReadAllLines(Path.Combine(outputDir, "JointDetialTraj.txt")));
            Assert.NotEmpty(File.ReadAllLines(Path.Combine(outputDir, "CartDetialTraj.txt")));
        }
        finally
        {
            Directory.Delete(configRoot, recursive: true);
        }
    }

    /// <summary>
    /// 验证 SaveTrajectoryInfo 会导出按 J519 8ms 实发周期重采样的点位，并保留当前 speed_ratio 诊断信息。
    /// </summary>
    [Fact]
    public void ControllerClientCompatService_SaveTrajectoryInfo_ExportsActualSendRowsWithSpeedRatio()
    {
        var configRoot = CreateTempConfigRoot();
        try
        {
            WriteRobotConfigWithDemoTrajectory(configRoot);
            var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
            var runtime = new RecordingControllerRuntime();
            var service = new ControllerClientCompatService(
                options,
                new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
                runtime,
                new ControllerClientTrajectoryOrchestrator(),
                new RobotConfigLoader());

            service.SetUpRobot("FANUC_LR_Mate_200iD");
            runtime.SetSpeedRatio(0.5);

            service.SaveTrajectoryInfo("demo-flyshot");

            var outputDir = Path.Combine(configRoot, "Data", "demo-flyshot");
            var pointsPath = Path.Combine(outputDir, "ActualSendJointTraj.txt");
            var timingPath = Path.Combine(outputDir, "ActualSendTiming.txt");
            var shotEventsPath = Path.Combine(outputDir, "ShotEvents.json");
            Assert.True(File.Exists(pointsPath));
            Assert.True(File.Exists(timingPath));
            Assert.True(File.Exists(shotEventsPath));

            var pointRows = File.ReadAllLines(pointsPath).Select(ParseSpaceSeparatedDoubles).ToArray();
            var timingRows = File.ReadAllLines(timingPath).Select(ParseSpaceSeparatedDoubles).ToArray();
            var shotEventsJson = File.ReadAllText(shotEventsPath);
            var planningExportDuration = double.Parse(
                File.ReadLines(Path.Combine(outputDir, "JointDetialTraj.txt")).Last().Split(' ')[0],
                CultureInfo.InvariantCulture);
            var planningExportRowsAtServoPeriod = (int)Math.Ceiling(Math.Max(0.0, (planningExportDuration / 0.008) - 1e-9)) + 1;

            Assert.Equal(pointRows.Length, timingRows.Length);
            Assert.True(
                pointRows.Length > planningExportRowsAtServoPeriod,
                $"ActualSend 应按当前 speedRatio 的执行视图导出，不能再用旧 JointDetialTraj 规划时长约束点数。actual={pointRows.Length}, planningRows={planningExportRowsAtServoPeriod}");
            Assert.Equal(0.0, pointRows[0][0], precision: 6);
            Assert.Equal(0.008, pointRows[1][0], precision: 6);
            Assert.Equal(0.008, timingRows[1][2], precision: 6);
            Assert.Equal(1.0, timingRows[1][3], precision: 6);
            Assert.Equal(0.5, timingRows[1][4], precision: 6);
            Assert.Equal(0.0, timingRows[1][5], precision: 6);
            Assert.Contains("\"trigger_window_seconds\": 0.1", shotEventsJson);
            Assert.Contains("\"selected_sample_index\"", shotEventsJson);
        }
        finally
        {
            Directory.Delete(configRoot, recursive: true);
        }
    }

    /// <summary>
    /// 验证 SaveTrajectoryInfo 的旧格式规划导出只受 planning_speed_scale 影响，不跟随运行时 speed_ratio 改变。
    /// </summary>
    [Fact]
    public void ControllerClientCompatService_SaveTrajectoryInfo_KeepsLegacyPlanningExportsIndependentFromSpeedRatio()
    {
        var fullSpeedRoot = CreateTempConfigRoot();
        var halfSpeedRoot = CreateTempConfigRoot();
        try
        {
            WriteRobotConfigWithDemoTrajectory(fullSpeedRoot, planningSpeedScale: 0.45, speedRatio: 1.0);
            WriteRobotConfigWithDemoTrajectory(halfSpeedRoot, planningSpeedScale: 0.45, speedRatio: 0.5);

            SaveDemoTrajectoryInfo(fullSpeedRoot);
            SaveDemoTrajectoryInfo(halfSpeedRoot);

            var fullSpeedOutput = Path.Combine(fullSpeedRoot, "Data", "demo-flyshot");
            var halfSpeedOutput = Path.Combine(halfSpeedRoot, "Data", "demo-flyshot");
            AssertNumericFilesEqual(
                Path.Combine(fullSpeedOutput, "JointTraj.txt"),
                Path.Combine(halfSpeedOutput, "JointTraj.txt"));
            AssertNumericFilesEqual(
                Path.Combine(fullSpeedOutput, "JointDetialTraj.txt"),
                Path.Combine(halfSpeedOutput, "JointDetialTraj.txt"));
            AssertNumericFilesEqual(
                Path.Combine(fullSpeedOutput, "CartTraj.txt"),
                Path.Combine(halfSpeedOutput, "CartTraj.txt"));
            AssertNumericFilesEqual(
                Path.Combine(fullSpeedOutput, "CartDetialTraj.txt"),
                Path.Combine(halfSpeedOutput, "CartDetialTraj.txt"));

            var fullSpeedActualSend = File.ReadAllLines(Path.Combine(fullSpeedOutput, "ActualSendJointTraj.txt"));
            var halfSpeedActualSend = File.ReadAllLines(Path.Combine(halfSpeedOutput, "ActualSendJointTraj.txt"));
            var fullSpeedTiming = File.ReadAllLines(Path.Combine(fullSpeedOutput, "ActualSendTiming.txt"));
            var halfSpeedTiming = File.ReadAllLines(Path.Combine(halfSpeedOutput, "ActualSendTiming.txt"));
            Assert.NotEqual(fullSpeedActualSend.Length, halfSpeedActualSend.Length);
            Assert.NotEqual(fullSpeedTiming.Length, halfSpeedTiming.Length);
        }
        finally
        {
            Directory.Delete(fullSpeedRoot, recursive: true);
            Directory.Delete(halfSpeedRoot, recursive: true);
        }
    }

    /// <summary>
    /// 验证 saveTrajectory 导出的 JointDetialTraj.txt 来自旧规划导出视图的 16ms 明细轨迹，
    /// 而不是来自 speed_ratio 后的实发执行视图。
    /// </summary>
    [Fact]
    public void FlyshotTrajectoryArtifactWriter_WriteUploadedFlyshot_JointDetailUsesExportPlanningDenseDownsample()
    {
        var fixture = LoadUttcMs11RuntimeFixture();
        var configRoot = CreateTempConfigRoot();
        try
        {
            var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
            var writer = new FlyshotTrajectoryArtifactWriter(options, new RobotModelLoader());
            var orchestrator = new ControllerClientTrajectoryOrchestrator();
            var exportBundle = orchestrator.PlanUploadedFlyshot(
                fixture.Robot,
                fixture.Uploaded,
                settings: EnableSmoothStartStopTiming(fixture.Settings),
                planningSpeedScale: 1.0);
            var executionBundle = orchestrator.PlanUploadedFlyshot(
                fixture.Robot,
                fixture.Uploaded,
                settings: EnableSmoothStartStopTiming(fixture.Settings),
                planningSpeedScale: 1.0,
                speedRatio: 0.5);

            writer.WriteUploadedFlyshot("UTTC_MS11", fixture.Robot, exportBundle, executionBundle, speedRatio: 0.5);

            var outputDir = Path.Combine(configRoot, "Data", "UTTC_MS11");
            var exportedRows = File.ReadAllLines(Path.Combine(outputDir, "JointDetialTraj.txt"))
                .Select(ParseSpaceSeparatedDoubles)
                .ToArray();
            var expectedRows = DownsampleDenseRows(
                exportBundle.Result.DenseJointTrajectory!,
                samplePeriodSeconds: 0.016)
                .Select(static row => row.ToArray())
                .ToArray();

            Assert.Equal(expectedRows.Length, exportedRows.Length);
            for (var rowIndex = 0; rowIndex < expectedRows.Length; rowIndex++)
            {
                Assert.Equal(expectedRows[rowIndex].Length, exportedRows[rowIndex].Length);
                for (var columnIndex = 0; columnIndex < expectedRows[rowIndex].Length; columnIndex++)
                {
                    Assert.Equal(expectedRows[rowIndex][columnIndex], exportedRows[rowIndex][columnIndex], precision: 6);
                }
            }
        }
        finally
        {
            Directory.Delete(configRoot, recursive: true);
        }
    }

    /// <summary>
    /// 创建只包含当前支持机器人 JSON 模型和 RobotConfig.json 的临时运行配置根。
    /// </summary>
    private static string CreateTempConfigRoot()
    {
        var configRoot = Path.Combine(Path.GetTempPath(), "flyshot-runtime-tests", Guid.NewGuid().ToString("N"), "Config");
        var modelDir = Path.Combine(configRoot, "Models");
        Directory.CreateDirectory(modelDir);

        var sourceModel = Path.Combine(
            TestRobotFactory.GetReplacementRoot(),
            "Config",
            "Models",
            "LR_Mate_200iD_7L.json");
        File.Copy(sourceModel, Path.Combine(modelDir, "LR_Mate_200iD_7L.json"));

        return configRoot;
    }

    /// <summary>
    /// 写入包含一条飞拍轨迹的最小 RobotConfig.json，供兼容服务从统一配置恢复轨迹。
    /// </summary>
    /// <param name="configRoot">测试运行配置根。</param>
    private static void WriteRobotConfigWithDemoTrajectory(
        string configRoot,
        double planningSpeedScale = 0.45,
        double speedRatio = 1.0)
    {
        File.WriteAllText(
            Path.Combine(configRoot, "RobotConfig.json"),
            $$"""
            {
              "robot": {
                "use_do": true,
                "io_addr": [7, 8],
                "io_keep_cycles": 2,
                "speed_ratio": {{speedRatio.ToString(CultureInfo.InvariantCulture)}},
                "planning_speed_scale": {{planningSpeedScale.ToString(CultureInfo.InvariantCulture)}},
                "acc_limit": 1.0,
                "jerk_limit": 1.0,
                "adapt_icsp_try_num": 5
              },
              "flying_shots": {
                "demo-flyshot": {
                  "traj_waypoints": [
                    [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                    [0.001, 0.0, 0.0, 0.0, 0.0, 0.0],
                    [0.002, 0.0, 0.0, 0.0, 0.0, 0.0],
                    [0.003, 0.0, 0.0, 0.0, 0.0, 0.0]
                  ],
                  "shot_flags": [false, true, false, false],
                  "offset_values": [0, 1, 0, 0],
                  "addr": [[], [7, 8], [], []]
                }
              }
            }
            """);
    }

    /// <summary>
    /// 使用指定运行配置根执行 demo-flyshot 的 SaveTrajectoryInfo。
    /// </summary>
    private static void SaveDemoTrajectoryInfo(string configRoot)
    {
        var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
        var service = new ControllerClientCompatService(
            options,
            new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
            new RecordingControllerRuntime(),
            new ControllerClientTrajectoryOrchestrator(),
            new RobotConfigLoader());

        service.SetUpRobot("FANUC_LR_Mate_200iD");
        service.SaveTrajectoryInfo("demo-flyshot");
    }

    /// <summary>
    /// 按数值逐行比较两个轨迹文本文件，避免纯文本格式差异影响判断。
    /// </summary>
    private static void AssertNumericFilesEqual(string expectedPath, string actualPath)
    {
        var expectedRows = File.ReadAllLines(expectedPath).Select(ParseSpaceSeparatedDoubles).ToArray();
        var actualRows = File.ReadAllLines(actualPath).Select(ParseSpaceSeparatedDoubles).ToArray();

        Assert.Equal(expectedRows.Length, actualRows.Length);
        for (var rowIndex = 0; rowIndex < expectedRows.Length; rowIndex++)
        {
            Assert.Equal(expectedRows[rowIndex].Length, actualRows[rowIndex].Length);
            for (var columnIndex = 0; columnIndex < expectedRows[rowIndex].Length; columnIndex++)
            {
                Assert.Equal(expectedRows[rowIndex][columnIndex], actualRows[rowIndex][columnIndex], precision: 6);
            }
        }
    }

    /// <summary>
    /// 角度转弧度，供轨迹整形测试构造输入。
    /// </summary>
    private static double DegreesToRadians(double degrees)
    {
        return degrees * Math.PI / 180.0;
    }

    /// <summary>
    /// 弧度转角度，供轨迹整形测试验证单步变化量。
    /// </summary>
    private static double RadiansToDegrees(double radians)
    {
        return radians * 180.0 / Math.PI;
    }

    /// <summary>
    /// 验证稠密轨迹端点仍与原始示教点一致。
    /// </summary>
    private static void AssertJointRadiansEqual(IReadOnlyList<double> expectedRadians, IReadOnlyList<double> actualRadians)
    {
        Assert.Equal(expectedRadians.Count, actualRadians.Count);
        for (var index = 0; index < expectedRadians.Count; index++)
        {
            Assert.Equal(expectedRadians[index], actualRadians[index], precision: 6);
        }
    }

    /// <summary>
    /// 加载运行时 Config/RobotConfig.json 中的 UTTC_MS11 轨迹和对应机器人配置。
    /// </summary>
    private static UttcMs11RuntimeFixture LoadUttcMs11RuntimeFixture()
    {
        var configRoot = TestRobotFactory.GetConfigRoot();
        var configPath = Path.Combine(configRoot, "RobotConfig.json");
        var loaded = new RobotConfigLoader().Load(configPath, configRoot);
        var program = loaded.Programs["UTTC_MS11"];
        var uploaded = new ControllerClientCompatUploadedTrajectory(
            name: program.Name,
            waypoints: program.Waypoints.Select(static waypoint => waypoint.Positions),
            shotFlags: program.ShotFlags,
            offsetValues: program.OffsetValues,
            addressGroups: program.AddressGroups.Select(static group => group.Addresses));
        var options = new ControllerClientCompatOptions
        {
            ConfigRoot = configRoot
        };
        var robot = new ControllerClientCompatRobotCatalog(options, new RobotModelLoader())
            .LoadProfile("FANUC_LR_Mate_200iD", loaded.Robot.AccLimitScale, loaded.Robot.JerkLimitScale);

        return new UttcMs11RuntimeFixture(configRoot, loaded.Robot, uploaded, robot);
    }

    /// <summary>
    /// 为首尾平滑对比测试显式打开二次时间重映射，避免受现场 legacy-fit 配置影响。
    /// </summary>
    private static CompatibilityRobotSettings EnableSmoothStartStopTiming(CompatibilityRobotSettings settings)
    {
        return new CompatibilityRobotSettings(
            useDo: settings.UseDo,
            ioAddresses: settings.IoAddresses,
            ioKeepCycles: settings.IoKeepCycles,
            triggerSampleIndexOffsetCycles: settings.TriggerSampleIndexOffsetCycles,
            accLimitScale: settings.AccLimitScale,
            jerkLimitScale: settings.JerkLimitScale,
            adaptIcspTryNum: settings.AdaptIcspTryNum,
            planningSpeedScale: settings.PlanningSpeedScale,
            speedRatio: settings.SpeedRatio,
            smoothStartStopTiming: true);
    }

    /// <summary>
    /// 为 legacy-fit 对比测试显式关闭二次时间重映射。
    /// </summary>
    private static CompatibilityRobotSettings DisableSmoothStartStopTiming(CompatibilityRobotSettings settings)
    {
        return new CompatibilityRobotSettings(
            useDo: settings.UseDo,
            ioAddresses: settings.IoAddresses,
            ioKeepCycles: settings.IoKeepCycles,
            triggerSampleIndexOffsetCycles: settings.TriggerSampleIndexOffsetCycles,
            accLimitScale: settings.AccLimitScale,
            jerkLimitScale: settings.JerkLimitScale,
            adaptIcspTryNum: settings.AdaptIcspTryNum,
            planningSpeedScale: settings.PlanningSpeedScale,
            speedRatio: settings.SpeedRatio,
            smoothStartStopTiming: false);
    }

    /// <summary>
    /// 构造飞拍倍率测试使用的最小兼容设置。
    /// </summary>
    private static CompatibilityRobotSettings CreateFlyshotSettings(double planningSpeedScale, double speedRatio)
    {
        return new CompatibilityRobotSettings(
            useDo: true,
            ioAddresses: [7, 8],
            ioKeepCycles: 2,
            triggerSampleIndexOffsetCycles: 7,
            accLimitScale: 1.0,
            jerkLimitScale: 1.0,
            adaptIcspTryNum: 5,
            planningSpeedScale: planningSpeedScale,
            speedRatio: speedRatio,
            smoothStartStopTiming: true);
    }

    /// <summary>
    /// 把指定窗口内的整形前后逐点数据导出为 CSV，包含关节、步长、速度、加速度与跃度。
    /// </summary>
    private static void WriteEdgeComparisonCsv(
        string path,
        IReadOnlyList<IReadOnlyList<double>> rawDense,
        IReadOnlyList<IReadOnlyList<double>> shapedDense,
        int startIndex,
        int count)
    {
        var lines = new List<string>
        {
            BuildEdgeComparisonHeader()
        };

        for (var index = startIndex; index < startIndex + count && index < rawDense.Count && index < shapedDense.Count; index++)
        {
            var columns = new List<string>
            {
                index.ToString(CultureInfo.InvariantCulture),
                rawDense[index][0].ToString("F6", CultureInfo.InvariantCulture)
            };

            AppendJointColumns(columns, "raw_j", index, rawDense, static (rows, rowIndex, jointIndex) => RadiansToDegrees(rows[rowIndex][jointIndex + 1]));
            AppendJointColumns(columns, "shaped_j", index, shapedDense, static (rows, rowIndex, jointIndex) => RadiansToDegrees(rows[rowIndex][jointIndex + 1]));
            AppendJointColumns(columns, "raw_step", index, rawDense, ComputeStepDegrees);
            AppendJointColumns(columns, "shaped_step", index, shapedDense, ComputeStepDegrees);
            AppendJointColumns(columns, "raw_velocity", index, rawDense, ComputeVelocityDegrees);
            AppendJointColumns(columns, "shaped_velocity", index, shapedDense, ComputeVelocityDegrees);
            AppendJointColumns(columns, "raw_acceleration", index, rawDense, ComputeAccelerationDegrees);
            AppendJointColumns(columns, "shaped_acceleration", index, shapedDense, ComputeAccelerationDegrees);
            AppendJointColumns(columns, "raw_jerk", index, rawDense, ComputeJerkDegrees);
            AppendJointColumns(columns, "shaped_jerk", index, shapedDense, ComputeJerkDegrees);
            lines.Add(string.Join(",", columns));
        }

        File.WriteAllLines(path, lines);
    }

    /// <summary>
    /// 构造首尾整形逐点对比 CSV 表头。
    /// </summary>
    private static string BuildEdgeComparisonHeader()
    {
        var columns = new List<string>
        {
            "sample_index",
            "time_seconds"
        };

        AppendJointHeader(columns, "raw_j");
        AppendJointHeader(columns, "shaped_j");
        AppendJointHeader(columns, "raw_step");
        AppendJointHeader(columns, "shaped_step");
        AppendJointHeader(columns, "raw_velocity");
        AppendJointHeader(columns, "shaped_velocity");
        AppendJointHeader(columns, "raw_acceleration");
        AppendJointHeader(columns, "shaped_acceleration");
        AppendJointHeader(columns, "raw_jerk");
        AppendJointHeader(columns, "shaped_jerk");

        return string.Join(",", columns);
    }

    /// <summary>
    /// 追加 J1..J6 表头列名。
    /// </summary>
    private static void AppendJointHeader(List<string> columns, string prefix)
    {
        for (var jointIndex = 1; jointIndex <= 6; jointIndex++)
        {
            columns.Add($"{prefix}_j{jointIndex}");
        }
    }

    /// <summary>
    /// 追加某一行对应的六轴数值列。
    /// </summary>
    private static void AppendJointColumns(
        List<string> columns,
        string prefix,
        int rowIndex,
        IReadOnlyList<IReadOnlyList<double>> rows,
        Func<IReadOnlyList<IReadOnlyList<double>>, int, int, double> selector)
    {
        for (var jointIndex = 0; jointIndex < 6; jointIndex++)
        {
            columns.Add(selector(rows, rowIndex, jointIndex).ToString("F6", CultureInfo.InvariantCulture));
        }
    }

    /// <summary>
    /// 计算某一行相对上一行的单步角度变化量。
    /// </summary>
    private static double ComputeStepDegrees(IReadOnlyList<IReadOnlyList<double>> rows, int rowIndex, int jointIndex)
    {
        if (rowIndex <= 0)
        {
            return 0.0;
        }

        return RadiansToDegrees(rows[rowIndex][jointIndex + 1] - rows[rowIndex - 1][jointIndex + 1]);
    }

    /// <summary>
    /// 计算某一行的角速度，单位 deg/s。
    /// </summary>
    private static double ComputeVelocityDegrees(IReadOnlyList<IReadOnlyList<double>> rows, int rowIndex, int jointIndex)
    {
        if (rowIndex <= 0)
        {
            return 0.0;
        }

        var dt = rows[rowIndex][0] - rows[rowIndex - 1][0];
        if (dt <= 0.0)
        {
            return 0.0;
        }

        return ComputeStepDegrees(rows, rowIndex, jointIndex) / dt;
    }

    /// <summary>
    /// 计算某一行的角加速度，单位 deg/s^2。
    /// </summary>
    private static double ComputeAccelerationDegrees(IReadOnlyList<IReadOnlyList<double>> rows, int rowIndex, int jointIndex)
    {
        if (rowIndex <= 1)
        {
            return 0.0;
        }

        var dt = rows[rowIndex][0] - rows[rowIndex - 1][0];
        if (dt <= 0.0)
        {
            return 0.0;
        }

        return (ComputeVelocityDegrees(rows, rowIndex, jointIndex) - ComputeVelocityDegrees(rows, rowIndex - 1, jointIndex)) / dt;
    }

    /// <summary>
    /// 计算某一行的角跃度，单位 deg/s^3。
    /// </summary>
    private static double ComputeJerkDegrees(IReadOnlyList<IReadOnlyList<double>> rows, int rowIndex, int jointIndex)
    {
        if (rowIndex <= 2)
        {
            return 0.0;
        }

        var dt = rows[rowIndex][0] - rows[rowIndex - 1][0];
        if (dt <= 0.0)
        {
            return 0.0;
        }

        return (ComputeAccelerationDegrees(rows, rowIndex, jointIndex) - ComputeAccelerationDegrees(rows, rowIndex - 1, jointIndex)) / dt;
    }

    /// <summary>
    /// 统计给定窗口内所有关节的最大绝对值跃度，便于快速看首尾尖峰是否被压低。
    /// </summary>
    private static double ComputeWindowMaxAbsJerkDegrees(
        IReadOnlyList<IReadOnlyList<double>> rows,
        int startIndex,
        int count)
    {
        var maxAbsJerk = 0.0;
        for (var rowIndex = startIndex; rowIndex < startIndex + count && rowIndex < rows.Count; rowIndex++)
        {
            for (var jointIndex = 0; jointIndex < 6; jointIndex++)
            {
                maxAbsJerk = Math.Max(maxAbsJerk, Math.Abs(ComputeJerkDegrees(rows, rowIndex, jointIndex)));
            }
        }

        return maxAbsJerk;
    }

    /// <summary>
    /// 解析空格分隔的关节轨迹行。
    /// </summary>
    private static double[] ParseSpaceSeparatedDoubles(string line)
    {
        return line
            .Split(' ', StringSplitOptions.RemoveEmptyEntries)
            .Select(static value => double.Parse(value, CultureInfo.InvariantCulture))
            .ToArray();
    }

    /// <summary>
    /// 将 8ms 执行稠密轨迹按目标周期抽稀为低频视图，并始终保留终点。
    /// </summary>
    private static IReadOnlyList<IReadOnlyList<double>> DownsampleDenseRows(
        IReadOnlyList<IReadOnlyList<double>> denseRows,
        double samplePeriodSeconds)
    {
        var result = new List<IReadOnlyList<double>>();
        var epsilon = 1e-6;
        var nextSampleTime = 0.0;

        foreach (var row in denseRows)
        {
            var sampleTime = row[0];
            if (sampleTime + epsilon < nextSampleTime)
            {
                continue;
            }

            if (Math.Abs(sampleTime - nextSampleTime) <= epsilon || sampleTime.Equals(0.0))
            {
                result.Add(row);
                nextSampleTime += samplePeriodSeconds;
            }
        }

        if (result.Count == 0 || !ReferenceEquals(result[^1], denseRows[^1]))
        {
            result.Add(denseRows[^1]);
        }

        return result;
    }

    /// <summary>
    /// 用真实可运行轨迹前 4 个采样点拟合局部三次曲线，返回相对首点时间的系数。
    /// 曲线形式为 p(t)=c3*t^3+c2*t^2+c1*t+c0，单位保持输入文件的角度制。
    /// </summary>
    private static LocalCubicFit FitCubicFromFirstFourRows(IReadOnlyList<double[]> rows, int valueColumn)
    {
        var t0 = rows[0][0];
        var matrix = new double[4][];
        var rhs = new double[4];
        for (var index = 0; index < 4; index++)
        {
            var localTime = rows[index][0] - t0;
            matrix[index] =
            [
                localTime * localTime * localTime,
                localTime * localTime,
                localTime,
                1.0
            ];
            rhs[index] = rows[index][valueColumn];
        }

        var solution = SolveLinearSystem4x4(matrix, rhs);
        return new LocalCubicFit(
            Coefficient3: solution[0],
            Coefficient2: solution[1],
            Coefficient1: solution[2],
            Coefficient0: solution[3],
            LastLocalTime: rows[3][0] - t0);
    }

    /// <summary>
    /// 用高斯消元求解 4x4 线性方程组。
    /// </summary>
    private static double[] SolveLinearSystem4x4(double[][] matrix, double[] rhs)
    {
        var a = matrix.Select(static row => row.ToArray()).ToArray();
        var b = rhs.ToArray();

        for (var pivot = 0; pivot < 4; pivot++)
        {
            var bestRow = pivot;
            for (var row = pivot + 1; row < 4; row++)
            {
                if (Math.Abs(a[row][pivot]) > Math.Abs(a[bestRow][pivot]))
                {
                    bestRow = row;
                }
            }

            if (Math.Abs(a[bestRow][pivot]) <= 1e-12)
            {
                throw new InvalidOperationException("真实轨迹前 4 点无法稳定拟合局部三次曲线。");
            }

            if (bestRow != pivot)
            {
                (a[pivot], a[bestRow]) = (a[bestRow], a[pivot]);
                (b[pivot], b[bestRow]) = (b[bestRow], b[pivot]);
            }

            var divisor = a[pivot][pivot];
            for (var column = pivot; column < 4; column++)
            {
                a[pivot][column] /= divisor;
            }

            b[pivot] /= divisor;

            for (var row = 0; row < 4; row++)
            {
                if (row == pivot)
                {
                    continue;
                }

                var factor = a[row][pivot];
                for (var column = pivot; column < 4; column++)
                {
                    a[row][column] -= factor * a[pivot][column];
                }

                b[row] -= factor * b[pivot];
            }
        }

        return b;
    }
}

/// <summary>
/// 封装运行时 UTTC_MS11 轨迹、配置和机器人模型，避免测试反复拼装。
/// </summary>
internal sealed record UttcMs11RuntimeFixture(
    string ConfigRoot,
    CompatibilityRobotSettings Settings,
    ControllerClientCompatUploadedTrajectory Uploaded,
    RobotProfile Robot);

/// <summary>
/// 记录基于真实轨迹前 4 点拟合得到的局部三次曲线系数。
/// </summary>
internal sealed record LocalCubicFit(
    double Coefficient3,
    double Coefficient2,
    double Coefficient1,
    double Coefficient0,
    double LastLocalTime);

/// <summary>
/// 为运行时编排测试构造稳定的最小领域对象。
/// </summary>
internal static class TestRobotFactory
{
    /// <summary>
    /// 构造六轴测试机器人配置，避免运行时测试依赖真实 .robot 文件。
    /// </summary>
    /// <returns>可用于规划和运行时状态校验的机器人配置。</returns>
    public static RobotProfile CreateRobotProfile()
    {
        return new RobotProfile(
            name: "TestRobot",
            modelPath: "Models/Test.robot",
            degreesOfFreedom: 6,
            jointLimits: Enumerable.Range(1, 6)
                .Select(static index => new JointLimit($"J{index}", 10.0, 20.0, 100.0))
                .ToArray(),
            jointCouplings: Array.Empty<JointCoupling>(),
            servoPeriod: TimeSpan.FromMilliseconds(8),
            triggerPeriod: TimeSpan.FromMilliseconds(8));
    }

    /// <summary>
    /// 构造一条含单个拍照点的上传飞拍轨迹。
    /// </summary>
    /// <returns>可用于触发时间轴测试的上传轨迹。</returns>
    public static ControllerClientCompatUploadedTrajectory CreateUploadedTrajectoryWithSingleShot()
    {
        return new ControllerClientCompatUploadedTrajectory(
            name: "demo-flyshot",
            waypoints:
            [
                [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                [0.001, 0.0, 0.0, 0.0, 0.0, 0.0],
                [0.002, 0.0, 0.0, 0.0, 0.0, 0.0],
                [0.003, 0.0, 0.0, 0.0, 0.0, 0.0]
            ],
            shotFlags: [false, true, false, false],
            offsetValues: [0, 1, 0, 0],
            addressGroups:
            [
                Array.Empty<int>(),
                [7, 8],
                Array.Empty<int>(),
                Array.Empty<int>()
            ]);
    }

    /// <summary>
    /// 构造一份真实依赖注入等价的兼容服务，覆盖运行时和编排器协作。
    /// </summary>
    /// <returns>可执行 ControllerClient 兼容语义的服务实例。</returns>
    public static ControllerClientCompatService CreateCompatService()
    {
        var options = new ControllerClientCompatOptions
        {
            ConfigRoot = GetConfigRoot()
        };

        return new ControllerClientCompatService(
            options,
            new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
            new FanucControllerRuntime(),
            new ControllerClientTrajectoryOrchestrator(),
            new RobotConfigLoader());
    }

    /// <summary>
    /// 定位 replacement 仓库内的运行配置根目录。
    /// </summary>
    /// <returns>当前仓库 Config 目录。</returns>
    public static string GetConfigRoot()
    {
        return Path.Combine(GetReplacementRoot(), "Config");
    }

    /// <summary>
    /// 定位 replacement 仓库根目录，供测试读取仓库内固化配置。
    /// </summary>
    /// <returns>replacement 仓库根目录。</returns>
    public static string GetReplacementRoot()
    {
        var current = new DirectoryInfo(AppContext.BaseDirectory);
        while (current is not null)
        {
            if (File.Exists(Path.Combine(current.FullName, "FlyshotReplacement.sln")))
            {
                return current.FullName;
            }

            current = current.Parent;
        }

        throw new DirectoryNotFoundException("Unable to locate the flyshot replacement root.");
    }

    /// <summary>
    /// 定位父工作区根目录，供兼容服务加载真实机器人模型。
    /// </summary>
    /// <returns>父工作区根目录。</returns>
    public static string GetWorkspaceRoot()
    {
        var current = new DirectoryInfo(AppContext.BaseDirectory);
        while (current is not null)
        {
            if (File.Exists(Path.Combine(current.FullName, "FlyshotReplacement.sln")))
            {
                return Path.GetFullPath(Path.Combine(current.FullName, ".."));
            }

            current = current.Parent;
        }

        throw new DirectoryNotFoundException("Unable to locate the flyshot workspace root.");
    }
}

/// <summary>
/// 记录 ResetRobot 入参的测试运行时，用于验证兼容服务传递的机器人配置。
/// </summary>
internal sealed class RecordingControllerRuntime : IControllerRuntime
{
    private double _speedRatio = 1.0;

    /// <summary>
    /// 获取最近一次 ResetRobot 收到的机器人配置。
    /// </summary>
    public RobotProfile? LastRobotProfile { get; private set; }

    /// <summary>
    /// 获取最近一次 ExecuteTrajectory 收到的结果对象。
    /// </summary>
    public TrajectoryResult? LastExecutedResult { get; private set; }

    /// <inheritdoc />
    public void ResetRobot(RobotProfile robot, string robotName)
    {
        LastRobotProfile = robot;
    }

    /// <inheritdoc />
    public void SetActiveController(bool sim)
    {
    }

    /// <inheritdoc />
    public void Connect(string robotIp)
    {
    }

    /// <inheritdoc />
    public void Disconnect()
    {
    }

    /// <inheritdoc />
    public void EnableRobot(int bufferSize)
    {
    }

    /// <inheritdoc />
    public void DisableRobot()
    {
    }

    /// <inheritdoc />
    public void StopMove()
    {
    }

    /// <inheritdoc />
    public double GetSpeedRatio() => _speedRatio;

    /// <inheritdoc />
    public void SetSpeedRatio(double ratio)
    {
        _speedRatio = ratio;
    }

    /// <inheritdoc />
    public IReadOnlyList<double> GetTcp() => [0.0, 0.0, 0.0];

    /// <inheritdoc />
    public void SetTcp(double x, double y, double z)
    {
    }

    /// <inheritdoc />
    public bool GetIo(int port, string ioType) => false;

    /// <inheritdoc />
    public void SetIo(int port, bool value, string ioType)
    {
    }

    /// <inheritdoc />
    public IReadOnlyList<double> GetJointPositions() => Array.Empty<double>();

    /// <inheritdoc />
    public IReadOnlyList<double> GetPose() => Array.Empty<double>();

    /// <inheritdoc />
    public ControllerStateSnapshot GetSnapshot()
    {
        return new ControllerStateSnapshot(
            capturedAt: DateTimeOffset.UtcNow,
            connectionState: "Connected",
            isEnabled: true,
            isInMotion: false,
            speedRatio: _speedRatio,
            jointPositions: Array.Empty<double>(),
            cartesianPose: Array.Empty<double>(),
            activeAlarms: Array.Empty<RuntimeAlarm>());
    }

    /// <inheritdoc />
    public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions)
    {
        LastExecutedResult = result;
    }

    /// <inheritdoc />
    public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
    {
        LastExecutedResult = result;
    }
}

/// <summary>
/// 模拟第一段运动异步完成的测试运行时，用于验证兼容层是否等待 move_to_start 完成。
/// </summary>
internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
{
    private readonly object _lock = new();
    private readonly TimeSpan _firstMotionCompletionDelay;
    private double[] _jointPositions;
    private bool _isEnabled;
    private bool _isInMotion;
    private bool _firstMotionCompleted;

    /// <summary>
    /// 初始化可延迟完成第一段运动的测试运行时。
    /// </summary>
    /// <param name="initialJointPositions">运行时报告的初始关节位置。</param>
    /// <param name="firstMotionCompletionDelay">第一段运动完成前保持忙碌的时间。</param>
    public DelayedCompletionControllerRuntime(
        IReadOnlyList<double> initialJointPositions,
        TimeSpan firstMotionCompletionDelay)
    {
        _jointPositions = initialJointPositions.ToArray();
        _firstMotionCompletionDelay = firstMotionCompletionDelay;
    }

    /// <summary>
    /// 获取所有 ExecuteTrajectory 调用记录。
    /// </summary>
    public List<(TrajectoryResult Result, IReadOnlyList<double> FinalJointPositions)> ExecuteCalls { get; } = [];

    /// <summary>
    /// 获取第二条轨迹是否在第一段 move_to_start 完成前启动。
    /// </summary>
    public bool SecondTrajectoryStartedBeforeFirstMotionCompleted { get; private set; }

    /// <inheritdoc />
    public void ResetRobot(RobotProfile robot, string robotName)
    {
    }

    /// <inheritdoc />
    public void SetActiveController(bool sim)
    {
    }

    /// <inheritdoc />
    public void Connect(string robotIp)
    {
    }

    /// <inheritdoc />
    public void Disconnect()
    {
    }

    /// <inheritdoc />
    public void EnableRobot(int bufferSize)
    {
        _isEnabled = true;
    }

    /// <inheritdoc />
    public void DisableRobot()
    {
        _isEnabled = false;
    }

    /// <inheritdoc />
    public void StopMove()
    {
        lock (_lock)
        {
            _isInMotion = false;
        }
    }

    /// <inheritdoc />
    public double GetSpeedRatio() => 1.0;

    /// <inheritdoc />
    public void SetSpeedRatio(double ratio)
    {
    }

    /// <inheritdoc />
    public IReadOnlyList<double> GetTcp() => [0.0, 0.0, 0.0];

    /// <inheritdoc />
    public void SetTcp(double x, double y, double z)
    {
    }

    /// <inheritdoc />
    public bool GetIo(int port, string ioType) => false;

    /// <inheritdoc />
    public void SetIo(int port, bool value, string ioType)
    {
    }

    /// <inheritdoc />
    public IReadOnlyList<double> GetJointPositions()
    {
        lock (_lock)
        {
            return _jointPositions.ToArray();
        }
    }

    /// <inheritdoc />
    public IReadOnlyList<double> GetPose() => Array.Empty<double>();

    /// <inheritdoc />
    public ControllerStateSnapshot GetSnapshot()
    {
        lock (_lock)
        {
            return new ControllerStateSnapshot(
                capturedAt: DateTimeOffset.UtcNow,
                connectionState: "Connected",
                isEnabled: _isEnabled,
                isInMotion: _isInMotion,
                speedRatio: 1.0,
                jointPositions: _jointPositions.ToArray(),
                cartesianPose: Array.Empty<double>(),
                activeAlarms: Array.Empty<RuntimeAlarm>());
        }
    }

    /// <inheritdoc />
    public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions)
    {
        lock (_lock)
        {
            ExecuteCalls.Add((result, finalJointPositions.ToArray()));
            if (ExecuteCalls.Count == 1)
            {
                _isInMotion = true;
                _ = Task.Run(async () =>
                {
                    await Task.Delay(_firstMotionCompletionDelay).ConfigureAwait(false);
                    lock (_lock)
                    {
                        _jointPositions = finalJointPositions.ToArray();
                        _isInMotion = false;
                        _firstMotionCompleted = true;
                    }
                });
                return;
            }

            if (!_firstMotionCompleted)
            {
                SecondTrajectoryStartedBeforeFirstMotionCompleted = true;
            }

            _jointPositions = finalJointPositions.ToArray();
        }
    }

    /// <inheritdoc />
    public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
    {
    }
}

/// <summary>
/// 模拟 runtime 执行入口长期占用的测试运行时，用于验证兼容层锁边界。
/// </summary>
internal sealed class BlockingExecutionControllerRuntime : IControllerRuntime
{
    private readonly object _lock = new();
    private readonly ManualResetEventSlim _executionStarted = new(false);
    private readonly ManualResetEventSlim _releaseExecution = new(false);
    private readonly double[] _jointPositions;
    private bool _isEnabled;
    private bool _isInMotion;
    private int _executeCallCount;

    /// <summary>
    /// 初始化一份会阻塞 ExecuteTrajectory 的测试运行时。
    /// </summary>
    /// <param name="initialJointPositions">初始关节反馈。</param>
    public BlockingExecutionControllerRuntime(IReadOnlyList<double> initialJointPositions)
    {
        _jointPositions = initialJointPositions.ToArray();
    }

    /// <summary>
    /// 获取 runtime 执行入口被调用的次数。
    /// </summary>
    public int ExecuteCallCount
    {
        get
        {
            lock (_lock)
            {
                return _executeCallCount;
            }
        }
    }

    /// <summary>
    /// 等待指定序号的执行调用进入 runtime。
    /// </summary>
    /// <param name="timeout">最长等待时间。</param>
    /// <param name="expectedCallCount">期望已经进入的执行次数。</param>
    /// <returns>是否在超时前等到。</returns>
    public bool WaitForExecutionStarted(TimeSpan timeout, int expectedCallCount = 1)
    {
        var deadline = DateTimeOffset.UtcNow.Add(timeout);
        while (DateTimeOffset.UtcNow < deadline)
        {
            lock (_lock)
            {
                if (_executeCallCount >= expectedCallCount)
                {
                    return true;
                }
            }

            _executionStarted.Wait(TimeSpan.FromMilliseconds(10));
        }

        return false;
    }

    /// <summary>
    /// 释放当前阻塞的执行调用。
    /// </summary>
    public void ReleaseExecution()
    {
        _releaseExecution.Set();
    }

    public void ResetRobot(RobotProfile robot, string robotName)
    {
    }

    public void SetActiveController(bool sim)
    {
    }

    public void Connect(string robotIp)
    {
    }

    public void Disconnect()
    {
    }

    public void EnableRobot(int bufferSize)
    {
        _isEnabled = true;
    }

    public void DisableRobot()
    {
        _isEnabled = false;
    }

    public void StopMove()
    {
        lock (_lock)
        {
            _isInMotion = false;
        }

        ReleaseExecution();
    }

    public double GetSpeedRatio() => 1.0;

    public void SetSpeedRatio(double ratio)
    {
    }

    public IReadOnlyList<double> GetTcp() => [0.0, 0.0, 0.0];

    public void SetTcp(double x, double y, double z)
    {
    }

    public bool GetIo(int port, string ioType) => false;

    public void SetIo(int port, bool value, string ioType)
    {
    }

    public IReadOnlyList<double> GetJointPositions()
    {
        return _jointPositions.ToArray();
    }

    public IReadOnlyList<double> GetPose() => [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];

    public ControllerStateSnapshot GetSnapshot()
    {
        lock (_lock)
        {
            return new ControllerStateSnapshot(
                capturedAt: DateTimeOffset.UtcNow,
                connectionState: "Connected",
                isEnabled: _isEnabled,
                isInMotion: _isInMotion,
                speedRatio: 1.0,
                jointPositions: _jointPositions.ToArray(),
                cartesianPose: Array.Empty<double>(),
                activeAlarms: Array.Empty<RuntimeAlarm>());
        }
    }

    public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions)
    {
        lock (_lock)
        {
            _executeCallCount++;
            _isInMotion = true;
            _executionStarted.Set();
            _releaseExecution.Reset();
        }

        _releaseExecution.Wait();

        lock (_lock)
        {
            _isInMotion = false;
            _executionStarted.Reset();
        }
    }

    public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
    {
        ExecuteTrajectory(result, _jointPositions);
    }
}

/// <summary>
/// 模拟 move_to_start 逻辑完成后，真实反馈仍停留在旧位置的测试运行时。
/// </summary>
internal sealed class StickyFeedbackControllerRuntime : IControllerRuntime
{
    private readonly object _lock = new();
    private readonly TimeSpan _firstMotionCompletionDelay;
    private double[] _jointPositions;
    private bool _isEnabled;
    private bool _isInMotion;
    private bool _firstMotionCompleted;

    public StickyFeedbackControllerRuntime(
        IReadOnlyList<double> initialJointPositions,
        TimeSpan firstMotionCompletionDelay)
    {
        _jointPositions = initialJointPositions.ToArray();
        _firstMotionCompletionDelay = firstMotionCompletionDelay;
    }

    public List<(TrajectoryResult Result, IReadOnlyList<double> FinalJointPositions)> ExecuteCalls { get; } = [];

    public void ResetRobot(RobotProfile robot, string robotName)
    {
    }

    public void SetActiveController(bool sim)
    {
    }

    public void Connect(string robotIp)
    {
    }

    public void Disconnect()
    {
    }

    public void EnableRobot(int bufferSize)
    {
        _isEnabled = true;
    }

    public void DisableRobot()
    {
        _isEnabled = false;
    }

    public void StopMove()
    {
        lock (_lock)
        {
            _isInMotion = false;
        }
    }

    public double GetSpeedRatio() => 1.0;

    public void SetSpeedRatio(double ratio)
    {
    }

    public IReadOnlyList<double> GetTcp() => [0.0, 0.0, 0.0];

    public void SetTcp(double x, double y, double z)
    {
    }

    public bool GetIo(int port, string ioType) => false;

    public void SetIo(int port, bool value, string ioType)
    {
    }

    public IReadOnlyList<double> GetJointPositions()
    {
        lock (_lock)
        {
            return _jointPositions.ToArray();
        }
    }

    public IReadOnlyList<double> GetPose() => Array.Empty<double>();

    public ControllerStateSnapshot GetSnapshot()
    {
        lock (_lock)
        {
            return new ControllerStateSnapshot(
                capturedAt: DateTimeOffset.UtcNow,
                connectionState: "Connected",
                isEnabled: _isEnabled,
                isInMotion: _isInMotion,
                speedRatio: 1.0,
                jointPositions: _jointPositions.ToArray(),
                cartesianPose: Array.Empty<double>(),
                activeAlarms: Array.Empty<RuntimeAlarm>());
        }
    }

    public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions)
    {
        lock (_lock)
        {
            ExecuteCalls.Add((result, finalJointPositions.ToArray()));
            if (!_firstMotionCompleted)
            {
                _isInMotion = true;
                _ = Task.Run(async () =>
                {
                    await Task.Delay(_firstMotionCompletionDelay).ConfigureAwait(false);
                    lock (_lock)
                    {
                        // 模拟控制链逻辑上结束，但真实反馈仍卡在旧位置。
                        _isInMotion = false;
                        _firstMotionCompleted = true;
                    }
                });
                return;
            }

            _jointPositions = finalJointPositions.ToArray();
        }
    }

    public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
    {
    }
}
