import XCTest
@testable import RideFormCore

final class SmoothnessTests: XCTestCase {
    func testEvenGridIncludesNyquistEnergyWithOneSidedWeighting() throws {
        var configuration = MotionAnalyzer.defaultConfiguration
        configuration.resamplingParameters["windowHann"] = 0.1
        func metric(lowAmplitude: Double) -> SmoothnessMetrics {
            let trace = (0..<100).map { i in
                let alternating = i % 2 == 0 ? 1.0 : -1.0
                let slow = lowAmplitude*cos(2 * Double.pi*Double(i)/100)
                return AnalysisPoint(timestamp: Double(i)/50, sequence: UInt64(i), segmentID: 0, anglesRadians: Vector3(), angularVelocity: Vector3(x: alternating+slow), angularAcceleration: Vector3(), angularJerk: Vector3(), userAcceleration: Vector3())
            }
            return SmoothnessMetrics.compute(trace: trace, excursionRadians: 1, configuration: configuration)
        }
        let pure = metric(lowAmplitude: 0)
        XCTAssertEqual(pure.spectral.timestamps.count, 100)
        XCTAssertEqual(try XCTUnwrap(pure.spectral.highFrequencyEnergyRatio.value), 1, accuracy: 1e-10)
        XCTAssertEqual(try XCTUnwrap(metric(lowAmplitude: 1).spectral.highFrequencyEnergyRatio.value), 2.0/3, accuracy: 1e-10)
    }
    func testTimeAndAmplitudeNormalizationDoesNotImposeRanking() throws {
        func trace(duration: Double, amplitude: Double) -> [AnalysisPoint] {
            (0...100).map { i in
                let u = Double(i)/100, t = u*duration
                // theta = amplitude*u^3, gyro = derivative, jerk = 6A/T^3.
                return AnalysisPoint(timestamp: t, sequence: UInt64(i), segmentID: 0, anglesRadians: Vector3(x: amplitude*u*u*u), angularVelocity: Vector3(x: -3*amplitude*u*u/duration), angularAcceleration: Vector3(x: -6*amplitude*u/(duration*duration)), angularJerk: Vector3(x: -6*amplitude/pow(duration,3)), userAcceleration: Vector3())
            }
        }
        let a = SmoothnessMetrics.compute(trace: trace(duration: 1, amplitude: 1), excursionRadians: 1, configuration: MotionAnalyzer.defaultConfiguration)
        let b = SmoothnessMetrics.compute(trace: trace(duration: 2, amplitude: 3), excursionRadians: 3, configuration: MotionAnalyzer.defaultConfiguration)
        XCTAssertEqual(try XCTUnwrap(a.normalizedSquaredLateralJerk.value), 36, accuracy: 1e-8)
        XCTAssertEqual(try XCTUnwrap(b.normalizedSquaredLateralJerk.value), 36, accuracy: 1e-8)
        XCTAssertNotEqual(a.integratedSquaredLateralJerk.value, b.integratedSquaredLateralJerk.value)
    }
    func testActualIntervalIntegralAndNormalization() throws {
        let times = [0.0,0.07,0.18,0.27,0.39,0.51,0.63]
        let trace = times.enumerated().map { i,t in AnalysisPoint(timestamp: t, sequence: UInt64(i), segmentID: 0, anglesRadians: Vector3(), angularVelocity: Vector3(), angularAcceleration: Vector3(), angularJerk: Vector3(x: -6), userAcceleration: Vector3()) }
        let metric = SmoothnessMetrics.compute(trace: trace, excursionRadians: 2, configuration: MotionAnalyzer.defaultConfiguration)
        XCTAssertEqual(try XCTUnwrap(metric.integratedSquaredLateralJerk.value), 36*0.63, accuracy: 1e-10)
        XCTAssertEqual(try XCTUnwrap(metric.rmsLateralJerk.value), 6, accuracy: 1e-10)
        XCTAssertEqual(try XCTUnwrap(metric.peakLateralJerk.value), 6, accuracy: 1e-10)
        XCTAssertEqual(try XCTUnwrap(metric.normalizedSquaredLateralJerk.value), pow(0.63,5)/4*36*0.63, accuracy: 1e-10)
        let zero = SmoothnessMetrics.compute(trace: trace, excursionRadians: 0, configuration: MotionAnalyzer.defaultConfiguration)
        XCTAssertEqual(zero.normalizedSquaredLateralJerk.unavailableReason, "near_zero_excursion")
    }
    func testPerturbationSpectralBandwidthAndGap() throws {
        func trace(high: Double, step: Double = 0.01) -> [AnalysisPoint] {
            (0...Int(2/step)).map { i in
                let t = Double(i)*step
                return AnalysisPoint(timestamp: t, sequence: UInt64(i), segmentID: 0, anglesRadians: Vector3(), angularVelocity: Vector3(x: sin(2 * .pi*t)+high*sin(16 * .pi*t)), angularAcceleration: Vector3(), angularJerk: Vector3(x: -pow(2 * .pi,2)*sin(2 * .pi*t)-high*pow(16 * .pi,2)*sin(16 * .pi*t)), userAcceleration: Vector3())
            }
        }
        let clean = SmoothnessMetrics.compute(trace: trace(high: 0), excursionRadians: 1, configuration: MotionAnalyzer.defaultConfiguration)
        let noisy = SmoothnessMetrics.compute(trace: trace(high: 0.3), excursionRadians: 1, configuration: MotionAnalyzer.defaultConfiguration)
        XCTAssertGreaterThan(try XCTUnwrap(noisy.spectral.highFrequencyEnergyRatio.value), try XCTUnwrap(clean.spectral.highFrequencyEnergyRatio.value)+0.05)
        XCTAssertGreaterThan(try XCTUnwrap(noisy.integratedSquaredLateralJerk.value), try XCTUnwrap(clean.integratedSquaredLateralJerk.value)*10)
        XCTAssertEqual(noisy.spectral.window, "Hann; mean removed")
        XCTAssertEqual(noisy.spectral.gridHz, 50)
        let sparse = SmoothnessMetrics.compute(trace: trace(high: 0, step: 0.2), excursionRadians: 1, configuration: MotionAnalyzer.defaultConfiguration)
        XCTAssertEqual(sparse.spectral.highFrequencyEnergyRatio.unavailableReason, "ineffective_sampling_bandwidth")
        var broken = trace(high: 0)
        broken[100].segmentID = 1
        let gap = SmoothnessMetrics.compute(trace: broken, excursionRadians: 1, configuration: MotionAnalyzer.defaultConfiguration)
        XCTAssertEqual(gap.integratedSquaredLateralJerk.unavailableReason, "sampling_gap")
        XCTAssertEqual(gap.spectral.highFrequencyEnergyRatio.unavailableReason, "sampling_gap")
    }
}
