import XCTest
@testable import RideForm
import RideFormCore

final class CalibrationIntegrationTests: XCTestCase {
    @MainActor func testFinalReturnOffsetDoesNotRejectAlreadyCapturedCalibration() throws {
        let controller = CalibrationController()
        controller.begin(recording: false, handsFree: true)
        for i in 0...6000 {
            if controller.waitingForVoice { controller.guidanceDidFinish() }
            let rotation: Vector3
            switch controller.stage {
            case .left: rotation = Vector3(x: -0.3)
            case .forward: rotation = Vector3(y: 0.3)
            case .returnToFinish: rotation = Vector3(y: 8 * .pi / 180)
            default: rotation = Vector3()
            }
            controller.ingest(MotionSample(sensorTimestamp: Double(i)/100, attitudeQuaternion: OrientationMath.quaternion(rotationVector: rotation)))
            if controller.stage == .awaitingConvention { break }
        }
        XCTAssertEqual(controller.stage, .awaitingConvention)
        XCTAssertEqual(controller.automaticRetries, 0)
        let summary = CaptureSummary(capabilities: [:], acceptedCounts: [:], ended: ClockAnchor(), clockMapping: ClockMapping(), rotationConvention: RotationConvention(capturedToCanonical: "identity", verified: true))
        XCTAssertTrue(controller.finish(summary: summary))
        XCTAssertTrue(try XCTUnwrap(controller.currentCalibration).neutralSamples.allSatisfy { $0.attitudeQuaternion == Quaternion() })
        XCTAssertEqual(controller.currentCalibration?.configuration.parameters["guidedFinalReturnAdvisory"], 1)
    }
    @MainActor func testSimilarGesturesRetryForwardPreservingCenterAndLeftThenSucceed() throws {
        let controller = CalibrationController()
        controller.begin(recording: false, handsFree: true)
        var tick = 0
        func collect(forward: Vector3) {
            for _ in 0..<1600 {
                if controller.stage == .awaitingConvention { return }
                if controller.waitingForVoice { controller.guidanceDidFinish() }
                let rotation = controller.stage == .left ? Vector3(x: -0.3) : controller.stage == .forward ? forward : Vector3()
                controller.ingest(MotionSample(sensorTimestamp: Double(tick)/100, attitudeQuaternion: OrientationMath.quaternion(rotationVector: rotation)))
                tick += 1
            }
        }
        let summary = CaptureSummary(capabilities: [:], acceptedCounts: [:], ended: ClockAnchor(), clockMapping: ClockMapping(), rotationConvention: RotationConvention(capturedToCanonical: "identity", verified: true))
        collect(forward: Vector3(x: -0.3))
        XCTAssertFalse(controller.finish(summary: summary))
        XCTAssertEqual(controller.stage, .returnToNeutral)
        XCTAssertTrue(controller.prompt.contains("saved"))
        collect(forward: Vector3(y: 0.3))
        XCTAssertTrue(controller.finish(summary: summary))
        let saved = try XCTUnwrap(controller.currentCalibration)
        XCTAssertLessThan(try XCTUnwrap(saved.neutralSamples.last).sensorTimestamp, 6)
        XCTAssertLessThan(try XCTUnwrap(saved.leftGestureSamples.last).sensorTimestamp, 7)
    }

    @MainActor func testReturnWithSmallOffsetAndBoundaryJitterAdvancesBothCenterSteps() throws {
        let controller = CalibrationController()
        controller.begin(recording: false, handsFree: true)
        var returns = Set<String>()
        for i in 0...1800 {
            if controller.waitingForVoice { controller.guidanceDidFinish() }
            let stage = controller.stage
            let t = Double(i)/100
            let rotation: Vector3
            switch stage {
            case .left: rotation = Vector3(x: -0.3)
            case .forward: rotation = Vector3(y: 0.3)
            case .returnToNeutral, .returnToFinish:
                returns.insert(String(describing: stage))
                // Roughly 4.6–6.3 degrees off the original center, crossing the entry boundary.
                rotation = Vector3(x: 0.095 + 0.015*sin(2 * .pi*t))
            default: rotation = Vector3()
            }
            controller.ingest(MotionSample(sensorTimestamp: t, attitudeQuaternion: OrientationMath.quaternion(rotationVector: rotation), rotationRate: Vector3(x: 0.095)))
            if controller.stage == .awaitingConvention { break }
        }
        XCTAssertEqual(controller.stage, .awaitingConvention)
        XCTAssertEqual(returns.count, 2)
        let summary = CaptureSummary(capabilities: [:], acceptedCounts: [:], ended: ClockAnchor(), clockMapping: ClockMapping(), rotationConvention: RotationConvention(capturedToCanonical: "identity", verified: true))
        XCTAssertTrue(controller.finish(summary: summary))
        let calibration = try XCTUnwrap(controller.currentCalibration)
        XCTAssertTrue(calibration.neutralSamples.allSatisfy { $0.attitudeQuaternion == Quaternion() }, "Returning must never redefine the original neutral")
        XCTAssertEqual(calibration.configuration.parameters["guidedReturnExitRadians"], 0.12)
    }

