import XCTest
@testable import RideFormCore

final class DiagnosticsTests: XCTestCase {
    func testKnownStationaryNoiseDriftExcludesMovement() throws {
        var session = analysisSession([])
        session.manifest.clockMapping = ClockMapping(sensorToHostOffset: 100, validated: true)
        session.events = [EventMarker(kind: "diagnosticPhase", label: "STATIONARY", hostUptime: 100),
                          EventMarker(kind: "diagnosticPhase", label: "MOVEMENT", hostUptime: 130)]
        session.motion.samples = (0...30).map { i -> MotionSample in
            let t = Double(i)
            let bias: Double = i < 30 ? (i % 2 == 0 ? 1.0 : 3.0) : 1000.0
            return
            MotionSample(sensorTimestamp: t, attitudeQuaternion: OrientationMath.quaternion(rotationVector: Vector3(z: t*0.001)),
                         rotationRate: Vector3(x: bias), gravity: Vector3(z: -1), userAcceleration: Vector3(y: 2))
        }
        let result = try SensorDiagnostics.analyze(session: session)
        XCTAssertEqual(result.fusedGyro.x.sampleCount, 30)
        XCTAssertEqual(try XCTUnwrap(result.fusedGyro.x.mean), 2, accuracy: 1e-12)
        XCTAssertEqual(try XCTUnwrap(result.fusedGyro.x.standardDeviation), 1, accuracy: 1e-12)
        XCTAssertEqual(try XCTUnwrap(result.fusedGyro.x.rms), sqrt(5), accuracy: 1e-12)
        XCTAssertEqual(try XCTUnwrap(result.gravityNorm.mean), 1, accuracy: 1e-12)
        XCTAssertEqual(try XCTUnwrap(result.driftSlopeRadiansPerSecond).z, 0.001, accuracy: 1e-10)
        XCTAssertEqual(try XCTUnwrap(result.driftEndMinusStartRadians).z, 0.029, accuracy: 1e-10)
        XCTAssertGreaterThan(try XCTUnwrap(result.quaternionAngleDispersionRadians.rms), 0)
        XCTAssertEqual(result.stationarySampling["motion"]?.count, 30)
    }
    func testMissingPhasesMappingAndInsufficientAreUnavailable() throws {
        var session = analysisSession([MotionSample(sensorTimestamp: 0)])
        var result = try SensorDiagnostics.analyze(session: session)
        XCTAssertNil(result.fusedGyro.x.mean)
        XCTAssertTrue(result.qualityFlags.contains("stationary_phase_incomplete"))
        XCTAssertTrue(result.qualityFlags.contains("clock_mapping_unavailable"))
        session.manifest.clockMapping = ClockMapping(sensorToHostOffset: 0, validated: true)
        session.events = [EventMarker(kind: "diagnosticPhase", label: "STATIONARY", hostUptime: 0), EventMarker(kind: "diagnosticPhase", label: "END", hostUptime: 1)]
        result = try SensorDiagnostics.analyze(session: session)
        XCTAssertEqual(result.fusedGyro.x.sampleCount, 1)
        XCTAssertNil(result.fusedGyro.x.standardDeviation)
        XCTAssertNil(result.driftSlopeRadiansPerSecond)
        XCTAssertTrue(result.qualityFlags.contains("stationary_phase_shorter_than_30_seconds"))
    }
    func testRawOnlyDiagnosticsWithoutCalibrationAndIndependentTimes() throws {
        var session = MotionSession()
        session.manifest.clockMapping = ClockMapping(sensorToHostOffset: 0, validated: true)
        session.events = [EventMarker(kind: "diagnosticPhase", label: "STATIONARY", hostUptime: 10), EventMarker(kind: "diagnosticPhase", label: "MOVEMENT", hostUptime: 40)]
        session.gyroscope.samples = [RawGyroscopeSample(sensorTimestamp: 10.1, rotationRate: Vector3(z: 3)), RawGyroscopeSample(sensorTimestamp: 20.3, rotationRate: Vector3(z: 5)), RawGyroscopeSample(sensorTimestamp: 40.1, rotationRate: Vector3(z: 900))]
        session.accelerometer.samples = [RawAccelerometerSample(sensorTimestamp: 11, acceleration: Vector3(z: -1)), RawAccelerometerSample(sensorTimestamp: 21, acceleration: Vector3(z: -1))]
        let result = try SensorDiagnostics.analyze(session: session)
        XCTAssertEqual(result.rawGyro.z.mean, 4)
        XCTAssertEqual(result.rawAccelerationIncludingGravity.z.mean, -1)
        XCTAssertNil(result.userAcceleration.x.mean)
        XCTAssertNil(result.driftEndMinusStartRadians)
        XCTAssertTrue(result.qualityFlags.contains("rider_orientation_unavailable_until_calibrated"))
        XCTAssertEqual(result.events, session.events)
    }
    func testDisplayDecimationPreservesExtremaAndGapEdges() throws {
        let samples: [MotionSample] = (0..<1000).map { i in
            let t = Double(i)*0.01 + (i >= 500 ? 2.0 : 0.0)
            let velocity: Double = i == 249 ? 500.0 : (i == 751 ? -600.0 : 0.0)
            return MotionSample(sequence: UInt64(i), sensorTimestamp: t, rotationRate: Vector3(x: velocity))
        }
        let full = try MotionAnalyzer.analyze(session: analysisSession(samples)).rawTrace
        let displayed = TraceDisplayDecimator.decimate(full, targetBuckets: 10)
        XCTAssertLessThan(displayed.count, full.count)
        for i in [0,249,499,500,751,999] { XCTAssertTrue(displayed.contains { $0.sequence == UInt64(i) }) }
        XCTAssertEqual(displayed.map { $0.angularVelocity.x }.max(), full.map { $0.angularVelocity.x }.max())
        XCTAssertEqual(displayed.map { $0.angularVelocity.x }.min(), full.map { $0.angularVelocity.x }.min())
    }
}
