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

$ErrorActionPreference = 'Stop'
$newtonsoftPath = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll'
if (Test-Path -LiteralPath $newtonsoftPath) { [Reflection.Assembly]::LoadFrom($newtonsoftPath) | Out-Null }
$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath))

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

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

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

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

function Text-FromCodePoints([int[]]$CodePoints) {
    return (($CodePoints | ForEach-Object { [Char]::ConvertFromUtf32($_) }) -join '')
}

$visualization = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization.'
$styleType = Get-RequiredType ($visualization + 'IeeeFigureStyle')

foreach ($expectation in @(
    @{ Name = 'FigureWidthPoints'; Value = 515.52 },
    @{ Name = 'FigureHeightPoints'; Value = 374.4 },
    @{ Name = 'RawColor'; Value = '#4D4D4D' },
    @{ Name = 'BSplineColor'; Value = '#0072B2' },
    @{ Name = 'BezierColor'; Value = '#D55E00' },
    @{ Name = 'QuinticColor'; Value = '#009E73' },
    @{ Name = 'LimitColor'; Value = '#CC79A7' }
)) {
    $field = $styleType.GetField($expectation.Name)
    if ($null -eq $field) { throw "IeeeFigureStyle must expose $($expectation.Name)." }
    if ($expectation.Value -is [double]) {
        Assert-Near $expectation.Value $field.GetValue($null) "IeeeFigureStyle $($expectation.Name) must match the fixed report style."
    }
    else {
        Assert-Equal $expectation.Value $field.GetValue($null) "IeeeFigureStyle $($expectation.Name) must match the fixed report style."
    }
}

$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.'
$comparison = $root + 'Comparison.'
$facade = $root + 'Facade.'
$coarse = 'MultiWheelC.TrajectoryPlanning.CoarsePath.'
$mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.'
$test = $root + 'Test.'

$modelType = Get-RequiredType ($visualization + 'SmoothingFigureModel')
$builderType = Get-RequiredType ($visualization + 'SmoothingFigureModelBuilder')
$figureDefinitionType = Get-RequiredType ($visualization + 'SmoothingFigureDefinition')
$figureSetBuilderType = Get-RequiredType ($visualization + 'SmoothingFigureSetBuilder')
$rendererType = Get-RequiredType ($visualization + 'SmoothingSvgRenderer')
$csvWriterType = Get-RequiredType ($visualization + 'SmoothingCsvWriter')
$comparisonResultType = Get-RequiredType ($comparison + 'PathSmoothingComparisonResult')
$comparisonRequestType = Get-RequiredType ($comparison + 'PathSmoothingComparisonRequest')
$comparisonServiceType = Get-RequiredType ($facade + 'PathSmoothingComparisonService')
$scenarioFactoryType = Get-RequiredType ($test + 'SmoothingScenarioFactory')
$gridMapType = Get-RequiredType ($mapping + 'PlanningGridMap')
$poseType = Get-RequiredType ($coarse + 'Pose2D')

$buildMethod = $builderType.GetMethod('Build', [Type[]]@($comparisonResultType, $gridMapType, $poseType, $poseType, [string], [string]))
if ($null -eq $buildMethod) { throw 'SmoothingFigureModelBuilder must expose Build(comparison, map, start, goal, scenarioId, scenarioLabel).' }
Assert-Equal $modelType $buildMethod.ReturnType 'SmoothingFigureModelBuilder.Build must return the immutable figure model.'
$figureSetBuildMethod = $figureSetBuilderType.GetMethod('Build', [Type[]]@($modelType))
if ($null -eq $figureSetBuildMethod) { throw 'SmoothingFigureSetBuilder must expose Build(SmoothingFigureModel).' }
$renderMethod = $rendererType.GetMethod('Render', [Type[]]@($figureDefinitionType))
if ($null -eq $renderMethod) { throw 'SmoothingSvgRenderer must expose Render(SmoothingFigureDefinition).' }
$writeMethod = $csvWriterType.GetMethod('Write', [Type[]]@($modelType))
if ($null -eq $writeMethod) { throw 'SmoothingCsvWriter must expose Write(SmoothingFigureModel).' }

$fixturePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'
$fixtureRequests = $scenarioFactoryType.GetMethod('CreateFixtureRequests', [Type[]]@([string])).Invoke($null, @((Resolve-Path $fixturePath).Path))
$comparisonRequest = $fixtureRequests[0]
Assert-Equal $comparisonRequestType $comparisonRequest.GetType() 'The figure test must consume a real immutable comparison request.'
$comparisonResult = [Activator]::CreateInstance($comparisonServiceType).Compare($comparisonRequest, [Threading.CancellationToken]::None)
$coarsePath = $comparisonRequest.SmoothingRequest.CoarsePath
$start = [Activator]::CreateInstance($poseType, @($coarsePath[0].X, $coarsePath[0].Y, $coarsePath[0].Heading))
$last = $coarsePath[$coarsePath.Count - 1]
$goal = [Activator]::CreateInstance($poseType, @($last.X, $last.Y, $last.Heading))
$builder = [Activator]::CreateInstance($builderType)
$rawLabel = Text-FromCodePoints @(0x539F, 0x59CB, 0x7C97, 0x8DEF, 0x5F84)
$bSplineLabel = (Text-FromCodePoints @(0x4E09, 0x6B21)) + ' B ' + (Text-FromCodePoints @(0x6837, 0x6761))
$bezierLabel = (Text-FromCodePoints @(0x5C40, 0x90E8, 0x4E09, 0x6B21)) + ' B' + [Char]::ConvertFromUtf32(0x00E9) + 'zier'
$quinticLabel = Text-FromCodePoints @(0x5206, 0x6BB5, 0x4E94, 0x6B21)
$scenarioId = (Text-FromCodePoints @(0x573A, 0x666F)) + '-escape'
$scenarioLabel = (Text-FromCodePoints @(0x8DEF, 0x5F84)) + ' <A&>'
$model = $buildMethod.Invoke($builder, @($comparisonResult, $comparisonRequest.SmoothingRequest.Map, $start, $goal, $scenarioId, $scenarioLabel))

$figureSet = $figureSetBuildMethod.Invoke([Activator]::CreateInstance($figureSetBuilderType), @($model))
$figures = @($figureSet.Figures)
$expectedStems = @('01-coarse-path-overview', '02-all-paths-comparison', '03-cubic-bspline-overview', '04-local-cubic-bezier-overview', '05-piecewise-quintic-overview', '06-curvature-comparison')
Assert-Equal ($expectedStems -join ',') (($figures | ForEach-Object { $_.FileStem }) -join ',') 'Figure set must publish the stable six-file order.'
Assert-True (-not $figures[1].ShowsMapContext) 'All-path comparison must exclude obstacles and endpoint decorations.'
Assert-True ($figures[2].ShowsMapContext -and $figures[2].Series[0].Opacity -lt 1) 'Individual smoother views must retain faded coarse-path context.'
Assert-Near (($figures[0].WorldXMaxMeters - $figures[0].WorldXMinMeters) / ($figures[0].WorldYMaxMeters - $figures[0].WorldYMinMeters)) ($figures[0].PlotWidthPoints / $figures[0].PlotHeightPoints) 'Overhead figures must preserve equal X/Y scale.'