    @MainActor func testReturnExcursionResetsHoldAndRequiresReentry() {
        let controller = CalibrationController()
        controller.begin(recording: false, handsFree: true)
        var tick = 0
        func feed(_ angle: Double, count: Int) {
            for _ in 0..<count {
                controller.ingest(MotionSample(sensorTimestamp: Double(tick)/100, attitudeQuaternion: OrientationMath.quaternion(rotationVector: Vector3(x: angle))))
                tick += 1
            }
        }
        while controller.stage != .returnToNeutral && tick < 1000 {
            if controller.waitingForVoice { controller.guidanceDidFinish() }
            feed(controller.stage == .left ? -0.3 : 0, count: 1)
        }
        XCTAssertEqual(controller.stage, .returnToNeutral)
        controller.guidanceDidFinish()
        feed(0.09, count: 30)
        feed(0.14, count: 1)
        feed(0.11, count: 100)
        XCTAssertEqual(controller.stage, .returnToNeutral, "The exit band alone cannot start a new hold")
        feed(0.09, count: 1)
        feed(0.11, count: 70)
        XCTAssertEqual(controller.stage, .forward)
    }

    @MainActor func testUnreturnedLeanExplainsWaitThenTimesOutInsteadOfAdvancing() {
        let controller = CalibrationController()
        controller.begin(recording: false, handsFree: true)
        var returnStarted: Double?
        var checkedReminder = false
        for i in 0...6000 {
            if controller.waitingForVoice { controller.guidanceDidFinish() }
            let t = Double(i)/100
            let returning = controller.stage == .returnToNeutral
            if returning && returnStarted == nil { returnStarted = t }
            controller.ingest(MotionSample(sensorTimestamp: t, attitudeQuaternion: OrientationMath.quaternion(rotationVector: Vector3(x: controller.stage == .left || returning ? -0.3 : 0))))
            if let start = returnStarted, t-start > 7, !checkedReminder {
                checkedReminder = true
                XCTAssertTrue(controller.prompt.contains("Still waiting"))
                XCTAssertTrue(controller.prompt.contains("forward"))
                XCTAssertEqual(controller.stage, .returnToNeutral)
            }
        }
        XCTAssertEqual(controller.stage, .retry)
        XCTAssertTrue(checkedReminder)
        XCTAssertEqual(controller.automaticRetries, 2)
        XCTAssertEqual(controller.error, .timedOut)
        XCTAssertTrue(controller.prompt.contains("center"))
    }

