////////////////////////////////////////////////////////////////////////////
//
// Copyright 2015 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

import XCTest
import RealmSwift

private func createStringObjects(_ factor: Int) -> Realm {
    let realm = inMemoryRealm(factor.description)
    try! realm.write {
        for _ in 0..<(1000 * factor) {
            realm.create(SwiftStringObject.self, value: ["a"])
            realm.create(SwiftStringObject.self, value: ["b"])
        }
    }
    return realm
}

private var smallRealm: Realm!
private var mediumRealm: Realm!
private var largeRealm: Realm!

private let isRunningOnDevice = TARGET_IPHONE_SIMULATOR == 0

class SwiftPerformanceTests: TestCase {
    override class var defaultTestSuite: XCTestSuite {
        #if !DEBUG && os(iOS)
            if isRunningOnDevice {
                return super.defaultTestSuite
            }
        #endif
        return XCTestSuite(name: "SwiftPerformanceTests")
    }

    override class func setUp() {
        super.setUp()
        autoreleasepool {
            smallRealm = createStringObjects(1)
            mediumRealm = createStringObjects(5)
            largeRealm = createStringObjects(50)
        }
    }

    override class func tearDown() {
        smallRealm = nil
        mediumRealm = nil
        largeRealm = nil
        super.tearDown()
    }

    override func resetRealmState() {
        // Do nothing, as we need to keep our in-memory realms around between tests
    }

    override func measure(_ block: (() -> Void)) {
        super.measure {
            autoreleasepool {
                block()
            }
        }
    }

    override func measureMetrics(_ metrics: [XCTPerformanceMetric], automaticallyStartMeasuring: Bool, for block: () -> Void) {
        super.measureMetrics(metrics, automaticallyStartMeasuring: automaticallyStartMeasuring) {
            autoreleasepool {
                block()
            }
        }
    }

    func inMeasureBlock(block: () -> Void) {
        measureMetrics(type(of: self).defaultPerformanceMetrics, automaticallyStartMeasuring: false) {
            _ = block()
        }
    }

    private func copyRealmToTestPath(_ realm: Realm) -> Realm {
        do {
            try FileManager.default.removeItem(at: testRealmURL())
        } catch let error as NSError {
            XCTAssertTrue(error.domain == NSCocoaErrorDomain && error.code == 4)
        } catch {
            fatalError("Unexpected error: \(error)")
        }

        try! realm.writeCopy(toFile: testRealmURL())
        return realmWithTestPath()
    }

    func testInsertMultiple() {
        inMeasureBlock {
            let realm = self.realmWithTestPath()
            self.startMeasuring()
            try! realm.write {
                for _ in 0..<5000 {
                    let obj = SwiftStringObject()
                    obj.stringCol = "a"
                    realm.add(obj)
                }
            }
            self.stopMeasuring()
            self.tearDown()
        }
    }

    func testInsertSingleLiteral() {
        inMeasureBlock {
            let realm = self.realmWithTestPath()
            self.startMeasuring()
            for _ in 0..<50 {
                try! realm.write {
                    _ = realm.create(SwiftStringObject.self, value: ["a"])
                }
            }
            self.stopMeasuring()
            self.tearDown()
        }
    }

    func testInsertMultipleLiteral() {
        inMeasureBlock {
            let realm = self.realmWithTestPath()
            self.startMeasuring()
            try! realm.write {
                for _ in 0..<5000 {
                    realm.create(SwiftStringObject.self, value: ["a"])
                }
            }
            self.stopMeasuring()
            self.tearDown()
        }
    }

    func testCountWhereQuery() {
        let realm = copyRealmToTestPath(largeRealm)
        measure {
            for _ in 0..<50 {
                let results = realm.objects(SwiftStringObject.self).filter("stringCol = 'a'")
                _ = results.count
            }
        }
    }

