using System.Text.Json;
using Flyshot.Core.Domain;
using Flyshot.Core.Planning.Sampling;

namespace Flyshot.Core.Tests;

/// <summary>
/// Verifies the Task 2 domain contracts before planning and runtime code depend on them.
/// </summary>
public sealed class DomainModelTests
{
    /// <summary>
    /// Ensures robot profiles keep a stable copy of joint limits and reject invalid dimensions.
    /// </summary>
    [Fact]
    public void RobotProfile_CopiesJointLimits_AndRejectsMismatchedDof()
    {
        var jointLimits = new[]
        {
            new JointLimit("J1", 7.85, 32.72, 272.7),
            new JointLimit("J2", 6.63, 27.63, 230.28)
        };

        var profile = new RobotProfile(
            name: "LR_Mate_200iD_7L",
            modelPath: "Models/LR_Mate_200iD_7L.robot",
            degreesOfFreedom: 2,
            jointLimits: jointLimits,
            jointCouplings: Array.Empty<JointCoupling>(),
            servoPeriod: TimeSpan.FromMilliseconds(8),
            triggerPeriod: TimeSpan.FromMilliseconds(8));

        Assert.Equal(2, profile.DegreesOfFreedom);
        Assert.NotSame(jointLimits, profile.JointLimits);

        // The planner must not accept a profile whose DOF and limits disagree.
        Assert.Throws<ArgumentException>(() => new RobotProfile(
            name: "InvalidRobot",
            modelPath: "Models/Invalid.robot",
            degreesOfFreedom: 3,
            jointLimits: jointLimits,
            jointCouplings: Array.Empty<JointCoupling>(),
            servoPeriod: TimeSpan.FromMilliseconds(8),
            triggerPeriod: TimeSpan.FromMilliseconds(8)));
    }

    /// <summary>
    /// Ensures uploaded flyshot programs keep their shot metadata aligned with teach waypoints.
    /// </summary>
    [Fact]
    public void FlyshotProgram_RejectsMisalignedShotMetadata()
    {
        var waypoints = new[]
        {
            new JointWaypoint(new[] { 0.0, 1.0 }),
            new JointWaypoint(new[] { 2.0, 3.0 })
        };

        var validProgram = new FlyshotProgram(
            name: "EOL10_EAU_0",
            waypoints: waypoints,
            shotFlags: new[] { true, false },
            offsetValues: new[] { 0, 1 },
            addressGroups: new[]
            {
                new IoAddressGroup(new[] { 100 }),
                new IoAddressGroup(Array.Empty<int>())
            });

        Assert.Equal(1, validProgram.ShotWaypointCount);

        // The gateway cannot recover from count mismatches after this point, so fail fast here.
        Assert.Throws<ArgumentException>(() => new FlyshotProgram(
            name: "BrokenProgram",
            waypoints: waypoints,
            shotFlags: new[] { true },
            offsetValues: new[] { 0, 1 },
            addressGroups: new[]
            {
                new IoAddressGroup(new[] { 100 }),
                new IoAddressGroup(Array.Empty<int>())
            }));
    }

    /// <summary>
    /// Ensures execution requests start from predictable defaults for compatibility paths.
    /// </summary>
    [Fact]
    public void TrajectoryRequest_UsesExpectedDefaults()
    {
        var request = new TrajectoryRequest(
            robot: CreateRobotProfile(),
            program: CreateProgram(),
            method: PlanningMethod.Icsp);

        Assert.False(request.MoveToStart);
        Assert.False(request.SaveTrajectoryArtifacts);
        Assert.False(request.UseCache);
        Assert.Equal(PlanningMethod.Icsp, request.Method);
    }

    /// <summary>
    /// Ensures runtime snapshots expose safe empty collections before the controller connects.
    /// </summary>
    [Fact]
    public void ControllerStateSnapshot_InitializesEmptyRuntimeCollections()
    {
        var snapshot = new ControllerStateSnapshot(
            capturedAt: DateTimeOffset.Parse("2026-04-23T10:00:00+08:00"),
            connectionState: "Disconnected",
            isEnabled: false,
            isInMotion: false,
            speedRatio: 100.0);

        Assert.Empty(snapshot.JointPositions);
        Assert.Empty(snapshot.CartesianPose);
        Assert.Empty(snapshot.ActiveAlarms);
        Assert.Empty(snapshot.StateTailWords);
    }

    /// <summary>
    /// 验证控制器快照会保留 TCP 10010 尾部状态字作为诊断字段。
    /// </summary>
    [Fact]
    public void ControllerStateSnapshot_CopiesStateTailWordsForDiagnostics()
    {
        var snapshot = new ControllerStateSnapshot(
            capturedAt: DateTimeOffset.Parse("2026-04-23T10:00:00+08:00"),
            connectionState: "Connected",
            isEnabled: true,
            isInMotion: false,
            speedRatio: 1.0,
            stateTailWords: [2u, 0u, 0u, 1u]);

        var json = JsonSerializer.Serialize(snapshot);

        Assert.Equal([2u, 0u, 0u, 1u], snapshot.StateTailWords);
        Assert.Contains("\"stateTailWords\":[2,0,0,1]", json);
    }

