//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import XCTest
@testable import SignalServiceKit

// LastResort
//

class KyberPreKeyStoreTest: XCTestCase {
    var keyValueStoreFactory: KeyValueStoreFactory!
    var dateProvider: DateProvider!
    var currentDate = Date()
    var db = MockDB()

    var identityKey: ECKeyPair!
    var kyberPreKeyStore: SSKKyberPreKeyStore!

    override func setUp() {
        keyValueStoreFactory = InMemoryKeyValueStoreFactory()
        dateProvider = { return self.currentDate }
        identityKey = ECKeyPair.generateKeyPair()
        kyberPreKeyStore = SSKKyberPreKeyStore(
            for: .aci,
            keyValueStoreFactory: keyValueStoreFactory,
            dateProvider: dateProvider
        )
    }

    func testCreate() {
        let key = try! self.db.write { tx in
            try self.kyberPreKeyStore.generateLastResortKyberPreKey(
                signedBy: self.identityKey,
                tx: tx
            )
        }

        XCTAssert(
            try! self.identityKey.keyPair.publicKey.verifySignature(
                message: Data(key.keyPair.publicKey.serialize()),
                signature: key.signature
            )
        )
        XCTAssertNotNil(key)
    }

    func testEncodeDecode() {
        let record = try! self.db.write { tx in
            try self.kyberPreKeyStore.generateLastResortKyberPreKey(
                signedBy: self.identityKey,
                tx: tx
            )
        }

        XCTAssertNotNil(record)

        let jsonData = try! JSONEncoder().encode(record)
        let decodedRecord = try! JSONDecoder().decode(KyberPreKeyRecord.self, from: jsonData)

        XCTAssertEqual(record.id, decodedRecord.id)
        XCTAssert(
            try self.identityKey.keyPair.publicKey.verifySignature(
                message: Data(decodedRecord.keyPair.publicKey.serialize()),
                signature: decodedRecord.signature
            )
        )
    }

    func testGenerateIncrementsNextId() {
        let metadataStore = keyValueStoreFactory.keyValueStore(
            collection: SSKKyberPreKeyStore.Constants.ACI.metadataStoreCollection
        )

        db.write { tx in
            metadataStore.setInt32(500, key: SSKKyberPreKeyStore.Constants.lastKeyId, transaction: tx)
        }

        let records = try! self.db.write { tx in
            try self.kyberPreKeyStore.generateKyberPreKeyRecords(
                count: 25,
                signedBy: self.identityKey,
                tx: tx
            )
        }

        XCTAssertEqual(records.first!.id, 501)
        XCTAssertEqual(records.last!.id, 525)

        let nextId = db.read { tx in
            metadataStore.getInt32(SSKKyberPreKeyStore.Constants.lastKeyId, transaction: tx)
        }
        XCTAssertEqual(nextId!, 525)
    }

    func testLastResortId() {
        let metadataStore = keyValueStoreFactory.keyValueStore(
            collection: SSKKyberPreKeyStore.Constants.ACI.metadataStoreCollection
        )

        db.write { tx in
            metadataStore.setInt32(
                0xFFFFFE,
                key: SSKKyberPreKeyStore.Constants.lastKeyId,
                transaction: tx
            )
        }

        let record1 = try! self.db.write { tx in
            try self.kyberPreKeyStore.generateLastResortKyberPreKey(signedBy: identityKey, tx: tx)
        }

        XCTAssertEqual(record1.id, 0xFFFFFF)

        let record2 = try! self.db.write { tx in
            try self.kyberPreKeyStore.generateLastResortKyberPreKey(signedBy: identityKey, tx: tx)
        }

        XCTAssertEqual(record2.id, 1)
    }

    // check that prekeyIds overflow in batches
    func testPreKeyIdOverFlow() {
        let metadataStore = keyValueStoreFactory.keyValueStore(
            collection: SSKKyberPreKeyStore.Constants.ACI.metadataStoreCollection
        )

        let batchCount: Int32 = 50
        db.write { tx in
            let lastKeyId: Int32 = 0x1000000 - batchCount
            metadataStore.setInt32(lastKeyId, key: SSKKyberPreKeyStore.Constants.lastKeyId, transaction: tx)
        }

        let records = try! self.db.write { tx in
            try self.kyberPreKeyStore.generateKyberPreKeyRecords(
                count: 50,
                signedBy: self.identityKey,
                tx: tx
            )
        }

        XCTAssertEqual(records.first!.id, 1)
        XCTAssertEqual(records.last!.id, 50)

        let nextId = db.read { tx in
            metadataStore.getInt32(SSKKyberPreKeyStore.Constants.lastKeyId, transaction: tx)
        }
        XCTAssertEqual(nextId!, 50)
    }

