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

import XCTest
@testable import SignalServiceKit

final class APNSRotationStoreTest: SignalBaseTest {

    lazy var messageFactory = IncomingMessageFactory()

    override func setUp() {
        super.setUp()

        let remoteConfigManager = self.remoteConfigManager as! StubbableRemoteConfigManager
        remoteConfigManager.cachedConfig = RemoteConfig(
            clockSkew: 0,
            isEnabledFlags: ["ios.enableAutoAPNSRotation": true],
            valueFlags: [:],
            timeGatedFlags: [:]
        )
    }

    override func tearDown() {
        super.tearDown()

        APNSRotationStore.nowMs = { return Date().ows_millisecondsSince1970 }
    }

    func testHasNoPushToken() {
        // Make sure we don't have an APNS token
        preferences.removeAllValues()
        let now = Date().ows_millisecondsSince1970

        // Make sure we are otherwise eligible to rotate, so
        // that we know it is the lack of an APNS token that stopped it.
        write { transaction in
            // Mark as checked in the past so that we'd be eligible after an app update.
            APNSRotationStore.nowMs = {
                return now
                    - APNSRotationStore.Constants.appVersionBakeTimeMs
                    - 1
            }
            APNSRotationStore.setAppVersionTimeForAPNSRotationIfNeeded(transaction: transaction)

            // Fake a missed message so we'd rotate.
            self.createIncomingMessage(
                receivedTimestamp: now - kMinuteInMs,
                transaction: transaction
            )
        }

        read {
            APNSRotationStore.nowMs = { now }
            // No rotation because there's no APNS token.
            XCTAssertFalse(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }

        // Set an APNS token.
        write { tx in
            preferences.setPushToken("123", tx: tx)
        }

        read {
            // Nothing changed but the token, but we should now rotate.
            XCTAssert(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }
    }

    func testHasWorkingPushToken() {
        let now = Date().ows_millisecondsSince1970

        // Make sure we are otherwise eligible to rotate, so
        // that we know it is having a good APNS token that stopped it.

        // Set an APNS token.
        write { tx in
            preferences.setPushToken("123", tx: tx)
        }

        write { transaction in
            // Mark as checked in the past so that we'd be eligible after an app update.
            APNSRotationStore.nowMs = {
                return now
                    - APNSRotationStore.Constants.appVersionBakeTimeMs
                    - 1
            }
            APNSRotationStore.setAppVersionTimeForAPNSRotationIfNeeded(transaction: transaction)
        }

        read {
            APNSRotationStore.nowMs = { now }
            // Make sure we need to rotate before marking the token as good.
            XCTAssert(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }

        // Now mark receiving a push (marking the token as working)
        write {
            APNSRotationStore.didReceiveAPNSPush(transaction: $0)
        }

        read {
            // The token should now be marked as good and not needing rotation.
            XCTAssertFalse(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }

        // Change the token!
        write { tx in
            preferences.setPushToken("abc", tx: tx)
        }

        read {
            // Now we need to rotate again because the token changed but we had missed
            // messages.
            XCTAssert(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }
    }

    func testHasWorkingPushTokenFromLongAgo() {
        let now = Date().ows_millisecondsSince1970

        // Make sure we are otherwise eligible to rotate, so
        // that we know the determining factor is the APNS token.

        // Set an APNS token.
        write { tx in
            preferences.setPushToken("123", tx: tx)
        }

        write { transaction in
            // Mark as checked in the past so that we'd be eligible after an app update.
            APNSRotationStore.nowMs = {
                return now
                    - APNSRotationStore.Constants.appVersionBakeTimeMs
                    - 1
            }
            APNSRotationStore.setAppVersionTimeForAPNSRotationIfNeeded(transaction: transaction)
        }

        read {
            APNSRotationStore.nowMs = { now }
            // Make sure we need to rotate before marking the token as good.
            XCTAssert(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }

        // Now mark receiving a push (marking the token as working)
        write {
            APNSRotationStore.didReceiveAPNSPush(transaction: $0)
        }

        read {
            // The token should now be marked as good and not needing rotation.
            XCTAssertFalse(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }

        // Let some time pass, but not too long.
        APNSRotationStore.nowMs = { now + APNSRotationStore.Constants.lastKnownWorkingAPNSTokenExpirationTimeMs - 1 }

        read {
            // Still no need to rotate, it was marked good a short time ago.
            XCTAssertFalse(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }

        // Now move the time far up, long since we marked the token as good,
        // so it can now be rotated.
        APNSRotationStore.nowMs = { now + APNSRotationStore.Constants.lastKnownWorkingAPNSTokenExpirationTimeMs + 1 }

        read {
            // Now we need to rotate because it was marked good too long ago.
            XCTAssert(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }
    }

    func testHasUpdatedRecently() {
        // Make sure we have an APNS Token
        write { tx in
            preferences.setPushToken("123", tx: tx)
        }
        let now = Date().ows_millisecondsSince1970

        // Should not want to rotate, because the NSE hasn't even had a chance
        // to write anything.
        read {
            APNSRotationStore.nowMs = { now }
            XCTAssertFalse(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }

        write {
            // mark the app version.
            APNSRotationStore.nowMs = { now }
            APNSRotationStore.setAppVersionTimeForAPNSRotationIfNeeded(transaction: $0)
        }

        read {
            // Simulate time having passed
            APNSRotationStore.nowMs = { now + APNSRotationStore.Constants.appVersionBakeTimeMs + 1 }
            // Now check, we should be eligible to rotate.
            XCTAssert(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }
    }

    func testHasRotatedRecently() {
        // Make sure we have an APNS Token
        write { tx in
            preferences.setPushToken("123", tx: tx)
        }
        let now = Date().ows_millisecondsSince1970

        // Make sure we are otherwise eligible to rotate, so
        // that we know it is the the recent rotation that stopped it.
        write { transaction in
            // Mark as checked in the past so that we'd be eligible after an app update.
            APNSRotationStore.nowMs = {
                return now
                    - APNSRotationStore.Constants.appVersionBakeTimeMs
                    - 1
            }
            APNSRotationStore.setAppVersionTimeForAPNSRotationIfNeeded(transaction: transaction)

            // But make a recent rotation.
            APNSRotationStore.nowMs = { now - APNSRotationStore.Constants.minRotationInterval + 1 }
            APNSRotationStore.didRotateAPNSToken(transaction: transaction)
        }

        read {
            APNSRotationStore.nowMs = { now }
            // No rotation because we just rotated recently.
            XCTAssertFalse(APNSRotationStore.canRotateAPNSToken(transaction: $0))

            // Move time forward.
            APNSRotationStore.nowMs = { now + APNSRotationStore.Constants.minRotationInterval + 1 }
            // Now we want to rotate
            XCTAssert(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }
    }

    func testRecentMissedMessages() {
        // Make sure we have an APNS Token
        write { tx in
            preferences.setPushToken("123", tx: tx)
        }
        let now = Date().ows_millisecondsSince1970

        let lastPushTime = now
            - APNSRotationStore.Constants.appVersionBakeTimeMs
            - 1
        write { transaction in
            // Mark as checked in the past so that we'd be eligible after an app update.
            APNSRotationStore.nowMs = {
                return lastPushTime
            }
            APNSRotationStore.setAppVersionTimeForAPNSRotationIfNeeded(transaction: transaction)
            APNSRotationStore.didReceiveAPNSPush(transaction: transaction)
        }

        // Change the token so its not marked good anymore.
        write { tx in
            preferences.setPushToken("abc", tx: tx)
        }

        read {
            APNSRotationStore.nowMs = { now }
            // We should be eligible for a rotation
            XCTAssert(APNSRotationStore.canRotateAPNSToken(transaction: $0))
        }

        // Insert a message in the past.
        write { transaction in
            self.createIncomingMessage(
                receivedTimestamp: lastPushTime,
                transaction: transaction
            )
        }

        // We shouldn't rotate without unprocessed messages.
        var onMessagesFlushed = APNSRotationStore.rotateIfNeededOnAppLaunchAndReadiness(performRotation: {
            XCTFail("Rotating when we shouldn't!")
        })
        XCTAssertNotNil(onMessagesFlushed)
        onMessagesFlushed?()

        // But if we insert some messages on app launch we should rotate!
        var didRotate = false
        onMessagesFlushed = APNSRotationStore.rotateIfNeededOnAppLaunchAndReadiness(performRotation: {
            didRotate = true
        })
        XCTAssertNotNil(onMessagesFlushed)
        XCTAssertFalse(didRotate)

        // Insert a message.
        write { transaction in
            self.createIncomingMessage(
                receivedTimestamp: now,
                transaction: transaction
            )
        }

        onMessagesFlushed?()

        XCTAssert(didRotate)
    }

    // MARK: - Helpers

    private func createIncomingMessage(
        receivedTimestamp: UInt64,
        transaction: SDSAnyWriteTransaction
    ) {
        let message = self.messageFactory.create(transaction: transaction)
        message.replaceReceived(atTimestamp: receivedTimestamp, transaction: transaction)
    }
}
