param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'))

$ErrorActionPreference = 'Stop'
$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath))

function Assert-True($Actual, [string]$Message) {
    if (-not $Actual) { throw $Message }
}

function Assert-Equal($Expected, $Actual, [string]$Message) {
    if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" }
}

function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) {
    if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) {
        throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance"
    }
}

function Assert-False($Actual, [string]$Message) {
    if ($Actual) { throw $Message }
}

function Assert-Throws([scriptblock]$Action, [string]$Message) {
    try {
        & $Action
    }
    catch {
        return
    }
    throw $Message
}

function Get-RequiredType([string]$Name) {
    return $assembly.GetType($Name, $true)
}

function New-GeometryPoint(
    [double]$X,
    [double]$Y,
    [double]$ArcLength,
    [double]$Heading,
    [double]$UnwrappedHeading,
    [bool]$IsGearSwitch = $false) {
    return [Activator]::CreateInstance($pointType, @(
        $X, $Y, $ArcLength, $Heading, $UnwrappedHeading,
        [double]1.0, $IsGearSwitch, $anchor))
}

function New-DirectionSegment(
    [int]$Index,
    $Direction,
    [object[]]$Points,
    [bool]$StartsAtGearSwitch = $false,
    [bool]$EndsAtGearSwitch = $false) {
    $typedPoints = [Array]::CreateInstance($pointType, $Points.Count)
    for ($pointIndex = 0; $pointIndex -lt $Points.Count; $pointIndex++) {
        $typedPoints.SetValue($Points[$pointIndex], $pointIndex)
    }

    return [Activator]::CreateInstance($segmentType, @(
        $Index, $Direction, $typedPoints, $StartsAtGearSwitch, $EndsAtGearSwitch))
}

function New-DirectionSegmentWithStartCurvature(
    [int]$Index,
    $Direction,
    [double]$StartVehicleCurvature,
    [object[]]$Points,
    [bool]$StartsAtGearSwitch = $false,
    [bool]$EndsAtGearSwitch = $false) {
    $typedPoints = [Array]::CreateInstance($pointType, $Points.Count)
    for ($pointIndex = 0; $pointIndex -lt $Points.Count; $pointIndex++) {
        $typedPoints.SetValue($Points[$pointIndex], $pointIndex)
    }

    return [Activator]::CreateInstance($segmentType, @(
        $Index, $Direction, $typedPoints, $StartsAtGearSwitch, $EndsAtGearSwitch, $StartVehicleCurvature))
}

function New-CoarsePoint(
    [double]$X,
    [double]$Y,
    [double]$ArcLength,
    $Direction,
    [bool]$IsGearSwitch = $false) {
    return [Activator]::CreateInstance($coarsePointType, @(
        $X, $Y, [double]0.0, [double]0.0, $ArcLength, $Direction,
        [double]0.0, [double]1.0, $IsGearSwitch, $coarseAnchor))
}

function Invoke-Analysis([object[]]$Segments, [double]$Spacing = 0.05) {
    $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count)
    for ($index = 0; $index -lt $Segments.Count; $index++) {
        $typedSegments.SetValue($Segments[$index], $index)
    }

    $arguments = [object[]]@($typedSegments, $Spacing, $null, $null)
    $accepted = $analyzeMethod.Invoke($analyzer, $arguments)
    $description = [string]::Join(',', @($Segments | ForEach-Object {
        "index=$($_.SegmentIndex);direction=$($_.Direction);points=$($_.Points.Count)"
    }))
    Assert-True $accepted ("Geometry analysis must accept the analytic candidate. Reason=" + $arguments[3] + '; Segments=' + $description)
    Assert-True ($null -ne $arguments[2]) 'Successful geometry analysis must return PathGeometryAnalysis.'
    return $arguments[2]
}

function Assert-AnalysisRejected([object[]]$Segments, [string]$Message) {
    $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count)
    for ($segmentIndex = 0; $segmentIndex -lt $Segments.Count; $segmentIndex++) {
        $typedSegments.SetValue($Segments[$segmentIndex], $segmentIndex)
    }

    $arguments = [object[]]@($typedSegments, [double]0.05, $null, $null)
    $accepted = $analyzeMethod.Invoke($analyzer, $arguments)
    Assert-True (-not $accepted) ($Message + '; Reason=' + $arguments[3])
}

function Invoke-RejectedAnalysis([object[]]$Segments, [string]$Message) {
    $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count)
    for ($index = 0; $index -lt $Segments.Count; $index++) {
        $typedSegments.SetValue($Segments[$index], $index)
    }

    $arguments = [object[]]@($typedSegments, [double]0.05, $null, $null)
    $accepted = $analyzeMethod.Invoke($analyzer, $arguments)
    Assert-False $accepted ($Message + '; Reason=' + $arguments[3])
}