    @MainActor func testSpokenGuideWaitsForInstructionThenCompletesWithoutTaps() throws {
        let controller = CalibrationController()
        controller.begin(recording: false, handsFree: true)
        controller.ingest(MotionSample(sensorTimestamp: 0))
        XCTAssertEqual(controller.sampleCount, 0, "Do not collect while instructions are speaking")
        var visited: Set<String> = []
        for i in 1...1600 {
            let stage = controller.stage
            visited.insert(String(describing: stage))
            if controller.waitingForVoice { controller.guidanceDidFinish() }
            let t = Double(i)/100
            let jitter = 0.01*sin(2 * .pi*t)
            let rotation = stage == .left ? Vector3(x: -0.3+jitter) : stage == .forward ? Vector3(y: 0.3+jitter) : Vector3(x: jitter)
            controller.ingest(MotionSample(sensorTimestamp: t, attitudeQuaternion: OrientationMath.quaternion(rotationVector: rotation), rotationRate: Vector3(x: 0.07)))
            if controller.stage == .awaitingConvention { break }
        }
        XCTAssertEqual(controller.stage, .awaitingConvention)
        XCTAssertTrue(visited.contains("returnToNeutral")); XCTAssertTrue(visited.contains("returnToFinish"))
        let summary = CaptureSummary(capabilities: [:], acceptedCounts: [:], ended: ClockAnchor(), clockMapping: ClockMapping(), rotationConvention: RotationConvention(capturedToCanonical: "identity", verified: true))
        XCTAssertTrue(controller.finish(summary: summary))
        XCTAssertTrue(controller.canRecord)
        let calibration = try XCTUnwrap(controller.currentCalibration)
        XCTAssertLessThan(calibration.actualDurationSeconds, 2.52)
        XCTAssertGreaterThanOrEqual(calibration.actualDurationSeconds, 2.5)
        XCTAssertEqual(calibration.configuration.version, "2")
    }
    @MainActor func testGuideTimesOutWeakGestureAndCancelStopsProgress() {
        let controller = CalibrationController()
        controller.begin(recording: false, handsFree: true)
        for i in 0...10000 {
            if controller.waitingForVoice { controller.guidanceDidFinish() }
            controller.ingest(MotionSample(sensorTimestamp: Double(i)/100))
        }
        XCTAssertEqual(controller.stage, .retry)
        XCTAssertEqual(controller.error, .timedOut)
        XCTAssertTrue(controller.prompt.contains("timed out"))
        XCTAssertEqual(controller.automaticRetries, 2)
        controller.cancel()
        controller.ingest(MotionSample(sensorTimestamp: 100))
        XCTAssertEqual(controller.stage, .idle)
        XCTAssertFalse(controller.canRecord)
    }
    @MainActor func testInitialBumpExpiresFromNeutralWindowWithoutRestart() {
        let controller = CalibrationController()
        XCTAssertTrue(controller.begin(recording: false))
        for i in 0...350 {
            controller.ingest(MotionSample(sensorTimestamp: Double(i)/100, rotationRate: Vector3(x: i < 20 ? 0.4 : 0)))
        }
        XCTAssertTrue(controller.advance(), "A later clean 2.5-second interval must replace the initial tap movement")
        XCTAssertEqual(controller.stage, .left)
    }
    @MainActor func testMovingNeutralOffersExplicitRetryAndRetainsPriorCalibration() throws {
        let neutral = (0...250).map { MotionSample(sensorTimestamp: Double($0) / 100) }
        let left = (0...50).map { MotionSample(sensorTimestamp: Double($0) / 100, attitudeQuaternion: OrientationMath.quaternion(rotationVector: Vector3(x: -0.3))) }
        let forward = (0...50).map { MotionSample(sensorTimestamp: Double($0) / 100, attitudeQuaternion: OrientationMath.quaternion(rotationVector: Vector3(y: 0.3))) }
        let convention = RotationConvention(capturedToCanonical: "identity", verified: true)
        let old = try CalibrationSolver.fit(neutral: neutral, left: left, forward: forward, convention: convention)
        let controller = CalibrationController(calibration: old)
        XCTAssertTrue(controller.begin(recording: false))
        for index in 0...250 {
            controller.ingest(MotionSample(sensorTimestamp: Double(index) / 100, rotationRate: Vector3(x: index == 0 ? 0.2 : 0)))
        }
        XCTAssertTrue(controller.canAdvance, "A completed interval must offer Continue even when quality validation will reject it")
        XCTAssertFalse(controller.advance())
        XCTAssertEqual(controller.error, .movingNeutral)
        XCTAssertEqual(controller.stage, .retry)
        XCTAssertTrue(controller.prompt.contains("Retry"))
        XCTAssertEqual(controller.currentCalibration, old)
        for index in 251...500 { controller.ingest(MotionSample(sensorTimestamp: Double(index) / 100)) }
        XCTAssertEqual(controller.stage, .retry, "More still samples must not silently skip the rejected interval")
        XCTAssertEqual(controller.sampleCount, 251)
        XCTAssertTrue(controller.begin(recording: false))
        XCTAssertEqual(controller.sampleCount, 0)
        for samples in [neutral, left, forward] {
            samples.forEach { controller.ingest($0) }
            XCTAssertTrue(controller.advance())
        }
        XCTAssertEqual(controller.stage, .awaitingConvention)
        let summary = CaptureSummary(capabilities: [:], acceptedCounts: [:], ended: ClockAnchor(), clockMapping: ClockMapping(), rotationConvention: convention)
        XCTAssertTrue(controller.finish(summary: summary))
        XCTAssertEqual(controller.currentCalibration?.neutralSamples, neutral)
        XCTAssertTrue(controller.canRecord)
    }

    @MainActor func testCompletedInvalidOrGappedNeutralRequiresRetryWhileShortIntervalWaits() {
        for invalidRate in [false, true] {
            let controller = CalibrationController()
            XCTAssertTrue(controller.begin(recording: false))
            for index in 0...250 where invalidRate || !(100...120).contains(index) {
                controller.ingest(MotionSample(sensorTimestamp: Double(index) / 100, rotationRate: Vector3(x: invalidRate && index == 100 ? .nan : 0)))
            }
            XCTAssertTrue(controller.canAdvance)
            XCTAssertFalse(controller.advance())
            XCTAssertEqual(controller.stage, .retry)
            XCTAssertEqual(controller.error, invalidRate ? .invalidInput : .tooShort)
        }
        let controller = CalibrationController()
        XCTAssertTrue(controller.begin(recording: false))
        controller.ingest(MotionSample(sensorTimestamp: 0))
        controller.ingest(MotionSample(sensorTimestamp: 0.01))
        XCTAssertFalse(controller.canAdvance)
        XCTAssertFalse(controller.advance())
        XCTAssertEqual(controller.stage, .neutral)
        XCTAssertEqual(controller.error, .tooShort)
    }