    /// <summary>
    /// Ensures JSON payloads keep stable enum and property names for downstream SDKs.
    /// </summary>
    [Fact]
    public void DomainObjects_SerializeStableContract()
    {
        var result = new TrajectoryResult(
            programName: "EOL10_EAU_0",
            method: PlanningMethod.SelfAdaptIcsp,
            isValid: true,
            duration: TimeSpan.FromSeconds(1.25),
            shotEvents: new[]
            {
                new ShotEvent(
                    waypointIndex: 0,
                    triggerTime: 0.5,
                    sampleIndex: 62,
                    sampleTime: 0.496,
                    addressGroup: new IoAddressGroup(new[] { 100 }))
            },
            triggerTimeline: new[]
            {
                new TrajectoryDoEvent(
                    waypointIndex: 0,
                    triggerTime: 0.5,
                    offsetCycles: 0,
                    holdCycles: 1,
                    addressGroup: new IoAddressGroup(new[] { 100 }),
                    referenceJointsDegrees: new[] { 12.5, -3.0 })
            },
            artifacts: new[]
            {
                new TrajectoryArtifact(
                    kind: TrajectoryArtifactKind.JointDenseTrajectory,
                    logicalName: "JointDetialTraj.txt",
                    relativePath: "artifacts/JointDetialTraj.txt")
            },
            failureReason: null,
            usedCache: true,
            originalWaypointCount: 4,
            plannedWaypointCount: 5,
            triggerSampleIndexOffsetCycles: 7);

        var json = JsonSerializer.Serialize(result);

        Assert.Contains("\"programName\":\"EOL10_EAU_0\"", json);
        Assert.Contains("\"method\":\"SelfAdaptIcsp\"", json);
        Assert.Contains("\"kind\":\"JointDenseTrajectory\"", json);
        Assert.Contains("\"usedCache\":true", json);
        Assert.Contains("\"triggerSampleIndexOffsetCycles\":7", json);
        Assert.Contains("\"referenceJointsDegrees\":[12.5,-3]", json);
    }

    /// <summary>
    /// 验证触发绑定允许在最佳 sample 基础上继续向后偏移固定命令周期。
    /// </summary>
    [Fact]
    public void TriggerSampleBinder_Bind_AppliesConfiguredSampleIndexOffset()
    {
        var trigger = new TrajectoryDoEvent(
            waypointIndex: 1,
            triggerTime: 0.008,
            offsetCycles: 0,
            holdCycles: 2,
            addressGroup: new IoAddressGroup([2, 4]),
            referenceJointsDegrees: [10.0, 20.0]);
        var samples = new[]
        {
            new J519SendSample(sampleIndex: 0, sendTime: 0.0, trajectoryTime: 0.0, speedRatio: 1.0, jointsDegrees: [0.0, 0.0]),
            new J519SendSample(sampleIndex: 1, sendTime: 0.008, trajectoryTime: 0.008, speedRatio: 1.0, jointsDegrees: [10.0, 20.0]),
            new J519SendSample(sampleIndex: 2, sendTime: 0.016, trajectoryTime: 0.016, speedRatio: 1.0, jointsDegrees: [11.0, 21.0]),
            new J519SendSample(sampleIndex: 3, sendTime: 0.024, trajectoryTime: 0.024, speedRatio: 1.0, jointsDegrees: [12.0, 22.0])
        };

        var binding = Assert.Single(Flyshot.Core.Planning.Sampling.TriggerSampleBinder.Bind([trigger], samples, sampleIndexOffsetCycles: 2));

        Assert.True(binding.FoundInWindow);
        Assert.Equal(3, binding.SampleIndex);
        Assert.Equal(0.024, binding.Sample.SendTime, precision: 6);
    }

    /// <summary>
    /// Creates a representative robot profile for request-level domain tests.
    /// </summary>
    private static RobotProfile CreateRobotProfile()
    {
        return new RobotProfile(
            name: "LR_Mate_200iD_7L",
            modelPath: "Models/LR_Mate_200iD_7L.robot",
            degreesOfFreedom: 2,
            jointLimits: new[]
            {
                new JointLimit("J1", 7.85, 32.72, 272.7),
                new JointLimit("J2", 6.63, 27.63, 230.28)
            },
            jointCouplings: Array.Empty<JointCoupling>(),
            servoPeriod: TimeSpan.FromMilliseconds(8),
            triggerPeriod: TimeSpan.FromMilliseconds(8));
    }

    /// <summary>
    /// Creates a representative uploaded program for request-level domain tests.
    /// </summary>
    private static FlyshotProgram CreateProgram()
    {
        return new FlyshotProgram(
            name: "EOL10_EAU_0",
            waypoints: new[]
            {
                new JointWaypoint(new[] { 0.0, 1.0 }),
                new JointWaypoint(new[] { 2.0, 3.0 })
            },
            shotFlags: new[] { true, false },
            offsetValues: new[] { 0, 1 },
            addressGroups: new[]
            {
                new IoAddressGroup(new[] { 100 }),
                new IoAddressGroup(Array.Empty<int>())
            });
    }
}