    // MARK: OneTime keys

   // Ensure that keys aren't marked as last resort
    func testOneTimeKeysNotMarkedAsLastResort() {
        let key = try! self.db.write { tx in
            try self.kyberPreKeyStore.generateKyberPreKeyRecords(
                count: 1,
                signedBy: self.identityKey,
                tx: tx
            ).first
        }

        XCTAssertNotNil(key)
        XCTAssert(
            try! self.identityKey.keyPair.publicKey.verifySignature(
                message: Data(key!.keyPair.publicKey.serialize()),
                signature: key!.signature
            )
        )
        XCTAssertFalse(key!.isLastResort)
    }

    func testGenerateDoesNotStore() {
        let key = try! self.db.write { tx in
            try self.kyberPreKeyStore.generateKyberPreKeyRecords(
                count: 1,
                signedBy: self.identityKey,
                tx: tx
            ).first
        }

        let keyStore = keyValueStoreFactory.keyValueStore(
            collection: SSKKyberPreKeyStore.Constants.ACI.keyStoreCollection
        )

        let fetchedKey = self.db.read { tx in
            keyStore.getObject(
                forKey: kyberPreKeyStore.key(for: key!.id),
                transaction: tx
            )
        }
        XCTAssertNil(fetchedKey)
    }

    // test that storing a batch of keys is reflected in storage
    func testGenerateAndStoreBatch() {
        let records = try! self.db.write { tx in
            let records = try self.kyberPreKeyStore.generateKyberPreKeyRecords(
                count: 5,
                signedBy: self.identityKey,
                tx: tx
            )

            try! self.kyberPreKeyStore.storeKyberPreKeyRecords(records: records, tx: tx)

            return records
        }

        let keyStore = keyValueStoreFactory.keyValueStore(
            collection: SSKKyberPreKeyStore.Constants.ACI.keyStoreCollection
        )

        self.db.read { tx in
            for record in records {
                let fetchedRecord = keyStore.getObject(
                    forKey: kyberPreKeyStore.key(for: record.id),
                    transaction: tx
                )
                XCTAssertNotNil(fetchedRecord)
            }
        }
    }

    // MARK: - LastResort
    func testMarkedAsLastResort() {
        // test that storing a batch of keys is reflected in storage
        let lastResortKey = try! self.db.write { tx in
            try self.kyberPreKeyStore.generateLastResortKyberPreKey(
                signedBy: self.identityKey,
                tx: tx
            )
        }

        XCTAssertNotNil(lastResortKey)
        XCTAssertTrue(lastResortKey.isLastResort)

        let keyStore = keyValueStoreFactory.keyValueStore(
            collection: SSKKyberPreKeyStore.Constants.ACI.keyStoreCollection
        )

        let fetchedKey = self.db.read { tx in
            keyStore.getObject(
                forKey: kyberPreKeyStore.key(for: lastResortKey.id),
                transaction: tx
            )
        }
        XCTAssertNil(fetchedKey)
    }

    // test that storing a batch of keys is reflected in storage
    func testLastResortFetching() {
        let lastResortKey = try! self.db.write { tx in
            let lastResortKey = try self.kyberPreKeyStore.generateLastResortKyberPreKey(
                signedBy: self.identityKey,
                tx: tx
            )

            try! self.kyberPreKeyStore.storeLastResortPreKey(
                record: lastResortKey,
                tx: tx
            )

            return lastResortKey
        }

        let keyStore = keyValueStoreFactory.keyValueStore(
            collection: SSKKyberPreKeyStore.Constants.ACI.keyStoreCollection
        )

        self.db.read { tx in
            // Check the raw record was stored
            let fetchedRecord = keyStore.getObject(
                forKey: kyberPreKeyStore.key(for: lastResortKey.id),
                transaction: tx
            )
            XCTAssertNotNil(fetchedRecord)

            // Check the record deserializes correctly
            let fetchedKey = self.kyberPreKeyStore.loadKyberPreKey(id: lastResortKey.id, tx: tx)
            XCTAssertNotNil(fetchedKey)
            XCTAssertTrue(fetchedKey!.isLastResort)
            XCTAssertEqual(fetchedKey!.id, lastResortKey.id)
        }
    }

