import XCTest
@testable import RideFormCore

final class SessionIOTests: XCTestCase {
    func testDiskRoundTripDrainsAndPreservesCapabilities() throws {
        let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        let manifest = SessionManifest(streams: [StreamMetadata(), StreamMetadata(kind: .accelerometer, capability: .unavailable(reason: "unsupported")), StreamMetadata(kind: .gyroscope)])
        let writer = try SessionWriter(directory: root, manifest: manifest, batchSize: 100)
        let samples = [MotionSample(sequence: 0, sensorTimestamp: 1.1234567890123457), MotionSample(sequence: 1, sensorTimestamp: 4.987654321098765)]
        samples.forEach(writer.enqueue)
        var completed = manifest; completed.boundaries.status = "completed"
        let done = expectation(description: "saved")
        writer.finalize(manifest: completed) { result in if case .failure(let error) = result { XCTFail(String(describing: error)) }; done.fulfill() }
        wait(for: [done], timeout: 5)
        let loaded = try SessionIO.load(root)
        XCTAssertEqual(loaded.motion.samples, samples)
        XCTAssertEqual(loaded.manifest.streams[0].writtenCount, 2)
        XCTAssertEqual(loaded.accelerometer.capability, .unavailable(reason: "unsupported"))
        XCTAssertEqual(loaded.gyroscope.capability, .available)
    }
}

extension SessionIOTests {
    private func finish(_ writer: SessionWriter, manifest: SessionManifest, expectSuccess: Bool = true) {
        let done = expectation(description: "writer")
        writer.finalize(manifest: manifest) { result in
            switch result {
            case .success: XCTAssertTrue(expectSuccess)
            case .failure: XCTAssertFalse(expectSuccess)
            }
            done.fulfill()
        }
        wait(for: [done], timeout: 5)
    }
    func testRealDiskFailureRetryAndImmutableMaster() throws {
        let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        var manifest = SessionManifest(); manifest.boundaries.status = "completed"
        let writer = try SessionWriter(directory: root, manifest: manifest)
        // A real conflicting directory prevents opening the optional raw stream file.
        let conflict = root.appendingPathComponent("accelerometer.jsonl")
        try FileManager.default.createDirectory(at: conflict, withIntermediateDirectories: false)
        writer.enqueue(RawAccelerometerSample(sequence: 0, sensorTimestamp: 2))
        writer.enqueue(MotionSample(sequence: 0, sensorTimestamp: 1))
        finish(writer, manifest: manifest, expectSuccess: false)
        XCTAssertEqual(try SessionCodec.decodeManifest(Data(contentsOf: root.appendingPathComponent("manifest.json"))).boundaries.status, "incomplete")
        try FileManager.default.moveItem(at: conflict, to: root.appendingPathComponent("conflict-retained"))
        writer.enqueue(RawAccelerometerSample(sequence: 1, sensorTimestamp: 3))
        finish(writer, manifest: manifest)
        let loaded = try SessionIO.load(root)
        XCTAssertEqual(loaded.accelerometer.samples.count, 2)
        XCTAssertEqual(loaded.manifest.streams.first { $0.kind == .accelerometer }?.statistics,
                       .calculate(timestamps: [2, 3], receiptUptimes: [0, 0]))
        XCTAssertEqual(loaded.manifest.streams.first { $0.kind == .accelerometer }?.acceptedCount, 2)
        let bytes = try Data(contentsOf: root.appendingPathComponent("manifest.json"))
        writer.enqueue(MotionSample(sequence: 1, sensorTimestamp: 8))
        finish(writer, manifest: manifest, expectSuccess: false)
        XCTAssertEqual(try Data(contentsOf: root.appendingPathComponent("manifest.json")), bytes)
        XCTAssertThrowsError(try SessionWriter(directory: root, manifest: manifest))
    }
    func testTruncatedFinalRowAndNonfiniteLoss() throws {
        let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        var manifest = SessionManifest(); manifest.boundaries.status = "completed"
        let writer = try SessionWriter(directory: root, manifest: manifest)
        writer.enqueue(MotionSample(sequence: 0, sensorTimestamp: 1))
        writer.enqueue(MotionSample(sequence: 1, sensorTimestamp: .nan))
        finish(writer, manifest: manifest)
        let before = try SessionIO.load(root)
        XCTAssertEqual(before.manifest.streams[0].acceptedCount, 2)
        XCTAssertEqual(before.manifest.streams[0].writtenCount, 1)
        XCTAssertEqual(before.manifest.streams[0].lossCount, 1)
        XCTAssertEqual(before.manifest.streams[0].statistics, .calculate(timestamps: [1], receiptUptimes: [0]))
        XCTAssertEqual(before.manifest.diagnostics.first?.code, "nonfinite_value")
        let file = root.appendingPathComponent("motion.jsonl")
        let handle = try FileHandle(forWritingTo: file); try handle.seekToEnd(); try handle.write(contentsOf: Data("{\"sequence\":".utf8)); try handle.close()
        let recovered = try SessionIO.load(root)
        XCTAssertEqual(recovered.motion.samples.count, 1)
        XCTAssertEqual(recovered.manifest.boundaries.status, "incomplete")
        XCTAssertTrue(recovered.manifest.diagnostics.contains { $0.code == "truncated_final_row" })
    }
    func testDerivedConfigurationAndInputIdentity() throws {
        let session = MotionSession()
        let sidecar = try DerivedSidecar(session: session, configuration: session.manifest.analysisConfiguration, payload: [1.0])
        XCTAssertTrue(sidecar.isCurrent(for: session, configuration: session.manifest.analysisConfiguration))
        var changed = session; changed.motion.samples.append(MotionSample(sensorTimestamp: 8))
        XCTAssertFalse(sidecar.isCurrent(for: changed, configuration: session.manifest.analysisConfiguration))
        var config = session.manifest.analysisConfiguration; config.algorithmVersion = "different"
        XCTAssertFalse(sidecar.isCurrent(for: session, configuration: config))
    }
}