    func testCountWhereTableView() {
        let realm = copyRealmToTestPath(mediumRealm)
        measure {
            for _ in 0..<50 {
                let results = realm.objects(SwiftStringObject.self).filter("stringCol = 'a'")
                _ = results.first
                _ = results.count
            }
        }
    }

    func testEnumerateAndAccessQuery() {
        let realm = copyRealmToTestPath(largeRealm)
        measure {
            for stringObject in realm.objects(SwiftStringObject.self).filter("stringCol = 'a'") {
                _ = stringObject.stringCol
            }
        }
    }

    func testEnumerateAndAccessAll() {
        let realm = copyRealmToTestPath(largeRealm)
        measure {
            for stringObject in realm.objects(SwiftStringObject.self) {
                _ = stringObject.stringCol
            }
        }
    }

    func testEnumerateAndAccessAllSlow() {
        let realm = copyRealmToTestPath(largeRealm)
        measure {
            let results = realm.objects(SwiftStringObject.self)
            for i in 0..<results.count {
                _ = results[i].stringCol
            }
        }
    }

    func testEnumerateAndAccessArrayProperty() {
        let realm = copyRealmToTestPath(largeRealm)
        realm.beginWrite()
        let arrayPropertyObject = realm.create(SwiftArrayPropertyObject.self,
                                                     value: ["name", realm.objects(SwiftStringObject.self).map { $0 } as NSArray, []])
        try! realm.commitWrite()

        measure {
            for stringObject in arrayPropertyObject.array {
                _ = stringObject.stringCol
            }
        }
    }

    func testEnumerateAndAccessArrayPropertySlow() {
        let realm = copyRealmToTestPath(largeRealm)
        realm.beginWrite()
        let arrayPropertyObject = realm.create(SwiftArrayPropertyObject.self,
                                                     value: ["name", realm.objects(SwiftStringObject.self).map { $0 } as NSArray, []])
        try! realm.commitWrite()

        measure {
            let list = arrayPropertyObject.array
            for i in 0..<list.count {
                _ = list[i].stringCol
            }
        }
    }

    func testEnumerateAndMutateAll() {
        let realm = copyRealmToTestPath(largeRealm)
        measure {
            try! realm.write {
                for stringObject in realm.objects(SwiftStringObject.self) {
                    stringObject.stringCol = "c"
                }
            }
        }
    }

    func testEnumerateAndMutateQuery() {
        let realm = copyRealmToTestPath(largeRealm)
        measure {
            try! realm.write {
                for stringObject in realm.objects(SwiftStringObject.self).filter("stringCol != 'b'") {
                    stringObject.stringCol = "c"
                }
            }
        }
    }

    func testDeleteAll() {
        inMeasureBlock {
            let realm = self.copyRealmToTestPath(largeRealm)
            self.startMeasuring()
            try! realm.write {
                realm.delete(realm.objects(SwiftStringObject.self))
            }
            self.stopMeasuring()
        }
    }

    func testQueryDeletion() {
        inMeasureBlock {
            let realm = self.copyRealmToTestPath(mediumRealm)
            self.startMeasuring()
            try! realm.write {
                realm.delete(realm.objects(SwiftStringObject.self).filter("stringCol = 'a' OR stringCol = 'b'"))
            }
            self.stopMeasuring()
        }
    }

    func testManualDeletion() {
        inMeasureBlock {
            let realm = self.copyRealmToTestPath(mediumRealm)
            let objects = realm.objects(SwiftStringObject.self).map { $0 }
            self.startMeasuring()
            try! realm.write {
                realm.delete(objects)
            }
            self.stopMeasuring()
        }
    }

    func testUnindexedStringLookup() {
        let realm = realmWithTestPath()
        try! realm.write {
            for i in 0..<1000 {
                realm.create(SwiftStringObject.self, value: [i.description])
            }
        }
        measure {
            for i in 0..<1000 {
                _ = realm.objects(SwiftStringObject.self).filter("stringCol = %@", i.description).first
            }
        }
    }