    func testMarkAsUsedOneTime () {
        let records = try! self.db.write { tx in
            let records = try self.kyberPreKeyStore.generateKyberPreKeyRecords(
                count: 5,
                signedBy: self.identityKey,
                tx: tx
            )

            try! self.kyberPreKeyStore.storeKyberPreKeyRecords(records: records, tx: tx)

            return records
        }

        let firstRecord = records.first!
        try! self.db.write { tx in
            try self.kyberPreKeyStore.markKyberPreKeyUsed(id: firstRecord.id, tx: tx)
        }

        let keyStore = keyValueStoreFactory.keyValueStore(
            collection: SSKKyberPreKeyStore.Constants.ACI.keyStoreCollection
        )

        self.db.read { tx in
            for record in records {
                let fetchedRecord = keyStore.getObject(
                    forKey: kyberPreKeyStore.key(for: record.id),
                    transaction: tx
                )
                if record.id == firstRecord.id {
                    XCTAssertNil(fetchedRecord)
                } else {
                    XCTAssertNotNil(fetchedRecord)
                }
            }
        }
    }

    func testMarkAsUsedLastResort() {
        let lastResortKey = try! self.db.write { tx in
            let lastResortKey = try self.kyberPreKeyStore.generateLastResortKyberPreKey(
                signedBy: self.identityKey,
                tx: tx
            )

            try! self.kyberPreKeyStore.storeLastResortPreKey(
                record: lastResortKey,
                tx: tx
            )

            return lastResortKey
        }

        try! self.db.write { tx in
            try self.kyberPreKeyStore.markKyberPreKeyUsed(id: lastResortKey.id, tx: tx)
        }

        self.db.read { tx in
            let fetchedKey = self.kyberPreKeyStore.loadKyberPreKey(id: lastResortKey.id, tx: tx)
            XCTAssertNotNil(fetchedKey)
        }
    }

    func testPniStoreIsSeparate() {

        let pniIdentityKey = ECKeyPair.generateKeyPair()
        let pniKyberPreKeyStore = SSKKyberPreKeyStore(
            for: .pni,
            keyValueStoreFactory: keyValueStoreFactory,
            dateProvider: dateProvider
        )

        func generateKeys(keyStore: SSKKyberPreKeyStore, identityKey: ECKeyPair) -> ([KyberPreKeyRecord], KyberPreKeyRecord) {
            return try! self.db.write { tx in
                let records = try keyStore.generateKyberPreKeyRecords(
                    count: 10,
                    signedBy: identityKey,
                    tx: tx
                )

                try! keyStore.storeKyberPreKeyRecords(records: records, tx: tx)

                let lastResort = try keyStore.generateLastResortKyberPreKey(
                    signedBy: identityKey,
                    tx: tx)

                try! keyStore.storeLastResortPreKey(record: lastResort, tx: tx)

                return (records, lastResort)
            }
        }

        let (aciRecords, aciLastResort) = generateKeys(keyStore: kyberPreKeyStore, identityKey: identityKey)
        let (pniRecords, pniLastResort) = generateKeys(keyStore: pniKyberPreKeyStore, identityKey: pniIdentityKey)

        let aciKeyStore = keyValueStoreFactory.keyValueStore(
            collection: SSKKyberPreKeyStore.Constants.ACI.keyStoreCollection
        )

        let pniKeyStore = keyValueStoreFactory.keyValueStore(
            collection: SSKKyberPreKeyStore.Constants.PNI.keyStoreCollection
        )

        self.db.read { tx in

            // make sure no PNI one-time keys in ACI store
            for record in aciRecords {
                let aciRecord = aciKeyStore.getObject(forKey: kyberPreKeyStore.key(for: record.id), transaction: tx)
                let pniRecord = pniKeyStore.getObject(forKey: kyberPreKeyStore.key(for: record.id), transaction: tx)

                XCTAssertNotNil(aciRecord)
                XCTAssertNil(pniRecord)
            }

            // make sure no ACI one-time keys in PNI store
            for record in pniRecords {
                let aciRecord = aciKeyStore.getObject(forKey: kyberPreKeyStore.key(for: record.id), transaction: tx)
                let pniRecord = pniKeyStore.getObject(forKey: kyberPreKeyStore.key(for: record.id), transaction: tx)

                XCTAssertNil(aciRecord)
                XCTAssertNotNil(pniRecord)
            }

            // make sure no PNI last resort keys in ACI store
            let aciLastResortRecord1 = aciKeyStore.getObject(forKey: kyberPreKeyStore.key(for: aciLastResort.id), transaction: tx)
            let pniLastResortRecord1 = pniKeyStore.getObject(forKey: kyberPreKeyStore.key(for: aciLastResort.id), transaction: tx)
                XCTAssertNotNil(aciLastResortRecord1)
                XCTAssertNil(pniLastResortRecord1)

            // make sure no ACI last resort keys in PNI store
            let aciLastResortRecord2 = aciKeyStore.getObject(forKey: kyberPreKeyStore.key(for: pniLastResort.id), transaction: tx)
            let pniLastResortRecord2 = pniKeyStore.getObject(forKey: kyberPreKeyStore.key(for: pniLastResort.id), transaction: tx)
                XCTAssertNil(aciLastResortRecord2)
                XCTAssertNotNil(pniLastResortRecord2)
        }
    }