function New-CoarsePathPoint(
    [double]$X,
    [double]$Y,
    [double]$ArcLength,
    $Direction,
    [bool]$IsGearSwitch = $false,
    [string]$SourceName = 'MotionPrimitive',
    [double]$Heading = 0.0,
    [double]$VehicleCurvature = 0.0) {
    return [Activator]::CreateInstance($coarsePointType, @(
        $X, $Y, $Heading, $Heading, $ArcLength,
        $Direction, $VehicleCurvature, [double]1.0, $IsGearSwitch,
        [Enum]::Parse($coarsePointSourceType, $SourceName)))
}

function New-EmptyGeometryMap {
    $mapRequest = [Activator]::CreateInstance($mapRequestType)
    $mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000))
    $mapRequest.ResolutionMm = [single]50
    $mapRequest.AllowExplicitEmptyMap = $true
    $map = [Activator]::CreateInstance($mapFactoryType).Create($mapRequest).Map
    Assert-True ($null -ne $map) 'Geometry test must create an explicit empty planning map.'
    return $map
}

function Invoke-GeometryValidation($Analysis, [object[]]$Segments, $Vehicle) {
    $typedSegments = [Array]::CreateInstance($segmentType, $Segments.Count)
    for ($index = 0; $index -lt $Segments.Count; $index++) {
        $typedSegments.SetValue($Segments[$index], $index)
    }

    $preparedPath = [Activator]::CreateInstance($preparedPathType, [object[]](, $typedSegments))
    $arguments = [object[]]@(
        $Analysis.Path, $Analysis.Segments, $preparedPath, (New-EmptyGeometryMap), $Vehicle, [double]0.05,
        $null, [double]0.0, $null)
    $accepted = $validateMethod.Invoke($validator, $arguments)
    return [pscustomobject]@{ Accepted = $accepted; Reason = $arguments[8] }
}