    func testIndexedStringLookup() {
        let realm = realmWithTestPath()
        try! realm.write {
            for i in 0..<1000 {
                realm.create(SwiftIndexedPropertiesObject.self, value: [i.description, i])
            }
        }
        measure {
            for i in 0..<1000 {
                _ = realm.objects(SwiftIndexedPropertiesObject.self).filter("stringCol = %@", i.description).first
            }
        }
    }

    func testLargeINQuery() {
        let realm = realmWithTestPath()
        realm.beginWrite()
        var ids = [Int]()
        for i in 0..<10000 {
            realm.create(SwiftIntObject.self, value: [i])
            if i % 2 != 0 {
                ids.append(i)
            }
        }
        try! realm.commitWrite()
        measure {
            _ = realm.objects(SwiftIntObject.self).filter("intCol IN %@", ids).first
        }
    }

    func testSortingAllObjects() {
        let realm = realmWithTestPath()
        try! realm.write {
            for _ in 0..<8000 {
                let randomNumber = Int(arc4random_uniform(UInt32(INT_MAX)))
                realm.create(SwiftIntObject.self, value: [randomNumber])
            }
        }
        measure {
            _ = realm.objects(SwiftIntObject.self).sorted(byKeyPath: "intCol", ascending: true).last
        }
    }

    func testRealmCreationCached() {
        var realm: Realm!
        dispatchSyncNewThread {
            realm = try! Realm()
        }

        measure {
            for _ in 0..<250 {
                autoreleasepool {
                    _ = try! Realm()
                }
            }
        }
        _ = realm.configuration
    }

    func testRealmCreationUncached() {
        measure {
            for _ in 0..<50 {
                autoreleasepool {
                    _ = try! Realm()
                }
            }
        }
    }

    func testCommitWriteTransaction() {
        inMeasureBlock {
            let realm = inMemoryRealm("test")
            realm.beginWrite()
            let object = realm.create(SwiftIntObject.self)
            try! realm.commitWrite()

            self.startMeasuring()
            while object.intCol < 100 {
                try! realm.write { object.intCol += 1 }
            }
            self.stopMeasuring()
        }
    }

    func testCommitWriteTransactionWithLocalNotification() {
        inMeasureBlock {
            let realm = inMemoryRealm("test")
            realm.beginWrite()
            let object = realm.create(SwiftIntObject.self)
            try! realm.commitWrite()

            let token = realm.observe { _, _ in }
            self.startMeasuring()
            while object.intCol < 100 {
                try! realm.write { object.intCol += 1 }
            }
            self.stopMeasuring()
            token.invalidate()
        }
    }

    func testCommitWriteTransactionWithCrossThreadNotification() {
        let stopValue = 100
        inMeasureBlock {
            let realm = inMemoryRealm("test")
            realm.beginWrite()
            let object = realm.create(SwiftIntObject.self)
            try! realm.commitWrite()

            let queue = DispatchQueue(label: "background")
            let semaphore = DispatchSemaphore(value: 0)
            queue.async {
                autoreleasepool {
                    let realm = inMemoryRealm("test")
                    let object = realm.objects(SwiftIntObject.self).first!
                    var token: NotificationToken! = nil
                    CFRunLoopPerformBlock(CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) {
                        token = realm.observe { _, _ in
                            if object.intCol == stopValue {
                                CFRunLoopStop(CFRunLoopGetCurrent())
                            }
                        }
                        semaphore.signal()
                    }
                    CFRunLoopRun()
                    token.invalidate()
                }
            }

            _ = semaphore.wait(timeout: DispatchTime.distantFuture)
            self.startMeasuring()
            while object.intCol < stopValue {
                try! realm.write { object.intCol += 1 }
            }
            queue.sync { }
            self.stopMeasuring()
        }
    }