$renderer = [Activator]::CreateInstance($rendererType)
$svgByStem = @{}
foreach ($figure in $figures) { $svgByStem[$figure.FileStem] = $renderMethod.Invoke($renderer, @($figure)) }
$svg = (($expectedStems | ForEach-Object { $svgByStem[$_] }) -join "`n")
Assert-True ($svg -match '<svg[^>]+viewBox="0 0 515\.52 374\.4"') 'SVG must use the fixed physical view box in typographic points.'
Assert-True $svg.Contains(($scenarioLabel -replace '&', '&amp;' -replace '<', '&lt;' -replace '>', '&gt;')) 'SVG text must XML-escape scenario labels.'
Assert-True $svg.Contains('font-family="SimSun"') 'SVG must emit explicit SimSun Chinese text runs.'
Assert-True $svg.Contains('font-family="Times New Roman"') 'SVG must emit explicit Times New Roman Latin/math text runs.'
foreach ($color in @('#4D4D4D', '#0072B2', '#D55E00', '#009E73', '#CC79A7')) { Assert-True $svg.Contains($color) "SVG must retain the fixed color $color." }
Assert-True $svg.Contains('class="trajectory-point"') 'All visible trajectory samples must render as point markers.'
Assert-True (-not $svg.Contains('stroke-dasharray')) 'Trajectory reports must not use dashed path styles.'
Assert-True (-not $svg.Contains('<path')) 'Trajectory reports must not join samples with SVG path elements.'
Assert-True $svgByStem['02-all-paths-comparison'].Contains('X (m)') 'Overhead comparison must label the X coordinate in metres.'
Assert-True $svgByStem['06-curvature-comparison'].Contains('s (m)') 'Curvature comparison must label arc length in metres.'
$curvatureUnit = [Char]::ConvertFromUtf32(0x03BA) + ' (m' + [Char]::ConvertFromUtf32(0x207B) + [Char]::ConvertFromUtf32(0x00B9) + ')'
Assert-True $svgByStem['06-curvature-comparison'].Contains($curvatureUnit) 'Curvature comparison must label curvature units.'

$priorCulture = [Globalization.CultureInfo]::CurrentCulture
try {
    [Globalization.CultureInfo]::CurrentCulture = [Globalization.CultureInfo]::GetCultureInfo('de-DE')
    [byte[]]$csvBytes = $writeMethod.Invoke([Activator]::CreateInstance($csvWriterType), @($model))
}
finally {
    [Globalization.CultureInfo]::CurrentCulture = $priorCulture
}
Assert-Equal 0xEF $csvBytes[0] 'CSV must begin with the UTF-8 BOM.'
Assert-Equal 0xBB $csvBytes[1] 'CSV must begin with the UTF-8 BOM.'
Assert-Equal 0xBF $csvBytes[2] 'CSV must begin with the UTF-8 BOM.'
$csv = [Text.Encoding]::UTF8.GetString($csvBytes, 3, $csvBytes.Length - 3)
$header = 'ScenarioId,Method,Status,PathLengthMeters,MaximumAbsoluteVehicleCurvaturePerMeter,RootMeanSquareVehicleCurvaturePerMeter,TotalAbsoluteCurvatureVariationPerMeter,CurvatureVariationEnergy,MinimumBodyClearanceMeters,MedianElapsedMilliseconds,TimingSampleCount,IsDeterministic,RetryCount,AcceptedStrength'
Assert-True $csv.StartsWith($header + "`r`n") 'CSV must preserve the specified stable header order.'
Assert-True $csv.Contains($scenarioId) 'CSV must preserve UTF-8 Chinese scenario content.'
Assert-True ($csv -match ('(?m)^' + [Regex]::Escape($scenarioId) + ',[^,]+,[^,]+,\d+\.\d+')) 'CSV numeric values must use invariant decimal points.'

$entryType = Get-RequiredType ($comparison + 'PathSmoothingComparisonEntry')
$methodType = Get-RequiredType ($root + 'SmoothingMethod')
$statusType = Get-RequiredType ($root + 'PathSmoothingStatus')
$metricsType = Get-RequiredType ($root + 'PathQualityMetrics')
$pointType = Get-RequiredType ($root + 'SmoothedPathPoint')
$segmentType = Get-RequiredType ($root + 'SmoothedPathSegment')
$binding = [Reflection.BindingFlags]::Static -bor [Reflection.BindingFlags]::NonPublic
$candidateFactory = $entryType.GetMethods($binding) | Where-Object { $_.Name -eq 'CreateCandidate' } | Select-Object -First 1
if ($null -eq $candidateFactory) { throw 'Comparison entry must retain the internal candidate factory used by report diagnostics.' }
$resultCtor = $comparisonResultType.GetConstructors([Reflection.BindingFlags]::Instance -bor [Reflection.BindingFlags]::NonPublic) |
    Where-Object { $_.GetParameters().Count -eq 5 } | Select-Object -First 1