function New-CircularDirectionSegment(
    $Direction,
    [double]$Curvature,
    [int]$Intervals,
    [double]$ChordLength) {
    $radius = 1.0 / $Curvature
    $headingStep = 2.0 * [Math]::Asin($Curvature * $ChordLength / 2.0)
    $points = New-Object System.Collections.Generic.List[object]
    for ($index = 0; $index -le $Intervals; $index++) {
        $theta = $headingStep * $index
        $heading = if ($Direction.ToString() -eq 'Forward') { $theta } else { $theta + [Math]::PI }
        [void]$points.Add((New-GeometryPoint `
            (1.0 + $radius * [Math]::Sin($theta)) `
            (1.0 + $radius * (1.0 - [Math]::Cos($theta))) `
            ($index * $ChordLength) $heading $heading))
    }
    return New-DirectionSegment 0 $Direction $points.ToArray()
}

function Get-OldPolylineCurvatureMaximum($Analysis) {
    $maximum = 0.0
    $path = $Analysis.Path
    for ($index = 0; $index -lt $path.Count; $index++) {
        if ($path.Count -eq 1) {
            $curvature = 0.0
        }
        elseif ($index -eq 0) {
            $curvature = ($path[1].UnwrappedHeading - $path[0].UnwrappedHeading) /
                ($path[1].ArcLength - $path[0].ArcLength)
        }
        elseif ($index -eq $path.Count - 1) {
            $curvature = ($path[$index].UnwrappedHeading - $path[$index - 1].UnwrappedHeading) /
                ($path[$index].ArcLength - $path[$index - 1].ArcLength)
        }
        else {
            $curvature = ($path[$index + 1].UnwrappedHeading - $path[$index - 1].UnwrappedHeading) /
                ($path[$index + 1].ArcLength - $path[$index - 1].ArcLength)
        }
        $maximum = [Math]::Max($maximum, [Math]::Abs($curvature))
    }
    return $maximum
}

function Get-QuinticAnalyticCurvatureMaximum(
    [double]$C2,
    [double]$C3,
    [double]$C4,
    [double]$C5,
    [int]$ReferenceSamples = 20000) {
    $maximum = 0.0
    for ($index = 0; $index -lt $ReferenceSamples; $index++) {
        $t = $index / [double]($ReferenceSamples - 1)
        $firstDerivative = 2.0 * $C2 * $t + 3.0 * $C3 * $t * $t +
            4.0 * $C4 * $t * $t * $t + 5.0 * $C5 * $t * $t * $t * $t
        $secondDerivative = 2.0 * $C2 + 6.0 * $C3 * $t +
            12.0 * $C4 * $t * $t + 20.0 * $C5 * $t * $t * $t
        $curvature = [Math]::Abs($secondDerivative / [Math]::Pow(1.0 + $firstDerivative * $firstDerivative, 1.5))
        $maximum = [Math]::Max($maximum, $curvature)
    }
    return $maximum
}

function New-QuinticDirectionSegment(
    [double]$C2,
    [double]$C3,
    [double]$C4,
    [double]$C5,
    [double]$DistributionPower,
    [int]$Samples = 400) {
    $points = New-Object System.Collections.Generic.List[object]
    $arcLength = 0.0
    $previousX = 0.0
    $previousY = 0.0
    for ($index = 0; $index -lt $Samples; $index++) {
        $t = [Math]::Pow($index / [double]($Samples - 1), $DistributionPower)
        $x = 1.0 + $t
        $y = $C2 * $t * $t + $C3 * $t * $t * $t + $C4 * $t * $t * $t * $t + $C5 * $t * $t * $t * $t * $t
        if ($index -gt 0) {
            $arcLength += [Math]::Sqrt(($x - $previousX) * ($x - $previousX) + ($y - $previousY) * ($y - $previousY))
        }
        $heading = [Math]::Atan2(
            2.0 * $C2 * $t + 3.0 * $C3 * $t * $t + 4.0 * $C4 * $t * $t * $t + 5.0 * $C5 * $t * $t * $t * $t,
            1.0)
        [void]$points.Add((New-GeometryPoint $x $y $arcLength $heading $heading))
        $previousX = $x
        $previousY = $y
    }
    return New-DirectionSegment 0 $forward $points.ToArray()
}

$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.'
$processing = $root + 'Processing.'
$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.'

$analyzerType = Get-RequiredType ($processing + 'PathGeometryAnalyzer')
$directionType = Get-RequiredType ($coarsePath + 'TravelDirection')
$sourceType = Get-RequiredType ($root + 'SmoothedPathPointSource')
$pointType = Get-RequiredType ($processing + 'SmoothingPoint2D')
$segmentType = Get-RequiredType ($processing + 'PreparedDirectionSegment')
$preparedPathType = Get-RequiredType ($processing + 'PreparedPath')
$analysisType = Get-RequiredType ($processing + 'PathGeometryAnalysis')
$preprocessorType = Get-RequiredType ($processing + 'PathSmoothingPreprocessor')
$resamplerType = Get-RequiredType ($processing + 'ArcLengthResampler')
$requestType = Get-RequiredType ($root + 'PathSmoothingRequest')
$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration')
$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint')
$coarseSegmentType = Get-RequiredType ($coarsePath + 'PathSegment')
$coarseSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource')
$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters')
$mapType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningGridMap'
$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint')
$coarseSegmentType = Get-RequiredType ($coarsePath + 'PathSegment')
$coarsePointSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource')
$smoothingRequestType = Get-RequiredType ($root + 'PathSmoothingRequest')
$smoothingConfigurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration')
$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters')
$boundsType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.MapBoundsMm'
$mapRequestType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapRequest'
$mapFactoryType = Get-RequiredType 'MultiWheelC.TrajectoryPlanning.Mapping.PlanningMapFactory'
$validatorType = Get-RequiredType ($root + 'Validation.SmoothedPathValidator')
$rawBaselineBuilderType = Get-RequiredType ($processing + 'RawPathBaselineBuilder')

Assert-True ($null -ne $preparedPathType) 'PreparedPath must be discoverable for smoothing algorithms.'
Assert-True ($null -ne $preprocessorType) 'PathSmoothingPreprocessor must be discoverable for request preparation.'
Assert-True ($null -ne $resamplerType) 'ArcLengthResampler must be discoverable for deterministic resampling.'
Assert-True ($null -ne $analysisType.GetProperty('MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter')) `
    'PathGeometryAnalysis must expose peak d-kappa/d-s.'

$analyzer = [Activator]::CreateInstance($analyzerType)
$analyzeMethod = $analyzerType.GetMethod('TryAnalyze')
Assert-True ($null -ne $analyzeMethod) 'PathGeometryAnalyzer must expose TryAnalyze.'
Assert-Equal 4 $analyzeMethod.GetParameters().Length 'TryAnalyze must accept segments, spacing, analysis, and reason.'
$validator = [Activator]::CreateInstance($validatorType)
$validateMethod = $validatorType.GetMethod('TryValidate')
Assert-True ($null -ne $validateMethod) 'SmoothedPathValidator must expose TryValidate.'
Assert-Equal 9 $validateMethod.GetParameters().Length 'SmoothedPathValidator.TryValidate must retain its public contract.'
$rawBaselineMethod = $rawBaselineBuilderType.GetMethod(
    'TryCreate',
    [Reflection.BindingFlags]'Static,NonPublic')
Assert-True ($null -ne $rawBaselineMethod) 'RawPathBaselineBuilder must expose its internal TryCreate path.'

$forward = [Enum]::Parse($directionType, 'Forward')
$reverse = [Enum]::Parse($directionType, 'Reverse')
$anchor = [Enum]::Parse($sourceType, 'Anchor')
$coarseAnchor = [Enum]::Parse($coarseSourceType, 'Start')

# The chord-corrected estimator must retain an exact circular curvature limit for both travel directions.
$maximumAllowedCurvature = 5.0 / 6.0
$circleVehicle = [Activator]::CreateInstance($vehicleType)
$circleVehicle.LengthMeters = 0.20
$circleVehicle.WidthMeters = 0.20
$circleVehicle.SafetyMarginMeters = 0.0
$circleVehicle.MaximumCurvaturePerMeter = $maximumAllowedCurvature
foreach ($direction in @($forward, $reverse)) {
    $exactLimitCircle = New-CircularDirectionSegment $direction $maximumAllowedCurvature 20 0.05
    $exactLimitAnalysis = Invoke-Analysis @($exactLimitCircle)
    foreach ($point in $exactLimitAnalysis.Path) {
        Assert-True ([Math]::Abs($point.VehicleCurvature) -le $maximumAllowedCurvature + 1.0e-9) `
            ('An exact-limit ' + $direction + ' circle must not exceed the curvature limit.')
    }
    $validation = Invoke-GeometryValidation $exactLimitAnalysis @($exactLimitCircle) $circleVehicle
    Assert-True $validation.Accepted ('The validator must accept an analyzed exact-limit ' + $direction + ' circle. Reason=' + $validation.Reason)
}

# An analyzed over-limit circle must remain detectable by the unchanged validator threshold.
$overLimitCircle = New-CircularDirectionSegment $forward ($maximumAllowedCurvature + 0.01) 20 0.05
$overLimitAnalysis = Invoke-Analysis @($overLimitCircle)
Assert-True ($overLimitAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter -gt $maximumAllowedCurvature + 1.0e-6) `
    'An over-limit circle must exceed the vehicle curvature limit by more than the validator tolerance.'
$overLimitValidation = Invoke-GeometryValidation $overLimitAnalysis @($overLimitCircle) $circleVehicle
Assert-False $overLimitValidation.Accepted 'The validator must reject an analyzed over-limit circle.'

# Trusted raw fallback must apply the exact vehicle-curvature gate before reconstruction.
$trustedLimit = 0.80
$trustedOverLimit = $trustedLimit + 0.0000005
$analyzedCurvature = $trustedLimit + 0.01
$chordLength = 0.05
$headingStep = 2.0 * [Math]::Asin($analyzedCurvature * $chordLength / 2.0)
$radius = 1.0 / $analyzedCurvature
$trustedPoints = [Array]::CreateInstance($coarsePointType, 21)
for ($index = 0; $index -le 20; $index++) {
    $theta = $headingStep * $index
    $trustedPoints.SetValue((New-CoarsePathPoint `
        (1.0 + $radius * [Math]::Sin($theta)) `
        (1.0 + $radius * (1.0 - [Math]::Cos($theta))) `
        ($index * $chordLength) `
        $forward `
        $false `
        $(if ($index -eq 0) { 'Start' } else { 'MotionPrimitive' }) `
        $theta `
        $trustedOverLimit), $index)
}
$trustedSegments = [Array]::CreateInstance($coarseSegmentType, 1)
$trustedSegments.SetValue(
    [Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 20, $false, $false)),
    0)
$trustedVehicle = [Activator]::CreateInstance($vehicleType)
$trustedVehicle.LengthMeters = 0.20
$trustedVehicle.WidthMeters = 0.20
$trustedVehicle.SafetyMarginMeters = 0.0
$trustedVehicle.MaximumCurvaturePerMeter = $trustedLimit
$trustedConfiguration = [Activator]::CreateInstance($smoothingConfigurationType)
$trustedRequest = [Activator]::CreateInstance($smoothingRequestType, @(
    $trustedPoints,
    $trustedSegments,
    (New-EmptyGeometryMap),
    $trustedVehicle,
    $trustedConfiguration))
$trustedPreprocessor = [Activator]::CreateInstance($preprocessorType)
$trustedPrepareMethod = $preprocessorType.GetMethod('TryPrepare')
$trustedPrepareArguments = [object[]]@($trustedRequest, $null, $null)
Assert-True $trustedPrepareMethod.Invoke($trustedPreprocessor, $trustedPrepareArguments) `
    ('Trusted raw fallback regression must prepare successfully. Reason=' + $trustedPrepareArguments[2])
$trustedBaselineArguments = [object[]]@(
    $trustedRequest,
    $trustedPrepareArguments[1],
    $analyzer,
    $trustedConfiguration.OutputSpacingMeters,
    $validator,
    $trustedConfiguration.MaximumCollisionCheckStepMeters,
    $null,
    $null)
Assert-False $rawBaselineMethod.Invoke($null, $trustedBaselineArguments) `
    'Trusted raw fallback must not publish a coarse curvature above the exact vehicle maximum.'
Assert-Equal $overLimitValidation.Reason $trustedBaselineArguments[7] `
    'Rejecting trusted raw fallback must preserve the original analyzed-path validation failure.'

# The chord-corrected estimator must not under-estimate these smooth references more than the former polyline estimator.
foreach ($quinticCase in @(
        [pscustomobject]@{ Name = 'SBend'; C2 = 0.0; C3 = 0.30; C4 = -0.45; C5 = 0.18; Power = 1.0 },
        [pscustomobject]@{ Name = 'EndpointPeak'; C2 = 0.18; C3 = -0.12; C4 = 0.0; C5 = 0.0; Power = 1.0 },
        [pscustomobject]@{ Name = 'NonUniformFinalInterval'; C2 = -0.12; C3 = 0.36; C4 = -0.30; C5 = 0.08; Power = 1.7 })) {
    $quintic = New-QuinticDirectionSegment $quinticCase.C2 $quinticCase.C3 $quinticCase.C4 $quinticCase.C5 $quinticCase.Power
    $quinticAnalysis = Invoke-Analysis @($quintic)
    $analyticMaximum = Get-QuinticAnalyticCurvatureMaximum $quinticCase.C2 $quinticCase.C3 $quinticCase.C4 $quinticCase.C5
    $newDeficit = [Math]::Max(0.0, $analyticMaximum - $quinticAnalysis.MaximumAbsoluteVehicleCurvaturePerMeter)
    $oldDeficit = [Math]::Max(0.0, $analyticMaximum - (Get-OldPolylineCurvatureMaximum $quinticAnalysis))
    Assert-True ($newDeficit -le $oldDeficit + 1.0e-6) `
        ($quinticCase.Name + ' must not have greater one-sided curvature under-estimation than the old polyline estimator.')
}

# A half-turn over a single chord has no unambiguous geometric curvature estimate.
$ambiguousTurn = New-DirectionSegment 0 $forward @(
    (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0),
    (New-GeometryPoint 1.0 0.0 1.0 ([Math]::PI) ([Math]::PI)))
Assert-AnalysisRejected @($ambiguousTurn) 'A two-point heading turn of π must be rejected as ambiguous.'

# Forward straight: resampling is exactly 0.05 m, preserves the exact endpoint, and has zero curvature.
$straight = New-DirectionSegment 0 $forward @(
    (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0),
    (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0))
$straightAnalysis = Invoke-Analysis @($straight)
Assert-Equal 21 $straightAnalysis.Path.Count 'A one-metre straight must produce twenty 0.05 m intervals plus the initial point.'
Assert-Near 0.0 $straightAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter 0.000000001 `
    'A straight must have zero peak d-kappa/d-s.'
foreach ($point in $straightAnalysis.Path) {
    Assert-Near 0.0 $point.VehicleCurvatureDerivative 0.000000001 `
        'A straight point must carry zero d-kappa/d-s.'
}
for ($index = 1; $index -lt $straightAnalysis.Path.Count; $index++) {
    $left = $straightAnalysis.Path[$index - 1]
    $right = $straightAnalysis.Path[$index]
    $distance = [Math]::Sqrt(($right.X - $left.X) * ($right.X - $left.X) + ($right.Y - $left.Y) * ($right.Y - $left.Y))
    Assert-Near 0.05 $distance 0.000000001 'Straight resampling intervals must be exactly 0.05 m.'
    Assert-Near 0.0 $right.GeometricCurvature 0.000000001 'A forward straight must have zero geometric curvature.'
    Assert-Near 0.0 $right.VehicleCurvature 0.000000001 'A forward straight must have zero vehicle curvature.'
}
$straightEnd = $straightAnalysis.Path[$straightAnalysis.Path.Count - 1]
Assert-Near 1.0 $straightEnd.X 0.0 'Resampling must retain the exact final X coordinate.'
Assert-Near 0.0 $straightEnd.Y 0.0 'Resampling must retain the exact final Y coordinate.'

# Raw coarse anchors carry the vehicle pose, which may differ slightly from the chord tangent of a finite integration step.
$poseAnchoredCurve = New-DirectionSegment 0 $forward @(
    (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0),
    (New-GeometryPoint 1.0 0.2 1.0 0.4 0.4))
$poseAnchoredAnalysis = Invoke-Analysis @($poseAnchoredCurve)
Assert-Near 0.0 $poseAnchoredAnalysis.Path[0].Heading 0.000000000001 'Geometry analysis must retain the first coarse-anchor heading rather than replace it with a chord tangent.'
$poseAnchoredEnd = $poseAnchoredAnalysis.Path[$poseAnchoredAnalysis.Path.Count - 1]
Assert-Near 0.4 $poseAnchoredEnd.Heading 0.000000000001 'Geometry analysis must retain the final coarse-anchor heading rather than replace it with a chord tangent.'

# A forward R=2 quarter circle has positive +0.5 1/m vehicle curvature.
$forwardArcPoints = New-Object System.Collections.Generic.List[object]
for ($index = 0; $index -le 32; $index++) {
    $theta = ([Math]::PI / 2.0) * $index / 32.0
    $x = 2.0 * [Math]::Sin($theta)
    $y = 2.0 * (1.0 - [Math]::Cos($theta))
    $arcLength = 2.0 * $theta
    [void]$forwardArcPoints.Add((New-GeometryPoint $x $y $arcLength $theta $theta))
}
$forwardArc = New-DirectionSegment 0 $forward $forwardArcPoints.ToArray()
$forwardArcAnalysis = Invoke-Analysis @($forwardArc)
$forwardArcMidpoint = $forwardArcAnalysis.Path[[int]($forwardArcAnalysis.Path.Count / 2)]
Assert-Near 0.5 $forwardArcMidpoint.GeometricCurvature 0.01 'An R=2 quarter circle must have geometric curvature +0.5 1/m.'
Assert-Near 0.5 $forwardArcMidpoint.VehicleCurvature 0.01 'A forward R=2 quarter circle must have vehicle curvature +0.5 1/m.'

# The same spatial R=2 circle in reverse retains geometric curvature but negates vehicle curvature.
$reverseArc = New-DirectionSegment 0 $reverse $forwardArcPoints.ToArray()
$reverseArcAnalysis = Invoke-Analysis @($reverseArc)
$reverseArcMidpoint = $reverseArcAnalysis.Path[[int]($reverseArcAnalysis.Path.Count / 2)]
Assert-Near 0.5 $reverseArcMidpoint.GeometricCurvature 0.01 'Reverse travel must not change geometric curvature.'
Assert-Near -0.5 $reverseArcMidpoint.VehicleCurvature 0.01 'A reverse R=2 quarter circle must have vehicle curvature -0.5 1/m.'

# Gear-switch poses are intentionally duplicated: they keep equal arc length and never enter a derivative denominator.
$forwardBeforeSwitch = New-DirectionSegment 0 $forward @(
    (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0),
    (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0 $true)) $false $true
$reverseAfterSwitch = New-DirectionSegment 1 $reverse @(
    (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0 $true),
    (New-GeometryPoint 0.0 0.0 2.0 0.0 0.0)) $true $false
$switchAnalysis = Invoke-Analysis @($forwardBeforeSwitch, $reverseAfterSwitch)
$firstSegment = $switchAnalysis.Segments[0]
$secondSegment = $switchAnalysis.Segments[1]
$switchLeft = $switchAnalysis.Path[$firstSegment.EndIndex]
$switchRight = $switchAnalysis.Path[$secondSegment.StartIndex]
Assert-Near $switchLeft.X $switchRight.X 0.0 'Gear-switch endpoints must retain duplicate X coordinates.'
Assert-Near $switchLeft.Y $switchRight.Y 0.0 'Gear-switch endpoints must retain duplicate Y coordinates.'
Assert-Near $switchLeft.ArcLength $switchRight.ArcLength 0.0 'Gear-switch endpoints must retain duplicate arc length.'
Assert-Equal 'Forward' $switchLeft.Direction.ToString() 'The first gear-switch pose must retain its forward segment direction.'
Assert-Equal 'Reverse' $switchRight.Direction.ToString() 'The second gear-switch pose must retain its reverse segment direction.'
Assert-True (-not [double]::IsNaN($switchLeft.GeometricCurvature)) 'No derivative may cross the gear-switch duplicate point.'
Assert-True (-not [double]::IsNaN($switchRight.GeometricCurvature)) 'No reverse derivative may cross the gear-switch duplicate point.'
Assert-True (-not [double]::IsNaN($switchLeft.VehicleCurvatureDerivative)) `
    'The forward side of a gear switch must have a finite one-sided derivative.'
Assert-True (-not [double]::IsNaN($switchRight.VehicleCurvatureDerivative)) `
    'The reverse side of a gear switch must have a finite one-sided derivative.'

# Curvature at a curved segment's end and the following straight reverse segment's start must remain independently differentiated.
$forwardArcToSwitch = New-DirectionSegment 0 $forward $forwardArcPoints.ToArray() $false $true
$reverseStraightAfterArc = New-DirectionSegment 1 $reverse @(
    (New-GeometryPoint 2.0 2.0 0.0 ([Math]::PI / 2.0) ([Math]::PI / 2.0) $true),
    (New-GeometryPoint 2.0 1.0 1.0 ([Math]::PI / 2.0) ([Math]::PI / 2.0))) $true $false
$curveSwitchAnalysis = Invoke-Analysis @($forwardArcToSwitch, $reverseStraightAfterArc)
$reverseStraightStart = $curveSwitchAnalysis.Path[$curveSwitchAnalysis.Segments[1].StartIndex]
Assert-Near 0.0 $reverseStraightStart.GeometricCurvature 0.000000001 'A gear-switch must not use the preceding curve to differentiate a reverse straight segment.'
Assert-Near 0.0 $reverseStraightStart.VehicleCurvatureDerivative 0.000000001 `
    'No curvature derivative may cross from the preceding forward arc into a reverse straight.'

$physicalStart = New-DirectionSegmentWithStartCurvature 0 $forward 0.20 @(
    (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0),
    (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0))
$physicalStartAnalysis = Invoke-Analysis @($physicalStart)
Assert-Near 0.20 $physicalStartAnalysis.Path[0].VehicleCurvature 0.000000001 `
    'The unified analyzer must retain a real start steering-curvature boundary state.'
Assert-True ($physicalStartAnalysis.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter -gt 0.0) `
    'A real start-curvature mismatch must remain visible to the quality analyzer.'

# A boundary that claims a gear switch must be a duplicated pose with opposite direction; discontinuities are rejected.
$invalidSwitch = New-DirectionSegment 1 $reverse @(
    (New-GeometryPoint 1.2 0.0 1.0 0.0 0.0 $true),
    (New-GeometryPoint 0.2 0.0 2.0 0.0 0.0)) $true $false
Assert-AnalysisRejected @($forwardBeforeSwitch, $invalidSwitch) 'A discontinuous gear-switch boundary must be rejected.'
$trailingGearSwitch = New-DirectionSegment 0 $forward @(
    (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0),
    (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0)) $false $true
Assert-AnalysisRejected @($trailingGearSwitch) 'The final direction segment must not advertise a non-existent trailing gear switch.'

# The public preprocessor receives a raw coarse path and resets every prepared direction segment to local arc length zero.
$coarsePoints = [Array]::CreateInstance($coarsePointType, 4)
$coarsePoints.SetValue((New-CoarsePoint 0.0 0.0 0.0 $forward), 0)
$coarsePoints.SetValue((New-CoarsePoint 1.0 0.0 1.0 $forward), 1)
$coarsePoints.SetValue((New-CoarsePoint 1.0 0.0 1.0 $reverse $true), 2)
$coarsePoints.SetValue((New-CoarsePoint 0.0 0.0 2.0 $reverse), 3)
$coarseSegments = [Array]::CreateInstance($coarseSegmentType, 2)
$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 1, $false, $true)), 0)
$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(1, $reverse, 2, 3, $true, $false)), 1)
$vehicle = [Activator]::CreateInstance($vehicleType)
$vehicle.LengthMeters = 1.0
$vehicle.WidthMeters = 0.5
$vehicle.SafetyMarginMeters = 0.0
$vehicle.MaximumCurvaturePerMeter = 1.0
$configuration = [Activator]::CreateInstance($configurationType)
$uninitializedMap = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject($mapType)
$request = [Activator]::CreateInstance($requestType, @($coarsePoints, $coarseSegments, $uninitializedMap, $vehicle, $configuration))
$preprocessor = [Activator]::CreateInstance($preprocessorType)
$prepareMethod = $preprocessorType.GetMethod('TryPrepare')
$prepareArguments = [object[]]@($request, $null, $null)
$prepared = $prepareMethod.Invoke($preprocessor, $prepareArguments)
Assert-True $prepared ('Preprocessor must accept a legal forward/reverse raw coarse path. Reason=' + $prepareArguments[2])
Assert-Near 0.0 $prepareArguments[1].Segments[1].Points[0].ArcLength 0.0 'Every prepared direction segment must begin at local arc length zero.'
Assert-True $prepareArguments[1].Segments[1].Points[0].IsGearSwitchPoint 'The reverse prepared segment must retain its gear-switch point.'

# Finite coordinates can still overflow distance arithmetic; public resampling must reject them instead of emitting NaN/Infinity.
$overflowSegment = New-DirectionSegment 0 $forward @(
    (New-GeometryPoint -1.0e308 0.0 0.0 0.0 0.0),
    (New-GeometryPoint 1.0e308 0.0 1.0 0.0 0.0))
$resampler = [Activator]::CreateInstance($resamplerType)
$segmentResampleMethod = @($resamplerType.GetMethods() | Where-Object {
    $_.Name -eq 'TryResample' -and $_.GetParameters()[0].ParameterType -eq $segmentType
})[0]
$resampleArguments = [object[]]@($overflowSegment, [double]0.05, $null, $null)
$resampled = $segmentResampleMethod.Invoke($resampler, $resampleArguments)
Assert-True (-not $resampled) ('Resampling must reject a distance overflow. Reason=' + $resampleArguments[3])

# A prepared path must reject null direction segments rather than silently dropping them during flattening.
$nullSegmentArray = [Array]::CreateInstance($segmentType, 1)
$nullSegmentRejected = $false
try { [void][Activator]::CreateInstance($preparedPathType, @($nullSegmentArray)) } catch { $nullSegmentRejected = $true }
Assert-True $nullSegmentRejected 'PreparedPath must reject a null direction segment.'

# Unwrapped heading must not jump by 2π when tangents cross the -π/π branch cut.
$crossing = New-DirectionSegment 0 $forward @(
    (New-GeometryPoint 0.0 0.0 0.0 (170.0 * [Math]::PI / 180.0) (170.0 * [Math]::PI / 180.0)),
    (New-GeometryPoint -1.0 ([Math]::Tan(10.0 * [Math]::PI / 180.0)) 1.015 (170.0 * [Math]::PI / 180.0) (170.0 * [Math]::PI / 180.0)),
    (New-GeometryPoint -2.0 0.0 2.03 (-170.0 * [Math]::PI / 180.0) (-170.0 * [Math]::PI / 180.0)))
$crossingAnalysis = Invoke-Analysis @($crossing)
for ($index = 1; $index -lt $crossingAnalysis.Path.Count; $index++) {
    $difference = [Math]::Abs($crossingAnalysis.Path[$index].UnwrappedHeading - $crossingAnalysis.Path[$index - 1].UnwrappedHeading)
    Assert-True ($difference -lt [Math]::PI) 'Unwrapped headings must remain continuous across the ±π branch cut.'
}

# The request preprocessor must reset arc length independently for every direction segment,
# while retaining the duplicated pose that represents a legal forward-to-reverse gear switch.
$preprocessor = [Activator]::CreateInstance($preprocessorType)
$prepareMethod = $preprocessorType.GetMethod('TryPrepare')
Assert-True ($null -ne $prepareMethod) 'PathSmoothingPreprocessor must expose TryPrepare.'
$coarsePath = [Array]::CreateInstance($coarsePointType, 5)
$coarsePath.SetValue((New-CoarsePathPoint 0.0 0.0 0.0 $forward $false 'Start'), 0)
$coarsePath.SetValue((New-CoarsePathPoint 1.0 0.0 1.0 $forward), 1)
$coarsePath.SetValue((New-CoarsePathPoint 2.0 0.0 2.0 $forward), 2)
$coarsePath.SetValue((New-CoarsePathPoint 2.0 0.0 2.0 $reverse $true), 3)
$coarsePath.SetValue((New-CoarsePathPoint 1.0 0.0 3.0 $reverse), 4)
$coarseSegments = [Array]::CreateInstance($coarseSegmentType, 2)
$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 2, $false, $true)), 0)
$coarseSegments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(1, $reverse, 3, 4, $true, $false)), 1)
$vehicle = [Activator]::CreateInstance($vehicleType)
$vehicle.LengthMeters = [double]0.80
$vehicle.WidthMeters = [double]0.60
$vehicle.SafetyMarginMeters = [double]0.05
$vehicle.MaximumCurvaturePerMeter = [double]0.80
$configuration = [Activator]::CreateInstance($smoothingConfigurationType)
$smoothingRequest = [Activator]::CreateInstance($smoothingRequestType, @(
    $coarsePath, $coarseSegments, (New-EmptyGeometryMap), $vehicle, $configuration))
$prepareArguments = [object[]]@($smoothingRequest, $null, $null)
Assert-True $prepareMethod.Invoke($preprocessor, $prepareArguments) ('Preprocessor must accept legal forward/reverse topology. Reason=' + $prepareArguments[2])
$preparedPath = $prepareArguments[1]
Assert-Equal 2 $preparedPath.Segments.Count 'Preprocessor must preserve both direction segments.'
Assert-Near 0.0 $preparedPath.Segments[1].Points[0].ArcLength 0.0 'The reverse segment must restart local arc length at zero.'
Assert-True $preparedPath.Segments[1].Points[0].IsGearSwitchPoint 'The duplicate reverse gear-switch point must be retained.'

# Segments may meet only at a paired, coincident forward/reverse gear switch.
$illegalGearJump = New-DirectionSegment 1 $reverse @(
    (New-GeometryPoint 1.25 0.0 1.0 0.0 0.0 $true),
    (New-GeometryPoint 0.25 0.0 2.0 0.0 0.0)) $true $false
Invoke-RejectedAnalysis @($forwardBeforeSwitch, $illegalGearJump) 'A gear-switch boundary whose poses differ must be rejected.'
$illegalNormalBoundary = New-DirectionSegment 1 $forward @(
    (New-GeometryPoint 2.0 0.0 0.0 0.0 0.0),
    (New-GeometryPoint 3.0 0.0 1.0 0.0 0.0)) $false $false
Invoke-RejectedAnalysis @($straight, $illegalNormalBoundary) 'A non-gear segment boundary must be rejected.'

# Finite endpoint coordinates can still overflow while computing their separation; reject before interpolation.
$resampler = [Activator]::CreateInstance($resamplerType)
$resamplePointsMethod = $resamplerType.GetMethods() | Where-Object {
    $_.Name -eq 'TryResample' -and $_.GetParameters().Length -eq 4 -and
    $_.GetParameters()[0].ParameterType -eq [System.Collections.Generic.IReadOnlyList``1].MakeGenericType($pointType)
} | Select-Object -First 1
Assert-True ($null -ne $resamplePointsMethod) 'ArcLengthResampler must expose point-list TryResample.'
$hugePoints = [Array]::CreateInstance($pointType, 2)
$hugeCoordinate = [double]::MaxValue / 2.0
$hugePoints.SetValue((New-GeometryPoint (-$hugeCoordinate) 0.0 0.0 0.0 0.0), 0)
$hugePoints.SetValue((New-GeometryPoint $hugeCoordinate 0.0 1.0 0.0 0.0), 1)
$resampleArguments = [object[]]@($hugePoints, [double]0.05, $null, $null)
Assert-False $resamplePointsMethod.Invoke($resampler, $resampleArguments) 'Resampling must reject an infinite geometric distance caused by finite coordinates.'

# PreparedPath is an all-or-nothing immutable topology snapshot: null direction segments are invalid.
$nullPreparedSegments = [Array]::CreateInstance($segmentType, 1)
Assert-Throws { [Activator]::CreateInstance($preparedPathType, @($nullPreparedSegments)) } 'PreparedPath must reject null direction segments.'

# Segment indices are deliberately dense and equal to their position in the candidate array.
$sparseSegment = New-DirectionSegment 2 $forward @(
    (New-GeometryPoint 0.0 0.0 0.0 0.0 0.0),
    (New-GeometryPoint 1.0 0.0 1.0 0.0 0.0))
Invoke-RejectedAnalysis @($sparseSegment) 'Prepared direction-segment indices must match their dense array position.'

Write-Output 'Path smoothing geometry checks passed.'