    func testCrossThreadSyncLatency() {
        let stopValue = 500
        let queue = DispatchQueue(label: "background")
        let semaphore = DispatchSemaphore(value: 0)

        inMeasureBlock {
            let realm = inMemoryRealm("test")
            realm.beginWrite()
            let object = realm.create(SwiftIntObject.self)
            try! realm.commitWrite()

            queue.async {
                autoreleasepool {
                    let realm = inMemoryRealm("test")
                    let object = realm.objects(SwiftIntObject.self).first!
                    var token: NotificationToken! = nil
                    CFRunLoopPerformBlock(CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) {
                        token = realm.observe { _, _ in
                            if object.intCol == stopValue {
                                CFRunLoopStop(CFRunLoopGetCurrent())
                            } else if object.intCol % 2 == 0 {
                                try! realm.write { object.intCol += 1 }
                            }
                        }
                        semaphore.signal()
                    }
                    CFRunLoopRun()
                    token.invalidate()
                }
            }

            let token = realm.observe { _, _ in
                if object.intCol % 2 == 1 && object.intCol < stopValue {
                    try! realm.write { object.intCol += 1 }
                }
            }

            _ = semaphore.wait(timeout: DispatchTime.distantFuture)

            self.startMeasuring()
            try! realm.write { object.intCol += 1 }
            while object.intCol < stopValue {
                #if swift(>=4.2)
                    RunLoop.current.run(mode: RunLoop.Mode.default, before: Date.distantFuture)
                #else
                    RunLoop.current.run(mode: RunLoopMode.defaultRunLoopMode, before: Date.distantFuture)
                #endif
            }
            queue.sync {}
            self.stopMeasuring()
            token.invalidate()
        }
    }

    func testValueForKeyForListObjects() {
        let realm = try! Realm()
        try! realm.write {
            for value in 0..<10000 {
                let listObject = SwiftListOfSwiftObject()
                let object = SwiftObject()
                object.intCol = value
                object.stringCol = String(value)
                listObject.array.append(object)
                realm.add(listObject)
            }
        }
        let objects = realm.objects(SwiftListOfSwiftObject.self)
        measure {
            _ = objects.value(forKeyPath: "array") as! [List<SwiftListOfSwiftObject>]
        }
    }

    func testValueForKeyForIntObjects() {
        let realm = try! Realm()
        try! realm.write {
            for value in 0..<10000 {
                autoreleasepool {
                    let object = SwiftObject()
                    object.intCol = value
                    realm.add(object)
                }
            }
        }
        let objects = realm.objects(SwiftObject.self)
        measure {
            _ = objects.value(forKeyPath: "intCol") as! [Int]
        }
    }

    func testValueForKeyForStringObjects() {
        let realm = try! Realm()
        try! realm.write {
            for value in 0..<10000 {
                autoreleasepool {
                    let object = SwiftObject()
                    object.stringCol = String(value)
                    realm.add(object)
                }
            }
        }
        let objects = realm.objects(SwiftObject.self)
        measure {
            _ = objects.value(forKeyPath: "stringCol") as! [String]
        }
    }

    func testValueForKeyForOptionalIntObjects() {
        let realm = try! Realm()
        try! realm.write {
            for value in 0..<10000 {
                autoreleasepool {
                    let object = SwiftOptionalObject()
                    object.optIntCol.value = value
                    realm.add(object)
                }
            }
        }
        let objects = realm.objects(SwiftOptionalObject.self)
        measure {
            _ = objects.value(forKeyPath: "optIntCol") as! [Int]
        }
    }

    func testValueForKeyForOptionalStringObjects() {
        let realm = try! Realm()
        try! realm.write {
            for value in 0..<10000 {
                autoreleasepool {
                    let object = SwiftOptionalObject()
                    object.optStringCol = String(value)
                    realm.add(object)
                }
            }
        }
        let objects = realm.objects(SwiftOptionalObject.self)
        measure {
            _ = objects.value(forKeyPath: "optStringCol") as! [String]
        }
    }
}