extension SessionIOTests {
    func testMissingEventsAndSensorTailAreIncompleteAndCountsReconcile() throws {
        let source = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        var manifest = SessionManifest(); manifest.boundaries.status = "completed"
        let writer = try SessionWriter(directory: source, manifest: manifest)
        writer.enqueue(MotionSample(sequence: 0, sensorTimestamp: 1))
        writer.enqueue(MotionSample(sequence: 1, sensorTimestamp: 2))
        writer.enqueue(EventMarker(hostUptime: 1))
        finish(writer, manifest: manifest)
        let damaged = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        try FileManager.default.createDirectory(at: damaged, withIntermediateDirectories: true)
        try Data(contentsOf: source.appendingPathComponent("manifest.json")).write(to: damaged.appendingPathComponent("manifest.json"))
        var tail = try SessionCodec.encode(MotionSample(sequence: 0, sensorTimestamp: 1)); tail.append(10)
        tail.append(contentsOf: "{truncated".utf8)
        try tail.write(to: damaged.appendingPathComponent("motion.jsonl"))
        let recovered = try SessionIO.load(damaged)
        XCTAssertEqual(recovered.manifest.boundaries.status, "incomplete")
        XCTAssertEqual(recovered.manifest.eventCount, 0)
        XCTAssertEqual(recovered.manifest.streams[0].acceptedCount, 2)
        XCTAssertEqual(recovered.manifest.streams[0].writtenCount, 1)
        XCTAssertEqual(recovered.manifest.streams[0].lossCount, 1)
        XCTAssertTrue(recovered.manifest.diagnostics.contains { $0.code == "event_count_mismatch" })
    }
}

extension SessionIOTests {
    func testTenMinuteRecordingFinalizesFromWrittenSampleTiming() throws {
        let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        var manifest = SessionManifest()
        manifest.boundaries.status = "completed"
        let writer = try SessionWriter(directory: root, manifest: manifest, batchSize: 4096)
        let count = 60_000
        for index in 0..<count {
            writer.enqueue(MotionSample(sequence: UInt64(index), sensorTimestamp: Double(index) / 100,
                                        receiptUptime: Double(index) / 100 + 0.005))
        }
        let done = expectation(description: "ten-minute recording saved")
        writer.finalize(manifest: manifest) { result in
            if case .failure(let error) = result { XCTFail(String(describing: error)) }
            done.fulfill()
        }
        wait(for: [done], timeout: 20)
        let saved = try SessionCodec.decodeManifest(Data(contentsOf: root.appendingPathComponent("manifest.json")))
        XCTAssertEqual(saved.boundaries.status, "completed")
        let stream = try XCTUnwrap(saved.streams.first { $0.kind == .motion })
        XCTAssertEqual(stream.writtenCount, count)
        XCTAssertEqual(stream.acceptedCount, count)
        XCTAssertEqual(stream.lossCount, 0)
        XCTAssertEqual(stream.statistics.count, count)
        XCTAssertEqual(try XCTUnwrap(stream.statistics.meanHz), 100, accuracy: 1e-8)
        XCTAssertEqual(stream.statistics.gapCount, 0)
    }
}


extension SessionIOTests {
    func testFinalStatisticsMatchWrittenStreamsWithValidatedClock() throws {
        let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        var manifest = SessionManifest()
        manifest.clockMapping = ClockMapping(sensorToHostOffset: 4, validated: true)
        manifest.boundaries.status = "completed"
        let writer = try SessionWriter(directory: root, manifest: manifest, batchSize: 2)
        for (index, time) in [1.0, 1.01, 1.08, 1.07].enumerated() {
            writer.enqueue(MotionSample(sequence: UInt64(index), sensorTimestamp: time, receiptUptime: time + 4.02))
            writer.enqueue(RawAccelerometerSample(sequence: UInt64(index), sensorTimestamp: time, receiptUptime: time + 4.03))
            writer.enqueue(RawGyroscopeSample(sequence: UInt64(index), sensorTimestamp: time, receiptUptime: time + 3.99))
        }
        finish(writer, manifest: manifest)
        let loaded = try SessionIO.load(root)
        let streams = [
            (StreamKind.motion, loaded.motion.samples.map(\.sensorTimestamp), loaded.motion.samples.map(\.receiptUptime)),
            (StreamKind.accelerometer, loaded.accelerometer.samples.map(\.sensorTimestamp), loaded.accelerometer.samples.map(\.receiptUptime)),
            (StreamKind.gyroscope, loaded.gyroscope.samples.map(\.sensorTimestamp), loaded.gyroscope.samples.map(\.receiptUptime))
        ]
        for (kind, times, receipts) in streams {
            XCTAssertEqual(loaded.manifest.streams.first { $0.kind == kind }?.statistics,
                           .calculate(timestamps: times, receiptUptimes: receipts, clockMapping: manifest.clockMapping))
        }
    }
}