if ($null -eq $resultCtor) { throw 'Comparison result must retain its immutable internal constructor.' }
$infeasible = [Enum]::Parse($statusType, 'Infeasible')
$failed = [Enum]::Parse($statusType, 'Failed')
$bSpline = [Enum]::Parse($methodType, 'CubicBSpline')
$bezier = [Enum]::Parse($methodType, 'LocalCubicBezier')
$quintic = [Enum]::Parse($methodType, 'PiecewiseQuintic')
$raw = $comparisonResult.RawPathBaseline
$timing = $comparisonResult.Entries[0].Timing
$infeasibleEntry = $candidateFactory.Invoke($null, [object[]]@($bSpline, $infeasible, [Activator]::CreateInstance($metricsType), $timing, 'synthetic-infeasible', 'rejected geometry', 1, [double]0.5, $raw.Path, $raw.Segments))
$emptyPoints = [Array]::CreateInstance($pointType, 0)
$emptySegments = [Array]::CreateInstance($segmentType, 0)
$failedEntry = $candidateFactory.Invoke($null, [object[]]@($bezier, $failed, [Activator]::CreateInstance($metricsType), $timing, 'synthetic-failure', 'numeric failure', 0, [double]0.0, $emptyPoints, $emptySegments))
$quinticEntry = $comparisonResult.Entries | Where-Object { $_.Method.ToString() -eq $quintic.ToString() } | Select-Object -First 1
$syntheticEntries = [Array]::CreateInstance($entryType, 3)
$syntheticEntries.SetValue($infeasibleEntry, 0)
$syntheticEntries.SetValue($failedEntry, 1)
$syntheticEntries.SetValue($quinticEntry, 2)
$syntheticResult = $resultCtor.Invoke([object[]]@($raw, $syntheticEntries, $null, $false, 'synthetic report diagnostics'))
$syntheticModel = $buildMethod.Invoke($builder, @($syntheticResult, $comparisonRequest.SmoothingRequest.Map, $start, $goal, 'diagnostics', 'diagnostics'))
$infeasibleSeries = $syntheticModel.Series | Where-Object { $_.Key -eq 'bspline' } | Select-Object -First 1
$failedSeries = $syntheticModel.Series | Where-Object { $_.Key -eq 'bezier' } | Select-Object -First 1
Assert-True $infeasibleSeries.IsCurveVisible 'A complete rejected candidate must remain visible in the report model.'
Assert-True ($infeasibleSeries.ViolationMarkers.Count -gt 0) 'A rejected candidate must expose at least one violation cross marker.'
Assert-True (-not $failedSeries.IsCurveVisible) 'A numerical failure with no geometry must not produce a fake curve.'
$syntheticFigureSet = $figureSetBuildMethod.Invoke([Activator]::CreateInstance($figureSetBuilderType), @($syntheticModel))
$syntheticSvg = $renderMethod.Invoke($renderer, @($syntheticFigureSet.Figures[2]))
Assert-True $syntheticSvg.Contains('data-series="bspline"') 'Rejected candidate geometry must remain visible in the SVG.'
Assert-True $syntheticSvg.Contains('violation-cross') 'Rejected candidate SVG geometry must include violation crosses.'
Assert-True $syntheticSvg.Contains('Infeasible') 'Legend must retain the rejected candidate status.'
Assert-True (-not $syntheticSvg.Contains('data-series="bezier"')) 'Numerical failure must not emit fake trajectory samples.'
$syntheticCsvBytes = $writeMethod.Invoke([Activator]::CreateInstance($csvWriterType), @($syntheticModel))
$syntheticCsv = [Text.Encoding]::UTF8.GetString($syntheticCsvBytes, 3, $syntheticCsvBytes.Length - 3)
Assert-True $syntheticCsv.Contains('CubicBSpline,Infeasible') 'CSV must retain the rejected candidate status.'
Assert-True $syntheticCsv.Contains('LocalCubicBezier,Failed') 'CSV must retain a numerical failure row.'

Write-Output 'Path smoothing SVG/CSV checks passed.'
