// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation import Quick @testable import SessionUtilitiesKit public class TestDependencies: Dependencies { private var singletonInstances: [Int: Any] = [:] private var cacheInstances: [Int: MutableCacheType] = [:] private var defaultsInstances: [Int: (any UserDefaultsType)] = [:] // MARK: - Subscript Access override public subscript(singleton singleton: SingletonConfig) -> S { return getValueSettingIfNull(singleton: singleton, &singletonInstances) } public subscript(singleton singleton: SingletonConfig) -> S? { get { return (singletonInstances[singleton.key] as? S) } set { singletonInstances[singleton.key] = newValue } } override public subscript(cache cache: CacheConfig) -> I { return getValueSettingIfNull(cache: cache, &cacheInstances) } public subscript(cache cache: CacheConfig) -> M? { get { return (cacheInstances[cache.key] as? M) } set { cacheInstances[cache.key] = newValue.map { cache.mutableInstance($0) } } } override public subscript(defaults defaults: UserDefaultsConfig) -> UserDefaultsType { return getValueSettingIfNull(defaults: defaults, &defaultsInstances) } public subscript(defaults defaults: UserDefaultsConfig) -> UserDefaultsType? { get { return defaultsInstances[defaults.key] } set { defaultsInstances[defaults.key] = newValue } } // MARK: - Timing and Async Handling private var _dateNow: Atomic = Atomic(nil) override public var dateNow: Date { get { (_dateNow.wrappedValue ?? Date()) } set { _dateNow.mutate { $0 = newValue } } } private var _fixedTime: Atomic = Atomic(nil) override public var fixedTime: Int { get { (_fixedTime.wrappedValue ?? 0) } set { _fixedTime.mutate { $0 = newValue } } } public var _forceSynchronous: Bool = false override public var forceSynchronous: Bool { get { _forceSynchronous } set { _forceSynchronous = newValue } } private var asyncExecutions: [Int: [() -> Void]] = [:] // MARK: - Initialization public init(initialState: ((TestDependencies) -> ())? = nil) { super.init() initialState?(self) } // MARK: - Functions override public func async(at timestamp: TimeInterval, closure: @escaping () -> Void) { asyncExecutions.append(closure, toArrayOn: Int(ceil(timestamp))) } @discardableResult override public func mutate( cache: CacheConfig, _ mutation: (inout M) -> R ) -> R { var value: M = ((cacheInstances[cache.key] as? M) ?? cache.createInstance(self)) return mutation(&value) } @discardableResult override public func mutate( cache: CacheConfig, _ mutation: (inout M) throws -> R ) throws -> R { var value: M = ((cacheInstances[cache.key] as? M) ?? cache.createInstance(self)) return try mutation(&value) } public func stepForwardInTime() { let targetTime: Int = ((_fixedTime.wrappedValue ?? 0) + 1) _fixedTime.mutate { $0 = targetTime } if let currentDate: Date = _dateNow.wrappedValue { _dateNow.mutate { $0 = Date(timeIntervalSince1970: currentDate.timeIntervalSince1970 + 1) } } // Run and clear any executions which should run at the target time let targetKeys: [Int] = asyncExecutions.keys .filter { $0 <= targetTime } targetKeys.forEach { key in asyncExecutions[key]?.forEach { $0() } asyncExecutions[key] = nil } } // MARK: - Instance upserting @discardableResult private func getValueSettingIfNull( singleton: SingletonConfig, _ store: inout [Int: Any] ) -> S { guard let value: S = (store[singleton.key] as? S) else { let value: S = singleton.createInstance(self) store[singleton.key] = value return value } return value } @discardableResult private func getValueSettingIfNull( cache: CacheConfig, _ store: inout [Int: MutableCacheType] ) -> I { guard let value: M = (store[cache.key] as? M) else { let value: M = cache.createInstance(self) let mutableInstance: MutableCacheType = cache.mutableInstance(value) store[cache.key] = mutableInstance return cache.immutableInstance(value) } return cache.immutableInstance(value) } @discardableResult private func getValueSettingIfNull( defaults: UserDefaultsConfig, _ store: inout [Int: (any UserDefaultsType)] ) -> UserDefaultsType { guard let value: UserDefaultsType = store[defaults.key] else { let value: UserDefaultsType = defaults.createInstance(self) store[defaults.key] = value return value } return value } } // MARK: - TestState Convenience internal extension TestState { init( wrappedValue: @escaping @autoclosure () -> T?, cache: CacheConfig, in dependencies: @escaping @autoclosure () -> TestDependencies? ) where T: MutableCacheType { self.init(wrappedValue: { let value: T? = wrappedValue() dependencies()![cache: cache] = (value as! M) return value }()) } init( wrappedValue: @escaping @autoclosure () -> T?, singleton: SingletonConfig, in dependencies: @escaping @autoclosure () -> TestDependencies? ) { self.init(wrappedValue: { let value: T? = wrappedValue() dependencies()![singleton: singleton] = (value as! S) return value }()) } init( wrappedValue: @escaping @autoclosure () -> T?, defaults: UserDefaultsConfig, in dependencies: @escaping @autoclosure () -> TestDependencies? ) where T: UserDefaultsType { self.init(wrappedValue: { let value: T? = wrappedValue() dependencies()![defaults: defaults] = value return value }()) } }