import Foundation
import GRDB
import Testing
@testable import ICallsSync

@Suite("ICallsSync CallRecord and stats")
struct CallRecordTests {
    @Test("SyncCallPayload round-trips basic fields")
    func payloadBasics() {
        let rec = SyncCallPayload(
            uniqueId: "zpk:42",
            address: "+14155551234",
            normalizedAddress: "+14155551234",
            contactName: "Alice",
            direction: "incoming",
            callType: "telephony",
            answered: true,
            durationSeconds: 127.5,
            startedAt: "2026-06-01T12:00:00Z",
            serviceProvider: "com.apple.Telephony"
        )
        #expect(rec.uniqueId == "zpk:42")
        #expect(rec.direction == "incoming")
        #expect(rec.answered == true)
        #expect(rec.durationSeconds == 127.5)
    }

    @Test("ICallsSyncStats and error defaults")
    func statsAndErrors() {
        var stats = ICallsSyncStats()
        #expect(stats.callCount == 0)
        stats.callCount = 17
        stats.syncedThisSession = 5
        #expect(stats.callCount == 17)

        let err = ICallsSyncError.databaseNotFound
        #expect(err.message == "Call history database not found")
        #expect(ICallsSyncError.fullDiskAccessRequired.message == "Full Disk Access required (Call History)")
        #expect(ICallsSyncError.none.message == "")
    }
}

@Suite("ICallsSync call-type classification")
struct CallTypeClassificationTests {
    @Test("FaceTime audio is ZCALLTYPE 16, video otherwise")
    func faceTime() {
        #expect(CallHistoryReader.classifyCallType(serviceProvider: "com.apple.FaceTime", callTypeRaw: 16) == "facetime_audio")
        #expect(CallHistoryReader.classifyCallType(serviceProvider: "com.apple.FaceTime", callTypeRaw: 1) == "facetime_video")
        #expect(CallHistoryReader.classifyCallType(serviceProvider: "com.apple.FaceTime", callTypeRaw: 0) == "facetime_video")
    }

    @Test("Cellular maps to telephony")
    func telephony() {
        #expect(CallHistoryReader.classifyCallType(serviceProvider: "com.apple.Telephony", callTypeRaw: 0) == "telephony")
        #expect(CallHistoryReader.classifyCallType(serviceProvider: nil, callTypeRaw: 1) == "telephony")
    }

    @Test("Unknown provider with no telephony hint is unknown")
    func unknown() {
        #expect(CallHistoryReader.classifyCallType(serviceProvider: nil, callTypeRaw: 0) == "unknown")
        #expect(CallHistoryReader.classifyCallType(serviceProvider: "com.example.weird", callTypeRaw: 7) == "unknown")
    }
}

@Suite("ICallsSync row parsing")
struct CallRecordParsingTests {
    @Test("ZORIGINATED + ZANSWERED map to direction/answered")
    func directionAndAnswered() {
        let outgoing = CallHistoryReader.parse(
            row: Row(["Z_PK": 1, "ZUNIQUE_ID": "u1", "ZADDRESS": "+14155550000",
                      "ZORIGINATED": 1, "ZANSWERED": 1, "ZDURATION": 30.0, "ZDATE": 700_000_000.0]),
            contactLookup: { _ in nil })
        #expect(outgoing.direction == "outgoing")
        #expect(outgoing.answered == true)
        #expect(outgoing.uniqueId == "u1")

        let missedIncoming = CallHistoryReader.parse(
            row: Row(["Z_PK": 2, "ZORIGINATED": 0, "ZANSWERED": 0, "ZDATE": 700_000_100.0]),
            contactLookup: { _ in nil })
        #expect(missedIncoming.direction == "incoming")
        #expect(missedIncoming.answered == false)
    }

    @Test("Empty ZUNIQUE_ID falls back to synthetic zpk id")
    func syntheticUniqueId() {
        let rec = CallHistoryReader.parse(
            row: Row(["Z_PK": 99, "ZUNIQUE_ID": "", "ZDATE": 700_000_000.0]),
            contactLookup: { _ in nil })
        #expect(rec.uniqueId == "zpk:99")
    }

