//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//

@testable import ElementX
import Foundation
import Testing

struct AppLockTimerTests {
    private let now = Date.now
    private var timer: AppLockTimer!
    
    var gracePeriod: TimeInterval {
        timer.gracePeriod
    }
    
    var halfGracePeriod: TimeInterval {
        timer.gracePeriod / 2
    }
    
    var gracePeriodX2: TimeInterval {
        timer.gracePeriod * 2
    }
    
    var gracePeriodX10: TimeInterval {
        timer.gracePeriod * 10
    }
    
    @Test
    mutating func timerLockedOnStartup() {
        setupTimer(unlocked: false)
        #expect(timer.computeLockState(didBecomeActiveAt: now),
                "The app should be locked on a fresh launch.")
        
        setupTimer(unlocked: false)
        #expect(timer.computeLockState(didBecomeActiveAt: now + 1),
                "The app should be locked after a fresh launch.")
        
        setupTimer(unlocked: false)
        #expect(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
                "The app should be locked after a fresh launch.")
        
        setupTimer(unlocked: false)
        #expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
                "The app should be locked after a fresh launch.")
        
        setupTimer(unlocked: false)
        #expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
                "The app should be locked after a fresh launch.")
    }
    
    @Test
    mutating func timerBeforeFirstUnlock() {
        setupTimer(unlocked: false, backgroundedAt: now)
        #expect(timer.computeLockState(didBecomeActiveAt: now),
                "The app should always remain locked after backgrounding when locked.")
        
        setupTimer(unlocked: false, backgroundedAt: now)
        #expect(timer.computeLockState(didBecomeActiveAt: now + 1),
                "The app should always remain locked after backgrounding when locked.")
        
        setupTimer(unlocked: false, backgroundedAt: now)
        #expect(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
                "The app should always remain locked after backgrounding when locked.")
        
        setupTimer(unlocked: false, backgroundedAt: now)
        #expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
                "The app should always remain locked after backgrounding when locked.")
        
        setupTimer(unlocked: false, backgroundedAt: now)
        #expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
                "The app should always remain locked after backgrounding when locked.")
    }
    
    @Test
    mutating func timerWhenUnlocked() {
        setupTimer(unlocked: true, backgroundedAt: now)
        #expect(!timer.computeLockState(didBecomeActiveAt: now + 1),
                "The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
        
        setupTimer(unlocked: true, backgroundedAt: now)
        #expect(!timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
                "The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
        
        setupTimer(unlocked: true, backgroundedAt: now)
        #expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
                "The app should become locked when it was unlocked and backgrounded for more than the grace period.")
        
        setupTimer(unlocked: true, backgroundedAt: now)
        #expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
                "The app should become locked when it was unlocked and backgrounded for more than the grace period.")
    }
    
    @Test
    mutating func timerRepeatingWithinGracePeriod() {
        setupTimer(unlocked: true, backgroundedAt: now)
        
        var nextCheck = now + halfGracePeriod
        #expect(!timer.computeLockState(didBecomeActiveAt: nextCheck),
                "The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
        timer.applicationDidEnterBackground(date: nextCheck)
        
        nextCheck = now + gracePeriod
        #expect(!timer.computeLockState(didBecomeActiveAt: nextCheck),
                "The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
        timer.applicationDidEnterBackground(date: nextCheck)
        
        nextCheck = now + gracePeriod + halfGracePeriod
        #expect(!timer.computeLockState(didBecomeActiveAt: nextCheck),
                "The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
        timer.applicationDidEnterBackground(date: nextCheck)
        
        nextCheck = now + gracePeriodX2
        #expect(!timer.computeLockState(didBecomeActiveAt: nextCheck),
                "The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
        timer.applicationDidEnterBackground(date: nextCheck)
        
        nextCheck = now + gracePeriodX10
        #expect(timer.computeLockState(didBecomeActiveAt: nextCheck),
                "The app should become locked however when finally staying backgrounded for longer than the grace period.")
    }
    
    @Test
    mutating func timerWithLongForeground() {
        setupTimer(unlocked: true)
        
        let backgroundDate = now + gracePeriodX10
        timer.applicationDidEnterBackground(date: backgroundDate)
        
        #expect(!timer.computeLockState(didBecomeActiveAt: backgroundDate + 1),
                "The grace period should be measured from the time the app was backgrounded, and not when it was unlocked.")
    }
    
    @Test
    mutating func changingTimeLocksApp() {
        setupTimer(unlocked: true, backgroundedAt: now)
        #expect(timer.computeLockState(didBecomeActiveAt: now - 1),
                "The the device's clock is changed to before the app was backgrounded, the device should remain locked.")
    }
    
    @Test
    mutating func noGracePeriod() {
        // Given a timer with no grace period that is in the background.
        setupTimer(gracePeriod: 0, unlocked: true)
        let backgroundDate = now + 1
        timer.applicationDidEnterBackground(date: backgroundDate)
        
        // Then the app should be locked immediately.
        #expect(timer.computeLockState(didBecomeActiveAt: backgroundDate))
    }
    
    @Test
    mutating func resignActive() {
        // Given a timer with no grace period.
        setupTimer(gracePeriod: 0, unlocked: true)
        
        // When entering the background.
        timer.applicationDidEnterBackground(date: now)
        
        // Then the app should be locked.
        #expect(timer.computeLockState(didBecomeActiveAt: now + 1))
        
        // When the app resigns active but doesn't enter the background.
        // (Nothing to do here, we just don't call applicationDidEnterBackground).
        
        // Then the app should also remain locked.
        #expect(timer.computeLockState(didBecomeActiveAt: now + 2))
        
        // When unlocking the app and resigning active (but not entering the background)
        timer.registerUnlock()
        // (Again, nothing to do here for resigning active)
        
        // Then the app should not become locked.
        #expect(!timer.computeLockState(didBecomeActiveAt: now + 3))
    }
    
    // MARK: - Helpers
    
    private mutating func setupTimer(gracePeriod: TimeInterval = 180, unlocked: Bool, backgroundedAt backgroundedDate: Date? = nil) {
        let timer = AppLockTimer(gracePeriod: gracePeriod)
        if unlocked {
            timer.registerUnlock()
        }
        if let backgroundedDate {
            timer.applicationDidEnterBackground(date: backgroundedDate)
        }
        self.timer = timer
    }
}
