HealthProbe / HealthProbeTests / SQLiteHealthArchiveStoreTests.swift
1 contributor
734 lines | 34.999kb
import HealthKit
import SQLite3
import XCTest
@testable import HealthProbe

final class SQLiteHealthArchiveStoreTests: XCTestCase {
    private final class AsyncResultBox<T>: @unchecked Sendable {
        var result: Result<T, Error>?
    }

    private var temporaryDirectory: URL!

    override func setUpWithError() throws {
        temporaryDirectory = FileManager.default.temporaryDirectory
            .appending(path: "HealthProbeTests-\(UUID().uuidString)", directoryHint: .isDirectory)
        try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true)
    }

    override func tearDownWithError() throws {
        if let temporaryDirectory {
            try? FileManager.default.removeItem(at: temporaryDirectory)
        }
        temporaryDirectory = nil
    }

    func testFreshArchiveInitializesSchemaAndPassesIntegrity() async throws {
        let store = SQLiteHealthArchiveStore(databaseURL: databaseURL())

        let report = try await store.checkIntegrity()
        let records = try await store.records(for: HealthArchiveRecordRequest(
            sampleTypeIdentifier: HKQuantityTypeIdentifier.stepCount.rawValue
        ))

        XCTAssertTrue(report.passed)
        XCTAssertEqual(report.schemaVersion, 2)
        XCTAssertEqual(report.sqliteIntegrityStatus, "ok")
        XCTAssertEqual(report.foreignKeyIssueCount, 0)
        XCTAssertTrue(report.missingTableNames.isEmpty)
        XCTAssertTrue(report.requiredTableNames.contains("sample_visibility_ranges"))
        XCTAssertTrue(report.requiredTableNames.contains("daily_type_aggregates"))
        XCTAssertTrue(records.isEmpty)
    }

    func testPrototypeArchiveIsResetAndReinitializedAsV2() async throws {
        let url = databaseURL()
        try createPrototypeDatabase(at: url)
        let store = SQLiteHealthArchiveStore(databaseURL: url)

        let report = try await store.checkIntegrity()

        XCTAssertTrue(report.passed)
        XCTAssertEqual(report.schemaVersion, 2)
        XCTAssertTrue(report.missingTableNames.isEmpty)
    }

    func testRepeatedSamplePageDoesNotDuplicateIdentityOrVersion() async throws {
        let url = databaseURL()
        let store = SQLiteHealthArchiveStore(databaseURL: url)
        let sample = makeStepCountSample()

        let firstWrite = try await store.upsertSamples([sample], observedAt: Date(timeIntervalSince1970: 2_000))
        let secondWrite = try await store.upsertSamples([sample], observedAt: Date(timeIntervalSince1970: 2_060))
        let records = try await store.records(for: HealthArchiveRecordRequest(
            sampleTypeIdentifier: sample.sampleType.identifier
        ))
        let report = try await store.checkIntegrity()
        let versionDebugRows = try sampleVersionDebugRows(at: url)
        let visibilityDebugRows = try visibilityRangeDebugRows(at: url)

        XCTAssertEqual(firstWrite.insertedCount, 1)
        XCTAssertEqual(firstWrite.updatedCount, 0)
        XCTAssertEqual(firstWrite.unchangedCount, 0)
        XCTAssertEqual(try countRows(in: "samples", at: url), 1)
        XCTAssertEqual(try countRows(in: "sample_versions", at: url), 1, versionDebugRows)
        XCTAssertEqual(try countRows(in: "sample_observation_events", at: url), 1)
        XCTAssertEqual(try countRows(in: "sample_observation_events WHERE event_kind = 'appeared'", at: url), 1)
        XCTAssertEqual(try countRows(in: "sample_observation_events WHERE event_kind = 'verified'", at: url), 0)
        XCTAssertEqual(try countRows(in: "sample_visibility_ranges", at: url), 1, visibilityDebugRows)
        XCTAssertEqual(try countRows(in: "source_revisions", at: url), 1)
        XCTAssertFalse(try tableExists("archive_samples", at: url))
        XCTAssertEqual(secondWrite.insertedCount, 0)
        XCTAssertEqual(secondWrite.updatedCount, 0)
        XCTAssertEqual(secondWrite.unchangedCount, 1)
        XCTAssertEqual(records.count, 1)
        XCTAssertEqual(records.first?.displayValue, "42.0 count")
        XCTAssertTrue(report.passed)
    }

    func testVerificationUsesArchiveV2TablesWithoutLegacyMirror() async throws {
        let url = databaseURL()
        let store = SQLiteHealthArchiveStore(databaseURL: url)
        let sample = makeStepCountSample()

        _ = try await store.upsertSamples([sample], observedAt: Date(timeIntervalSince1970: 2_000))
        try await store.markVerification(sampleType: sample.sampleType, verifiedAt: Date(timeIntervalSince1970: 2_060))
        let observationIDs = try observationIDs(at: url)

        XCTAssertEqual(observationIDs.count, 2)
        XCTAssertEqual(try countRows(in: "observation_type_runs", at: url), 2)
        XCTAssertEqual(try countRows(in: "observation_type_runs WHERE observation_id = \(observationIDs[0]) AND inserted_event_count = 1", at: url), 1)
        XCTAssertEqual(try countRows(in: "observation_type_summaries WHERE observation_id = \(observationIDs[1]) AND visible_record_count = 1", at: url), 1)
        XCTAssertFalse(try tableExists("archive_samples", at: url))
    }

    func testDiffSummaryAndRecordsBetweenObservationsUseSQLVisibility() async throws {
        let url = databaseURL()
        let store = SQLiteHealthArchiveStore(databaseURL: url)
        let firstSample = makeStepCountSample(value: 42, start: 1_000)
        let secondSample = makeStepCountSample(value: 7, start: 2_000)
        let typeIdentifier = HKQuantityTypeIdentifier.stepCount.rawValue

        _ = try await store.upsertSamples([firstSample], observedAt: Date(timeIntervalSince1970: 3_000))
        _ = try await store.upsertSamples([firstSample, secondSample], observedAt: Date(timeIntervalSince1970: 3_060))
        try await store.recordDisappearance(
            sampleUUIDHash: HashService.sampleUUIDHash(firstSample.uuid.uuidString),
            sampleTypeIdentifier: typeIdentifier,
            observedMissingAt: Date(timeIntervalSince1970: 3_120)
        )
        let observationIDs = try observationIDs(at: url)
        XCTAssertEqual(observationIDs.count, 3)

        let appearedSummary = try await store.diffSummary(HealthArchiveDiffRequest(
            fromObservationID: observationIDs[0],
            toObservationID: observationIDs[1],
            sampleTypeIdentifier: typeIdentifier
        ))
        let appearedRecords = try await store.diffRecords(HealthArchiveDiffRecordRequest(
            fromObservationID: observationIDs[0],
            toObservationID: observationIDs[1],
            sampleTypeIdentifier: typeIdentifier,
            kind: .appeared,
            limit: 10
        ))
        let disappearedSummary = try await store.diffSummary(HealthArchiveDiffRequest(
            fromObservationID: observationIDs[1],
            toObservationID: observationIDs[2],
            sampleTypeIdentifier: typeIdentifier
        ))
        let disappearedRecords = try await store.diffRecords(HealthArchiveDiffRecordRequest(
            fromObservationID: observationIDs[1],
            toObservationID: observationIDs[2],
            sampleTypeIdentifier: typeIdentifier,
            kind: .disappeared,
            limit: 10
        ))

        XCTAssertEqual(appearedSummary.appearedCount, 1)
        XCTAssertEqual(appearedSummary.disappearedCount, 0)
        XCTAssertEqual(appearedSummary.representationChangedCount, 0)
        XCTAssertEqual(appearedRecords.map(\.displayValue), ["7.0 count"])
        XCTAssertEqual(disappearedSummary.appearedCount, 0)
        XCTAssertEqual(disappearedSummary.disappearedCount, 1)
        XCTAssertEqual(disappearedSummary.representationChangedCount, 0)
        XCTAssertEqual(disappearedRecords.map(\.displayValue), ["42.0 count"])
        XCTAssertNotNil(disappearedRecords.first?.disappearedAt)

        let exportRequest = HealthArchiveReportRequest(
            reportID: UUID(),
            title: "Disappeared Step Count Export",
            typeIdentifierFilter: typeIdentifier,
            diffFromObservationID: observationIDs[1],
            diffToObservationID: observationIDs[2],
            diffKind: .disappeared
        )
        let exportPreview = try await store.exportPreview(exportRequest)
        let exportURL = try await store.exportReport(exportRequest)

        XCTAssertEqual(exportPreview.estimatedRecordCount, 1)
        XCTAssertTrue(FileManager.default.fileExists(atPath: exportURL.path))
        XCTAssertEqual(try countRows(in: "export_manifests WHERE record_count = 1 AND from_observation_id = \(observationIDs[1]) AND to_observation_id = \(observationIDs[2])", at: url), 1)
    }

    func testExportPreviewAndReportUseSQLiteManifest() async throws {
        let url = databaseURL()
        let store = SQLiteHealthArchiveStore(databaseURL: url)
        let typeIdentifier = HKQuantityTypeIdentifier.stepCount.rawValue
        let samples = [
            makeStepCountSample(value: 42, start: 1_000),
            makeStepCountSample(value: 7, start: 2_000)
        ]

        _ = try await store.upsertSamples(samples, observedAt: Date(timeIntervalSince1970: 3_000))

        let request = HealthArchiveReportRequest(
            reportID: UUID(),
            title: "Step Count Export",
            typeIdentifierFilter: typeIdentifier
        )
        let preview = try await store.exportPreview(request)
        let exportURL = try await store.exportReport(request)

        XCTAssertEqual(preview.estimatedRecordCount, 2)
        XCTAssertTrue(FileManager.default.fileExists(atPath: exportURL.path))
        XCTAssertEqual(try countRows(in: "export_manifests", at: url), 1)
        XCTAssertEqual(try countRows(in: "export_manifests WHERE record_count = 2", at: url), 1)

        let cache = try CoreDataArchiveCacheStore(inMemory: true)
        let rebuild = try cache.rebuild(fromArchiveAt: url)
        XCTAssertEqual(rebuild.exportManifestRows, 1)
    }

    func testGroupedObservationKeepsPageWritesDeletesAndVerificationTogether() async throws {
        let url = databaseURL()
        let store = SQLiteHealthArchiveStore(databaseURL: url)
        let firstSample = makeStepCountSample(value: 42, start: 1_000)
        let secondSample = makeStepCountSample(value: 7, start: 2_000)
        let typeIdentifier = HKQuantityTypeIdentifier.stepCount.rawValue

        _ = try await store.upsertSamples([firstSample], observedAt: Date(timeIntervalSince1970: 3_000))
        let observationID = try await store.beginObservation(
            observedAt: Date(timeIntervalSince1970: 3_060),
            triggerReason: "manual",
            selectedTypeSetHash: "selected-types"
        )
        _ = try await store.upsertSamples(
            [secondSample],
            observedAt: Date(timeIntervalSince1970: 3_060),
            observationID: observationID
        )
        try await store.recordDisappearance(
            sampleUUIDHash: HashService.sampleUUIDHash(firstSample.uuid.uuidString),
            sampleTypeIdentifier: typeIdentifier,
            observedMissingAt: Date(timeIntervalSince1970: 3_060),
            observationID: observationID
        )
        XCTAssertEqual(
            try countRows(
                in: "observation_type_summaries WHERE observation_id = \(observationID)",
                at: url
            ),
            0
        )
        XCTAssertEqual(
            try countRows(
                in: "daily_type_aggregates WHERE observation_id = \(observationID)",
                at: url
            ),
            0
        )
        try await store.markVerification(
            sampleType: secondSample.sampleType,
            verifiedAt: Date(timeIntervalSince1970: 3_060),
            observationID: observationID
        )
        try await store.finishObservation(
            observationID: observationID,
            status: "completed",
            endedAt: Date(timeIntervalSince1970: 3_070)
        )

        let observationIDs = try observationIDs(at: url)
        let summary = try await store.diffSummary(HealthArchiveDiffRequest(
            fromObservationID: observationIDs[0],
            toObservationID: observationID,
            sampleTypeIdentifier: typeIdentifier
        ))

        XCTAssertEqual(observationIDs.count, 2)
        XCTAssertEqual(summary.appearedCount, 1)
        XCTAssertEqual(summary.disappearedCount, 1)
        XCTAssertEqual(try countRows(in: "observations WHERE id = \(observationID) AND status = 'completed' AND selected_type_set_hash = 'selected-types'", at: url), 1)
        XCTAssertEqual(try countRows(in: "sample_observation_events WHERE observation_id = \(observationID)", at: url), 2)
        XCTAssertEqual(try countRows(in: "observation_type_runs WHERE observation_id = \(observationID) AND inserted_event_count = 1 AND deleted_event_count = 1 AND verified_visible_count = 1 AND status = 'completed'", at: url), 1)
        XCTAssertEqual(try countRows(in: "observation_type_summaries WHERE observation_id = \(observationID) AND visible_record_count = 1", at: url), 1)
        XCTAssertGreaterThan(try countRows(in: "daily_type_aggregates WHERE observation_id = \(observationID)", at: url), 0)
    }

    func testGroupedObservationCanBatchDeletedObjectsInSingleRun() async throws {
        let url = databaseURL()
        let store = SQLiteHealthArchiveStore(databaseURL: url)
        let samples = [
            makeStepCountSample(value: 42, start: 1_000),
            makeStepCountSample(value: 7, start: 2_000),
            makeStepCountSample(value: 9, start: 3_000)
        ]
        let typeIdentifier = HKQuantityTypeIdentifier.stepCount.rawValue

        _ = try await store.upsertSamples(samples, observedAt: Date(timeIntervalSince1970: 4_000))
        let observationID = try await store.beginObservation(
            observedAt: Date(timeIntervalSince1970: 4_060),
            triggerReason: "manual",
            selectedTypeSetHash: "selected-types"
        )
        let deletedCount = try await store.recordDisappearances(
            sampleUUIDHashes: samples.map { HashService.sampleUUIDHash($0.uuid.uuidString) },
            sampleTypeIdentifier: typeIdentifier,
            observedMissingAt: Date(timeIntervalSince1970: 4_060),
            observationID: observationID
        )
        try await store.markVerification(
            sampleType: samples[0].sampleType,
            verifiedAt: Date(timeIntervalSince1970: 4_060),
            observationID: observationID
        )

        XCTAssertEqual(deletedCount, 3)
        XCTAssertEqual(try countRows(in: "sample_observation_events WHERE observation_id = \(observationID) AND event_kind = 'disappeared'", at: url), 3)
        XCTAssertEqual(try countRows(in: "observation_type_runs WHERE observation_id = \(observationID) AND deleted_event_count = 3 AND status = 'completed'", at: url), 1)
    }

    func testLargeSyntheticDiffReturnsCountsAndBoundedPages() async throws {
        let url = databaseURL()
        let store = SQLiteHealthArchiveStore(databaseURL: url)
        let typeIdentifier = HKQuantityTypeIdentifier.stepCount.rawValue
        let initialCount = 1_200
        let appearedCount = 180
        let disappearedCount = 160
        let pageSize = 25
        let initialSamples = makeStepCountSamples(count: initialCount, startIndex: 0)
        let appearedSamples = makeStepCountSamples(count: appearedCount, startIndex: initialCount)

        _ = try await store.upsertSamples(initialSamples, observedAt: Date(timeIntervalSince1970: 1_000_000))
        _ = try await store.upsertSamples(initialSamples + appearedSamples, observedAt: Date(timeIntervalSince1970: 1_000_060))
        for (offset, sample) in initialSamples.prefix(disappearedCount).enumerated() {
            try await store.recordDisappearance(
                sampleUUIDHash: HashService.sampleUUIDHash(sample.uuid.uuidString),
                sampleTypeIdentifier: typeIdentifier,
                observedMissingAt: Date(timeIntervalSince1970: 1_000_120 + Double(offset))
            )
        }

        let observationIDs = try observationIDs(at: url)
        let firstObservationID = try XCTUnwrap(observationIDs.first)
        let lastObservationID = try XCTUnwrap(observationIDs.last)
        let queryStartedAt = Date()
        let summary = try await store.diffSummary(HealthArchiveDiffRequest(
            fromObservationID: firstObservationID,
            toObservationID: lastObservationID,
            sampleTypeIdentifier: typeIdentifier
        ))
        let firstPage = try await store.diffRecords(HealthArchiveDiffRecordRequest(
            fromObservationID: firstObservationID,
            toObservationID: lastObservationID,
            sampleTypeIdentifier: typeIdentifier,
            kind: .appeared,
            limit: pageSize
        ))
        let firstPageLastRecord = try XCTUnwrap(firstPage.last)
        let secondPage = try await store.diffRecords(HealthArchiveDiffRecordRequest(
            fromObservationID: firstObservationID,
            toObservationID: lastObservationID,
            sampleTypeIdentifier: typeIdentifier,
            kind: .appeared,
            afterCursor: RecordCursor(
                startDate: firstPageLastRecord.startDate,
                strictFingerprint: firstPageLastRecord.strictFingerprint
            ),
            limit: pageSize
        ))
        let queryElapsedSeconds = Date().timeIntervalSince(queryStartedAt)

        XCTAssertEqual(summary.appearedCount, appearedCount)
        XCTAssertEqual(summary.disappearedCount, disappearedCount)
        XCTAssertEqual(summary.representationChangedCount, 0)
        XCTAssertEqual(firstPage.count, pageSize)
        XCTAssertEqual(secondPage.count, pageSize)
        XCTAssertTrue(Set(firstPage.map(\.id)).isDisjoint(with: Set(secondPage.map(\.id))))
        XCTAssertLessThan(queryElapsedSeconds, 10)
    }

    func testLargeSyntheticDiffQueryMetrics() throws {
        let url = databaseURL()
        let store = SQLiteHealthArchiveStore(databaseURL: url)
        let typeIdentifier = HKQuantityTypeIdentifier.stepCount.rawValue
        let initialCount = 900
        let appearedCount = 120
        let disappearedCount = 100
        let pageSize = 25
        let initialSamples = makeStepCountSamples(count: initialCount, startIndex: 0)
        let appearedSamples = makeStepCountSamples(count: appearedCount, startIndex: initialCount)

        try waitForArchiveOperation {
            _ = try await store.upsertSamples(initialSamples, observedAt: Date(timeIntervalSince1970: 1_200_000))
            _ = try await store.upsertSamples(initialSamples + appearedSamples, observedAt: Date(timeIntervalSince1970: 1_200_060))
            for (offset, sample) in initialSamples.prefix(disappearedCount).enumerated() {
                try await store.recordDisappearance(
                    sampleUUIDHash: HashService.sampleUUIDHash(sample.uuid.uuidString),
                    sampleTypeIdentifier: typeIdentifier,
                    observedMissingAt: Date(timeIntervalSince1970: 1_200_120 + Double(offset))
                )
            }
        }

        let observationIDs = try observationIDs(at: url)
        let firstObservationID = try XCTUnwrap(observationIDs.first)
        let lastObservationID = try XCTUnwrap(observationIDs.last)
        let options = XCTMeasureOptions()
        options.iterationCount = 3

        measure(metrics: [XCTClockMetric(), XCTMemoryMetric()], options: options) {
            do {
                let result = try waitForArchiveOperation {
                    let summary = try await store.diffSummary(HealthArchiveDiffRequest(
                        fromObservationID: firstObservationID,
                        toObservationID: lastObservationID,
                        sampleTypeIdentifier: typeIdentifier
                    ))
                    let records = try await store.diffRecords(HealthArchiveDiffRecordRequest(
                        fromObservationID: firstObservationID,
                        toObservationID: lastObservationID,
                        sampleTypeIdentifier: typeIdentifier,
                        kind: .appeared,
                        limit: pageSize
                    ))
                    return (summary, records)
                }
                XCTAssertEqual(result.0.appearedCount, appearedCount)
                XCTAssertEqual(result.0.disappearedCount, disappearedCount)
                XCTAssertEqual(result.1.count, pageSize)
            } catch {
                XCTFail("Measured archive query failed: \(error)")
            }
        }
    }

    func testAggregateComparisonUsesMaterializedDailyAggregates() async throws {
        let url = databaseURL()
        let store = SQLiteHealthArchiveStore(databaseURL: url)
        let firstSample = makeStepCountSample(value: 42, start: 1_000)
        let secondSample = makeStepCountSample(value: 7, start: 2_000)
        let typeIdentifier = HKQuantityTypeIdentifier.stepCount.rawValue

        _ = try await store.upsertSamples([firstSample], observedAt: Date(timeIntervalSince1970: 3_000))
        _ = try await store.upsertSamples([firstSample, secondSample], observedAt: Date(timeIntervalSince1970: 3_060))
        let observationIDs = try observationIDs(at: url)
        XCTAssertEqual(observationIDs.count, 2)

        let rows = try await store.aggregateComparison(HealthArchiveAggregateComparisonRequest(
            fromObservationID: observationIDs[0],
            toObservationID: observationIDs[1],
            sampleTypeIdentifier: typeIdentifier,
            limit: 10
        ))

        XCTAssertEqual(rows.count, 1)
        XCTAssertEqual(rows.first?.sampleTypeIdentifier, typeIdentifier)
        XCTAssertEqual(rows.first?.fromVisibleRecordCount, 1)
        XCTAssertEqual(rows.first?.toVisibleRecordCount, 2)
        XCTAssertEqual(rows.first?.visibleRecordDelta, 1)
        XCTAssertEqual(rows.first?.fromValueSum, 42)
        XCTAssertEqual(rows.first?.toValueSum, 49)
        XCTAssertEqual(rows.first?.valueSumDelta, 7)
    }

    func testSourceProvenanceBreakdownReturnsVisibleSourceRows() async throws {
        let url = databaseURL()
        let store = SQLiteHealthArchiveStore(databaseURL: url)
        let sample = makeStepCountSample(value: 42, start: 1_000)
        let typeIdentifier = sample.sampleType.identifier

        _ = try await store.upsertSamples([sample], observedAt: Date(timeIntervalSince1970: 3_000))

        let rows = try await store.sourceProvenanceBreakdown(HealthArchiveSourceProvenanceRequest(
            visibleAtObservationID: nil,
            sampleTypeIdentifier: typeIdentifier,
            limit: 10
        ))

        XCTAssertEqual(rows.count, 1)
        XCTAssertEqual(rows.first?.sampleTypeIdentifier, typeIdentifier)
        XCTAssertEqual(rows.first?.visibleRecordCount, 1)
        XCTAssertEqual(rows.first?.valueSum, 42)
        XCTAssertEqual(rows.first?.sourceBundleIdentifier, sample.sourceRevision.source.bundleIdentifier)
    }

    func testConsolidationEvidenceClassifiesStableCountDropAsConsolidationLikely() async throws {
        let url = databaseURL()
        let store = SQLiteHealthArchiveStore(databaseURL: url)
        let typeIdentifier = HKQuantityTypeIdentifier.stepCount.rawValue

        let firstOldSample = makeStepCountSample(value: 10, start: 1_000, end: 1_500)
        let secondOldSample = makeStepCountSample(value: 20, start: 1_600, end: 2_000)
        let consolidatedSample = makeStepCountSample(value: 30, start: 1_000, end: 2_500)

        _ = try await store.upsertSamples([firstOldSample, secondOldSample], observedAt: Date(timeIntervalSince1970: 3_000))
        _ = try await store.upsertSamples([consolidatedSample], observedAt: Date(timeIntervalSince1970: 3_060))
        try await store.recordDisappearance(sampleUUIDHash: HashService.sampleUUIDHash(firstOldSample.uuid.uuidString), sampleTypeIdentifier: typeIdentifier, observedMissingAt: Date(timeIntervalSince1970: 3_120))
        try await store.recordDisappearance(sampleUUIDHash: HashService.sampleUUIDHash(secondOldSample.uuid.uuidString), sampleTypeIdentifier: typeIdentifier, observedMissingAt: Date(timeIntervalSince1970: 3_180))

        let observationIDs = try observationIDs(at: url)
        XCTAssertEqual(observationIDs.count, 4)

        let rows = try await store.consolidationEvidence(HealthArchiveConsolidationEvidenceRequest(
            fromObservationID: observationIDs[0],
            toObservationID: observationIDs[observationIDs.count - 1],
            sampleTypeIdentifier: typeIdentifier
        ))

        XCTAssertEqual(rows.count, 1)
        XCTAssertEqual(rows.first?.sampleTypeIdentifier, typeIdentifier)
        XCTAssertEqual(rows.first?.disappearedCount, 2)
        XCTAssertEqual(rows.first?.appearedCount, 1)
        XCTAssertEqual(rows.first?.fromVisibleRecordCount, 2)
        XCTAssertEqual(rows.first?.toVisibleRecordCount, 1)
        XCTAssertEqual(rows.first?.fromValueSum, 30)
        XCTAssertEqual(rows.first?.toValueSum, 30)
        XCTAssertEqual(rows.first?.label, "consolidation_likely")
        XCTAssertTrue(rows.first?.sourceCompatible == true)
    }

    func testConsolidationEvidenceClassifiesDenseStableShiftAsAggregateChanged() async throws {
        let url = databaseURL()
        let store = SQLiteHealthArchiveStore(databaseURL: url)
        let typeIdentifier = HKQuantityTypeIdentifier.stepCount.rawValue

        let firstOldSample = makeStepCountSample(value: 10, start: 1_000, end: 1_500)
        let secondOldSample = makeStepCountSample(value: 20, start: 5_000, end: 6_000)
        let denseSample = makeStepCountSample(value: 30, start: 1_000, end: 1_200)

        _ = try await store.upsertSamples([firstOldSample, secondOldSample], observedAt: Date(timeIntervalSince1970: 3_000))
        _ = try await store.upsertSamples([denseSample], observedAt: Date(timeIntervalSince1970: 3_060))
        try await store.recordDisappearance(sampleUUIDHash: HashService.sampleUUIDHash(firstOldSample.uuid.uuidString), sampleTypeIdentifier: typeIdentifier, observedMissingAt: Date(timeIntervalSince1970: 3_120))
        try await store.recordDisappearance(sampleUUIDHash: HashService.sampleUUIDHash(secondOldSample.uuid.uuidString), sampleTypeIdentifier: typeIdentifier, observedMissingAt: Date(timeIntervalSince1970: 3_180))

        let observationIDs = try observationIDs(at: url)
        XCTAssertEqual(observationIDs.count, 4)

        let rows = try await store.consolidationEvidence(HealthArchiveConsolidationEvidenceRequest(
            fromObservationID: observationIDs[0],
            toObservationID: observationIDs[observationIDs.count - 1],
            sampleTypeIdentifier: typeIdentifier
        ))

        XCTAssertEqual(rows.count, 1)
        XCTAssertEqual(rows.first?.label, "aggregate_changed")
        XCTAssertEqual(rows.first?.fromVisibleRecordCount, 2)
        XCTAssertEqual(rows.first?.toVisibleRecordCount, 1)
        XCTAssertEqual(rows.first?.fromValueSum, 30)
        XCTAssertEqual(rows.first?.toValueSum, 30)
    }

    private func databaseURL() -> URL {
        temporaryDirectory.appending(path: "Archive.sqlite")
    }

    private func createPrototypeDatabase(at url: URL) throws {
        var db: OpaquePointer?
        guard sqlite3_open_v2(url.path, &db, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK else {
            sqlite3_close(db)
            XCTFail("Could not create prototype database")
            return
        }
        defer { sqlite3_close(db) }
        let status = sqlite3_exec(db, "CREATE TABLE archive_samples (id TEXT PRIMARY KEY)", nil, nil, nil)
        XCTAssertEqual(status, SQLITE_OK)
    }

    private func makeStepCountSample() -> HKQuantitySample {
        makeStepCountSample(value: 42, start: 1_000)
    }

    private func makeStepCountSample(value: Double, start: TimeInterval, end: TimeInterval? = nil) -> HKQuantitySample {
        let quantityType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
        let quantity = HKQuantity(unit: .count(), doubleValue: value)
        let startDate = Date(timeIntervalSince1970: start)
        let endDate = Date(timeIntervalSince1970: end ?? (start + 300))
        return HKQuantitySample(type: quantityType, quantity: quantity, start: startDate, end: endDate)
    }

    private func makeStepCountSamples(count: Int, startIndex: Int) -> [HKQuantitySample] {
        (0..<count).map { offset in
            let index = startIndex + offset
            return makeStepCountSample(
                value: Double((index % 97) + 1),
                start: 10_000 + Double(index * 600)
            )
        }
    }

    private func waitForArchiveOperation<T>(_ operation: @escaping () async throws -> T) throws -> T {
        let expectation = expectation(description: "archive operation")
        let box = AsyncResultBox<T>()

        Task {
            do {
                box.result = .success(try await operation())
            } catch {
                box.result = .failure(error)
            }
            expectation.fulfill()
        }

        wait(for: [expectation], timeout: 20)
        return try XCTUnwrap(box.result).get()
    }

    private func countRows(in tableName: String, at url: URL) throws -> Int {
        var db: OpaquePointer?
        guard sqlite3_open_v2(url.path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK else {
            sqlite3_close(db)
            XCTFail("Could not open test database")
            return 0
        }
        defer { sqlite3_close(db) }

        var statement: OpaquePointer?
        guard sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM \(tableName)", -1, &statement, nil) == SQLITE_OK else {
            sqlite3_finalize(statement)
            XCTFail("Could not prepare count query")
            return 0
        }
        defer { sqlite3_finalize(statement) }

        guard sqlite3_step(statement) == SQLITE_ROW else {
            return 0
        }
        return Int(sqlite3_column_int(statement, 0))
    }

    private func tableExists(_ tableName: String, at url: URL) throws -> Bool {
        var db: OpaquePointer?
        guard sqlite3_open_v2(url.path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK else {
            sqlite3_close(db)
            XCTFail("Could not open test database")
            return false
        }
        defer { sqlite3_close(db) }

        let sql = """
        SELECT 1
        FROM sqlite_master
        WHERE type = 'table' AND name = ?
        LIMIT 1
        """
        var statement: OpaquePointer?
        guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
            sqlite3_finalize(statement)
            XCTFail("Could not prepare table existence query")
            return false
        }
        defer { sqlite3_finalize(statement) }

        sqlite3_bind_text(statement, 1, tableName, -1, unsafeBitCast(-1, to: sqlite3_destructor_type.self))
        return sqlite3_step(statement) == SQLITE_ROW
    }

    private func observationIDs(at url: URL) throws -> [Int64] {
        var db: OpaquePointer?
        guard sqlite3_open_v2(url.path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK else {
            sqlite3_close(db)
            XCTFail("Could not open test database")
            return []
        }
        defer { sqlite3_close(db) }

        var statement: OpaquePointer?
        guard sqlite3_prepare_v2(db, "SELECT id FROM observations ORDER BY id", -1, &statement, nil) == SQLITE_OK else {
            sqlite3_finalize(statement)
            XCTFail("Could not prepare observation query")
            return []
        }
        defer { sqlite3_finalize(statement) }

        var ids: [Int64] = []
        while sqlite3_step(statement) == SQLITE_ROW {
            ids.append(sqlite3_column_int64(statement, 0))
        }
        return ids
    }

    private func sampleVersionDebugRows(at url: URL) throws -> String {
        var db: OpaquePointer?
        guard sqlite3_open_v2(url.path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK else {
            sqlite3_close(db)
            return "could not open database"
        }
        defer { sqlite3_close(db) }

        let sql = """
        SELECT v.payload_hash, v.start_date, v.end_date, v.value_kind, v.numeric_value, v.unit,
               v.source_revision_id, src.bundle_identifier, sr.product_type, sr.version,
               sr.operating_system_version, v.hk_device_id, v.metadata_id
        FROM sample_versions v
        LEFT JOIN source_revisions sr ON sr.id = v.source_revision_id
        LEFT JOIN sources src ON src.id = sr.source_id
        ORDER BY v.id
        """
        var statement: OpaquePointer?
        guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
            sqlite3_finalize(statement)
            return "could not prepare version debug query"
        }
        defer { sqlite3_finalize(statement) }

        var rows: [String] = []
        while sqlite3_step(statement) == SQLITE_ROW {
            rows.append((0..<13).map { index in
                if sqlite3_column_type(statement, Int32(index)) == SQLITE_NULL {
                    return "null"
                }
                if let text = sqlite3_column_text(statement, Int32(index)) {
                    return String(cString: text)
                }
                return "\(sqlite3_column_double(statement, Int32(index)))"
            }.joined(separator: "|"))
        }
        return rows.joined(separator: "\n")
    }

    private func visibilityRangeDebugRows(at url: URL) throws -> String {
        var db: OpaquePointer?
        guard sqlite3_open_v2(url.path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK else {
            sqlite3_close(db)
            return "could not open database"
        }
        defer { sqlite3_close(db) }

        let sql = """
        SELECT sample_id, version_id, first_observation_id, last_observation_id, first_seen_at, last_seen_at
        FROM sample_visibility_ranges
        ORDER BY sample_id, version_id, first_observation_id
        """
        var statement: OpaquePointer?
        guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
            sqlite3_finalize(statement)
            return "could not prepare visibility debug query"
        }
        defer { sqlite3_finalize(statement) }

        var rows: [String] = []
        while sqlite3_step(statement) == SQLITE_ROW {
            rows.append((0..<6).map { index in
                if sqlite3_column_type(statement, Int32(index)) == SQLITE_NULL {
                    return "null"
                }
                if let text = sqlite3_column_text(statement, Int32(index)) {
                    return String(cString: text)
                }
                return "\(sqlite3_column_double(statement, Int32(index)))"
            }.joined(separator: "|"))
        }
        return rows.joined(separator: "\n")
    }
}