    @Test("Contact lookup fills name when ZNAME is absent")
    func contactEnrichment() {
        let rec = CallHistoryReader.parse(
            row: Row(["Z_PK": 3, "ZADDRESS": "+14155551234", "ZDATE": 700_000_000.0]),
            contactLookup: { $0 == "+14155551234" ? "Bob" : nil })
        #expect(rec.contactName == "Bob")
    }
}

@Suite("ICallsSync reader snapshot (WAL visibility)")
struct CallHistorySnapshotTests {
    /// Build a ZCALLRECORD fixture in WAL mode with autocheckpoint disabled, so
    /// inserted rows live in the `-wal` file. The writer queue is kept alive by
    /// the caller so the WAL is not flushed before the reader snapshots it —
    /// this is exactly the stale-read condition the snapshot fix exists to beat.
    private func makeFixture(at path: String) throws -> DatabaseQueue {
        let q = try DatabaseQueue(path: path)
        try q.writeWithoutTransaction { db in
            _ = try String.fetchOne(db, sql: "PRAGMA journal_mode = WAL")
            try db.execute(sql: "PRAGMA wal_autocheckpoint = 0")
        }
        try q.write { db in
            try db.execute(sql: """
                CREATE TABLE ZCALLRECORD (
                    Z_PK INTEGER PRIMARY KEY,
                    ZUNIQUE_ID TEXT, ZADDRESS TEXT, ZNAME TEXT,
                    ZORIGINATED INTEGER, ZANSWERED INTEGER, ZDURATION REAL,
                    ZDATE REAL, ZCALLTYPE INTEGER, ZSERVICE_PROVIDER TEXT
                )
                """)
            try db.execute(sql: """
                INSERT INTO ZCALLRECORD
                    (Z_PK, ZUNIQUE_ID, ZADDRESS, ZORIGINATED, ZANSWERED, ZDURATION, ZDATE, ZCALLTYPE, ZSERVICE_PROVIDER)
                VALUES
                    (1, 'older', '+14155550001', 0, 1, 12.0, 700000000.0, 0, 'com.apple.Telephony'),
                    (2, 'newer', '+14155550002', 1, 1, 34.0, 700000500.0, 16, 'com.apple.FaceTime')
                """)
        }
        return q
    }

    @Test("Reader sees rows still sitting in the WAL")
    func readsUnflushedWal() throws {
        let dir = FileManager.default.temporaryDirectory
            .appendingPathComponent("icalls-fixture-\(UUID().uuidString)", isDirectory: true)
        try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(at: dir) }
        let dbPath = dir.appendingPathComponent("CallHistory.storedata").path

        let writer = try makeFixture(at: dbPath)
        defer { try? writer.close() }

        let calls = CallHistoryReader.shared.readCalls(dbPath: dbPath, since: nil)
        #expect(calls.count == 2)
        // Oldest-first (ORDER BY ZDATE ASC) so the server ingests in order.
        #expect(calls.first?.uniqueId == "older")
        #expect(calls.last?.callType == "facetime_audio")
    }

    @Test("since filters out rows at or before the watermark")
    func sinceWatermark() throws {
        let dir = FileManager.default.temporaryDirectory
            .appendingPathComponent("icalls-fixture-\(UUID().uuidString)", isDirectory: true)
        try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(at: dir) }
        let dbPath = dir.appendingPathComponent("CallHistory.storedata").path

        let writer = try makeFixture(at: dbPath)
        defer { try? writer.close() }

        // Between the two rows (700000000 < 700000250 < 700000500).
        let watermark = Date(timeIntervalSinceReferenceDate: 700_000_250.0)
        let calls = CallHistoryReader.shared.readCalls(dbPath: dbPath, since: watermark)
        #expect(calls.count == 1)
        #expect(calls.first?.uniqueId == "newer")
    }
}