    @MainActor func testRawNeutralThenVerifiedSnapshotAndFailedReplacementRetainsCalibration() throws {
        let controller = CalibrationController()
        let summary = CaptureSummary(capabilities: [.motion: .available], acceptedCounts: [:], ended: ClockAnchor(), clockMapping: ClockMapping(), rotationConvention: RotationConvention(capturedToCanonical: "identity", verified: true))
        func feed(_ rotation: Vector3, duration: Double) {
            for index in 0...250 { controller.ingest(.motion(MotionSample(sensorTimestamp: Double(index)*duration/250, attitudeQuaternion: OrientationMath.quaternion(rotationVector: rotation)))) }
        }
        XCTAssertFalse(controller.canRecord)
        XCTAssertTrue(controller.begin(recording: false))
        feed(Vector3(), duration: 1)
        XCTAssertFalse(controller.advance())
        controller.cancel()
        XCTAssertTrue(controller.begin(recording: false))
        feed(Vector3(), duration: 2.5)
        XCTAssertTrue(controller.advance())
        feed(Vector3(x: -0.3), duration: 0.5)
        XCTAssertTrue(controller.advance())
        feed(Vector3(y: 0.3), duration: 0.5)
        XCTAssertTrue(controller.advance())
        XCTAssertTrue(controller.finish(summary: summary))
        XCTAssertTrue(controller.canRecord)
        let old = try XCTUnwrap(controller.currentCalibration)
        XCTAssertFalse(controller.begin(recording: true))
        XCTAssertEqual(controller.currentCalibration, old)
        XCTAssertEqual(controller.stage, .ready)
        XCTAssertTrue(controller.begin(recording: false))
        feed(Vector3(), duration: 2.5); XCTAssertTrue(controller.advance())
        feed(Vector3(x: -0.3), duration: 0.5); XCTAssertTrue(controller.advance())
        feed(Vector3(x: -0.3), duration: 0.5); XCTAssertTrue(controller.advance())
        XCTAssertFalse(controller.finish(summary: summary))
        XCTAssertEqual(controller.stage, .retry)
        XCTAssertEqual(controller.currentCalibration, old)
        controller.cancel()
        XCTAssertTrue(controller.canRecord)
    }
    @MainActor func testStandardMountSelectionChecksSavedSigns() {
        let controller = CalibrationController()
        XCTAssertTrue(controller.begin(recording: false, method: .standardMount))
        XCTAssertEqual(controller.setupMethod, .standardMount)
        for (q, duration) in [(Quaternion(), 2.5), (OrientationMath.quaternion(rotationVector: Vector3(z: -0.3)), 0.5), (OrientationMath.quaternion(rotationVector: Vector3(x: 0.3)), 0.5)] {
            for index in 0...250 { controller.ingest(MotionSample(sensorTimestamp: Double(index)*duration/250, attitudeQuaternion: q)) }
            XCTAssertTrue(controller.advance())
        }
        let summary = CaptureSummary(capabilities: [:], acceptedCounts: [:], ended: ClockAnchor(), clockMapping: ClockMapping(), rotationConvention: RotationConvention(capturedToCanonical: "identity", verified: true))
        XCTAssertTrue(controller.finish(summary: summary))
        XCTAssertEqual(controller.currentCalibration?.deviceToRiderBasis.m21, 1)
        XCTAssertEqual(controller.currentCalibration?.leftGestureSamples.count, 251)
        XCTAssertTrue(controller.canRecord)
    }
    @MainActor func testNeutralCanRemainUnverifiedUntilGesturesButUnknownSnapshotRejects() {
        let controller = CalibrationController()
        XCTAssertTrue(controller.begin(recording: false))
        for (q, duration) in [(Quaternion(), 2.5), (OrientationMath.quaternion(rotationVector: Vector3(x: -0.3)), 0.5), (OrientationMath.quaternion(rotationVector: Vector3(y: 0.3)), 0.5)] {
            for index in 0...250 { controller.ingest(MotionSample(sensorTimestamp: Double(index)*duration/250, attitudeQuaternion: q)) }
            XCTAssertTrue(controller.advance())
        }
        let summary = CaptureSummary(capabilities: [:], acceptedCounts: [:], ended: ClockAnchor(), clockMapping: ClockMapping(), rotationConvention: RotationConvention())
        XCTAssertFalse(controller.finish(summary: summary))
        XCTAssertEqual(controller.error, .unverifiedConvention)
        XCTAssertFalse(controller.canRecord)
    }
}