    func testCullOneTimeKeys() {
        currentDate = Date(
            timeIntervalSinceNow: -(SSKKyberPreKeyStore.Constants.oneTimeKeyExpirationInterval + 1)
        )
        try! self.db.write { tx in
            let records = try self.kyberPreKeyStore.generateKyberPreKeyRecords(
                count: 5,
                signedBy: self.identityKey,
                tx: tx
            )
            try self.kyberPreKeyStore.storeKyberPreKeyRecords(records: records, tx: tx)
        }

        currentDate = Date()

        let currentRecords = try! self.db.write { tx in
            let records = try self.kyberPreKeyStore.generateKyberPreKeyRecords(
                count: 5,
                signedBy: self.identityKey,
                tx: tx
            )

            try self.kyberPreKeyStore.storeKyberPreKeyRecords(records: records, tx: tx)
            return records
        }

        let keyStore = keyValueStoreFactory.keyValueStore(
            collection: SSKKyberPreKeyStore.Constants.ACI.keyStoreCollection
        )

        let numRecords = self.db.read { tx in
            keyStore.allKeys(transaction: tx).count
        }
        XCTAssertEqual(numRecords, 10)

        self.db.write { tx in
            try! self.kyberPreKeyStore.cullOneTimePreKeyRecords(tx: tx)
        }

        let recordsAfterCull: [KyberPreKeyRecord] = self.db.read { tx in
            try! keyStore.allCodableValues(transaction: tx).filter { !$0.isLastResort }
        }

        let sortedExpectedRecords = currentRecords.sorted { $0.id < $1.id }
        let sortedFoundRecords = recordsAfterCull.sorted { $0.id < $1.id }
        XCTAssertEqual(sortedExpectedRecords.count, sortedFoundRecords.count)
        zip(sortedFoundRecords, sortedExpectedRecords).forEach {
            XCTAssertEqual($0.id, $1.id)
        }

        XCTAssertEqual(recordsAfterCull.count, 5)
    }

    func testCullLastResortKeys() {
        currentDate = Date(
            timeIntervalSinceNow: -(SSKKyberPreKeyStore.Constants.lastResortKeyExpirationInterval + 1)
        )

        _ = try! self.db.write { tx in
            let record = try self.kyberPreKeyStore.generateLastResortKyberPreKey(signedBy: self.identityKey, tx: tx)
            try self.kyberPreKeyStore.storeLastResortPreKey(record: record, tx: tx)
            return record
        }

        currentDate = Date(
            timeIntervalSinceNow: -(SSKKyberPreKeyStore.Constants.lastResortKeyExpirationInterval - 1)
        )

        let oldUnexpiredLastResort = try! self.db.write { tx in
            let record = try self.kyberPreKeyStore.generateLastResortKyberPreKey(signedBy: self.identityKey, tx: tx)
            try self.kyberPreKeyStore.storeLastResortPreKey(record: record, tx: tx)
            return record
        }

        currentDate = Date(
            timeIntervalSinceNow: -(SSKKyberPreKeyStore.Constants.lastResortKeyExpirationInterval + 1)
        )

        let currentLastResort = try! self.db.write { tx in
            let record = try self.kyberPreKeyStore.generateLastResortKyberPreKey(signedBy: self.identityKey, tx: tx)
            try self.kyberPreKeyStore.storeLastResortPreKey(record: record, tx: tx)
            return record
        }

        let keyStore = keyValueStoreFactory.keyValueStore(
            collection: SSKKyberPreKeyStore.Constants.ACI.keyStoreCollection
        )

        currentDate = Date()

        self.db.write { tx in
            try! self.kyberPreKeyStore.cullLastResortPreKeyRecords(
                justUploadedLastResortPreKey: currentLastResort,
                tx: tx
            )
        }

        let recordsAfterCull: [KyberPreKeyRecord] = self.db.read { tx in
            try! keyStore.allCodableValues(transaction: tx).filter { $0.isLastResort }
        }

        XCTAssertEqual(recordsAfterCull.count, 2)
        XCTAssertNotNil(recordsAfterCull.firstIndex(where: { $0.id == oldUnexpiredLastResort.id }))
        XCTAssertNotNil(recordsAfterCull.firstIndex(where: { $0.id == currentLastResort.id }))
    }
}
