import Foundation
import XCTest
@testable import RideFormCore

final class SessionSchemaTests: XCTestCase {
    func testDifficultDoublesAndIndependentStreamsRoundTripExactly() throws {
        var session = MotionSession()
        let time = Double.pi.nextUp
        session.motion.samples = [MotionSample(sensorTimestamp: time, receiptUptime: time.nextUp,
                                              attitudeQuaternion: Quaternion(x: 0.12345678901234567, y: 0, z: 0, w: 1))]
        session.accelerometer.samples = [RawAccelerometerSample(sensorTimestamp: time.nextDown, receiptUptime: 14.2, acceleration: Vector3(x: Double.leastNonzeroMagnitude, y: -Double.pi, z: 1))]
        session.gyroscope.samples = [RawGyroscopeSample(sensorTimestamp: 19.987654321098765, receiptUptime: 20, rotationRate: Vector3(x: 0, y: 0, z: Double.greatestFiniteMagnitude))]
        let artifact = try SessionCodec.encode(session)
        let file = FileManager.default.temporaryDirectory.appendingPathComponent("rideform-schema-\(UUID()).json")
        try artifact.write(to: file)
        defer { try? FileManager.default.removeItem(at: file) }
        let reopened = try SessionCodec.decode(Data(contentsOf: file))
        XCTAssertEqual(reopened, session)
        XCTAssertEqual(reopened.motion.samples[0].sensorTimestamp.bitPattern, time.bitPattern)
        XCTAssertEqual(reopened.accelerometer.samples[0].acceleration.x.bitPattern, Double.leastNonzeroMagnitude.bitPattern)
        XCTAssertNotEqual(reopened.motion.samples[0].sensorTimestamp, reopened.accelerometer.samples[0].sensorTimestamp)
    }

    func testIPhoneAndWatchHaveIdenticalAnalysisInputs() throws {
        var phone = MotionSession()
        phone.motion.samples = [MotionSample(sensorTimestamp: 1, receiptUptime: 2)]
        var watch = phone
        watch.manifest.platform = .appleWatch
        watch.manifest.deviceModel = "synthetic Watch"
        let reopened = try SessionCodec.decode(SessionCodec.encode(watch))
        XCTAssertEqual(reopened.motion, phone.motion)
        XCTAssertEqual(reopened.calibration, phone.calibration)
        XCTAssertEqual(reopened.manifest.analysisConfiguration, phone.manifest.analysisConfiguration)
        XCTAssertNotEqual(reopened.manifest.platform, phone.manifest.platform)
    }

    func testAvailableEmptyStreamDiffersFromUnsupported() throws {
        var session = MotionSession()
        session.accelerometer.capability = .available
        session.gyroscope.capability = .unavailable(reason: "unsupported on device")
        let reopened = try SessionCodec.decode(SessionCodec.encode(session))
        XCTAssertEqual(reopened.accelerometer.capability, .available)
        XCTAssertEqual(reopened.gyroscope.capability, .unavailable(reason: "unsupported on device"))
        XCTAssertTrue(reopened.accelerometer.samples.isEmpty)
        XCTAssertTrue(reopened.gyroscope.samples.isEmpty)
    }

    func testUnknownSchemaVersionRejectsWithCompatibilityError() throws {
        var session = MotionSession()
        session.manifest.schemaVersion = 999
        let data = try JSONEncoder().encode(session)
        XCTAssertThrowsError(try SessionCodec.decode(data)) { error in
            XCTAssertEqual(error as? SessionSchemaError, .unsupportedVersion(999))
        }
    }

    func testNonfiniteValueProducesExplicitDiagnosticWithoutSubstitution() throws {
        var session = MotionSession()
        session.motion.samples = [MotionSample(sensorTimestamp: .nan, receiptUptime: 1)]
        XCTAssertThrowsError(try SessionCodec.encode(session)) { error in
            guard case let SessionSchemaError.nonFinite(diagnostic) = error else {
                return XCTFail("Expected a nonfinite diagnostic, got \(error)")
            }
            XCTAssertEqual(diagnostic.code, "nonfinite_value")
            XCTAssertTrue(diagnostic.fieldPath.contains("sensorTimestamp"))
            XCTAssertFalse(diagnostic.message.isEmpty)
            do { _ = try JSONEncoder().encode(diagnostic) }
            catch { XCTFail("Diagnostic must remain serializable: \(error)") }
        }
        XCTAssertTrue(session.motion.samples[0].sensorTimestamp.isNaN)
    }
}
