//
//  DynamicModelTests.swift
//  CoreStore
//
//  Copyright © 2018 John Rommel Estropia
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in all
//  copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//  SOFTWARE.
//

import XCTest

@testable
import CoreStore

#if os(macOS)
    typealias Color = NSColor

#else
    typealias Color = UIColor

#endif

class Animal: CoreStoreObject {

    @Field.Stored("species")
    var species: String = "Swift"

    @Field.Coded("color", coder: FieldCoders.NSCoding.self)
    var color: Color? = .blue

    @Field.Relationship("master")
    var master: Person?
}

class Dog: Animal {
    
    static let commonNicknames = ["Spot", "Benjie", "Max", "Milo"]

    @Field.Stored(
        "nickname",
        dynamicInitialValue: {
            commonNicknames.randomElement()!
        }
    )
    var nickname: String

    @Field.Stored("age")
    var age: Int = 1

    @Field.Relationship("friends")
    var friends: [Dog]

    @Field.Relationship("friendedBy", inverse: \.$friends)
    var friendedBy: Set<Dog>
}

struct CustomType {
    var string = "customString"
}

enum Job: String, CaseIterable {

    case unemployed
    case engineer
    case doctor
    case lawyer

    init?(data: Data) {

        guard
            let rawValue = String(data: data, encoding: .utf8),
            let value = Self.init(rawValue: rawValue)
            else {

            return nil
        }
        self = value
    }

    func toData() -> Data {

        return Data(self.rawValue.utf8)
    }
}

class Person: CoreStoreObject {

    @Field.Stored(
        "title",
        customSetter: { (object, field, newValue) in
            field.primitiveValue = newValue
            object.$displayName.primitiveValue = nil
        }
    )
    var title: String = "Mr."

    @Field.Stored(
        "name",
        customSetter: { (object, field, newValue) in
            field.primitiveValue = newValue
            object.$displayName.primitiveValue = nil
        }
    )
    var name: String = ""

    @Field.Virtual(
        "displayName",
        customGetter: Person.getDisplayName(_:_:),
        affectedByKeyPaths: Person.keyPathsAffectingDisplayName()
    )
    var displayName: String?

    @Field.Virtual(
        "customType",
        customGetter: { (object, field) in

            if let value = field.primitiveValue {

                return value
            }
            let value = CustomType()
            field.primitiveValue = value
            return value
        }
    )
    var customField: CustomType

    @Field.Coded(
        "job",
        coder: (
            encode: { $0.toData() },
            decode: { $0.flatMap(Job.init(data:)) ?? .unemployed }
        ),
        dynamicInitialValue: {
            Job.allCases.randomElement()!
        }
    )
    var job: Job

    @Field.Relationship("spouse")
    var spouse: Person?

    @Field.Relationship("pets", inverse: \.$master)
    var pets: Set<Animal>

    @Field.Relationship("_spouseInverse", inverse: \.$spouse)
    private var spouseInverse: Person?
    
    @Field.Coded("ownerInfosJson", coder: FieldCoders.Json.self)
    var ownerInfosJson: [OwnerInfo] = []
    
    @Field.Coded("ownerInfosPlist", coder: FieldCoders.Plist.self)
    var ownerInfosPlist: [OwnerInfo] = []
    
    struct OwnerInfo: Codable, Equatable {
        var id: UUID
        var displayName: String?
        var otherIDs: Set<UUID>
    }
    
    private static func getDisplayName(_ object: ObjectProxy<Person>, _ field: ObjectProxy<Person>.FieldProxy<String?>) -> String? {

        if let value = field.primitiveValue {

            return value
        }
        let title = object.$title.value
        let name = object.$name.value
        let value = "\(title) \(name)"
        field.primitiveValue = value
        return value
    }
    
    private static func keyPathsAffectingDisplayName() -> Set<String> {
        
        return [
            String(keyPath: \Person.$title),
            String(keyPath: \Person.$name)
        ]
    }
}


// MARK: - DynamicModelTests

class DynamicModelTests: BaseTestDataTestCase {
    
    @MainActor
    @objc
    dynamic func test_ThatDynamicModels_CanBeDeclaredCorrectly() {
        
        let dataStack = DataStack(
            CoreStoreSchema(
                modelVersion: "V1",
                entities: [
                    Entity<Animal>("Animal"),
                    Entity<Dog>("Dog", indexes: [[\Dog.$nickname, \Dog.$age]]),
                    Entity<Person>("Person")
                ],
                versionLock: [
                    "Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a],
                    "Dog": [0xad6de93adc5565d, 0x7897e51253eba5a3, 0xd12b9ce0b13600f3, 0x5a4827cd794cd15e],
                    "Person": [0xbaec6549a025b59f, 0x3415c71e9f46fcf3, 0xb86b33433cb335eb, 0xfe441fde120dab67]
                ]
            )
        )
        self.prepareStack(dataStack, configurations: [nil]) { (stack) in
            
            let k1 = String(keyPath: \Animal.$species)
            XCTAssertEqual(k1, "species")

            let k2 = String(keyPath: \Dog.$species)
            XCTAssertEqual(k2, "species")
            
            let k3 = String(keyPath: \Dog.$nickname)
            XCTAssertEqual(k3, "nickname")
            
            let updateDone = self.expectation(description: "update-done")
            let fetchDone = self.expectation(description: "fetch-done")
            let willSetPriorObserverDone = self.expectation(description: "willSet-observe-prior-done")
            let willSetNotPriorObserverDone = self.expectation(description: "willSet-observe-notPrior-done")
            let didSetObserverDone = self.expectation(description: "didSet-observe-done")
            
            let ownerInfos: [Person.OwnerInfo] = (1...3).map {
                .init(
                    id: .init(),
                    displayName: "ownerInfo_\($0)",
                    otherIDs: Set((1...3).map({ _ in .init() }))
                )
            }
            stack.perform(
                asynchronous: { (transaction) in
                    
                    let animal = transaction.create(Into<Animal>())
                    XCTAssertEqual(animal.species, "Swift")
                    XCTAssertTrue(type(of: animal.species) == String.self)
                    XCTAssertEqual(animal.color, Color.blue)
                    
                    animal.species = "Sparrow"
                    XCTAssertEqual(animal.species, "Sparrow")
                    
                    animal.color = .yellow
                    XCTAssertEqual(animal.color, Color.yellow)

                    for property in Animal.metaProperties(includeSuperclasses: true) {

                        switch property.keyPath {

                        case String(keyPath: \Animal.$species):
                            XCTAssertTrue(property is FieldContainer<Animal>.Stored<String>)

                        case String(keyPath: \Animal.$master):
                            XCTAssertTrue(property is FieldContainer<Animal>.Relationship<Person?>)

                        case String(keyPath: \Animal.$color):
                            XCTAssertTrue(property is FieldContainer<Animal>.Coded<Color?>)

                        default:
                            XCTFail("Unknown KeyPath: \"\(property.keyPath)\"")
                        }
                    }
                    
                    let dog = transaction.create(Into<Dog>())
                    XCTAssertEqual(dog.species, "Swift")
                    XCTAssertEqual(dog.age, 1)
                    XCTAssertTrue(Dog.commonNicknames.contains(dog.nickname))

                    for property in Dog.metaProperties(includeSuperclasses: true) {

                        switch property.keyPath {

                        case String(keyPath: \Dog.$species):
                            XCTAssertTrue(property is FieldContainer<Animal>.Stored<String>)

                        case String(keyPath: \Dog.$master):
                            XCTAssertTrue(property is FieldContainer<Animal>.Relationship<Person?>)

                        case String(keyPath: \Dog.$color):
                            XCTAssertTrue(property is FieldContainer<Animal>.Coded<Color?>)

                        case String(keyPath: \Dog.$nickname):
                            XCTAssertTrue(property is FieldContainer<Dog>.Stored<String>)

                        case String(keyPath: \Dog.$age):
                            XCTAssertTrue(property is FieldContainer<Dog>.Stored<Int>)

                        case String(keyPath: \Dog.$friends):
                            XCTAssertTrue(property is FieldContainer<Dog>.Relationship<[Dog]>)

                        case String(keyPath: \Dog.$friendedBy):
                            XCTAssertTrue(property is FieldContainer<Dog>.Relationship<Set<Dog>>)

                        default:
                            XCTFail("Unknown KeyPath: \"\(property.keyPath)\"")
                        }
                    }

                    let didSetObserver = dog.observe(\.$species, options: [.new, .old]) { (object, change) in

                        XCTAssertEqual(object, dog)
                        XCTAssertEqual(change.kind, .setting)
                        XCTAssertEqual(change.newValue, "Dog")
                        XCTAssertEqual(change.oldValue, "Swift")
                        XCTAssertFalse(change.isPrior)
                        XCTAssertEqual(object.species, "Dog")
                        didSetObserverDone.fulfill()
                    }
                    let willSetObserver = dog.observe(\.$species, options: [.new, .old, .prior]) { (object, change) in

                        XCTAssertEqual(object, dog)
                        XCTAssertEqual(change.kind, .setting)
                        XCTAssertEqual(change.oldValue, "Swift")

                        if change.isPrior {

                            XCTAssertNil(change.newValue)
                            XCTAssertEqual(object.species, "Swift")
                            willSetPriorObserverDone.fulfill()
                        }
                        else {

                            XCTAssertEqual(change.newValue, "Dog")
                            XCTAssertEqual(object.species, "Dog")
                            willSetNotPriorObserverDone.fulfill()
                        }
                    }
                    
                    dog.species = "Dog"
                    XCTAssertEqual(dog.species, "Dog")

                    didSetObserver.invalidate()
                    willSetObserver.invalidate()
                    
                    dog.nickname = "Spot"
                    XCTAssertEqual(dog.nickname, "Spot")
                    
                    let person = transaction.create(Into<Person>())
                    XCTAssertTrue(person.pets.isEmpty)
                    XCTAssertEqual(person.customField.string, "customString")
                    let initialJob = person.job
                    XCTAssertTrue(Job.allCases.contains(initialJob))
                    
                    XCTAssertEqual(
                        person.rawObject!
                            .runtimeType()
                            .keyPathsForValuesAffectingValue(forKey: "displayName"),
                        ["title", "name"]
                    )
                    
                    person.name = "Joe"
                    
                    XCTAssertEqual(person.rawObject!.value(forKey: "name") as! String?, "Joe")
                    XCTAssertEqual(person.rawObject!.value(forKey: "displayName") as! String?, "Mr. Joe")
                    
                    person.rawObject!.setValue("AAAA", forKey: "displayName")
                    XCTAssertEqual(person.rawObject!.value(forKey: "displayName") as! String?, "AAAA")
                    
                    person.name = "John"
                    XCTAssertEqual(person.name, "John")
                    XCTAssertEqual(person.displayName, "Mr. John") // Custom getter
                    
                    let personSnapshot1 = person.asSnapshot(in: transaction)!
                    XCTAssertEqual(person.name, personSnapshot1.$name)
                    XCTAssertEqual(person.title, personSnapshot1.$title)
                    XCTAssertEqual(person.displayName, personSnapshot1.$displayName)
                    XCTAssertEqual(person.job, personSnapshot1.$job)
                    
                    person.title = "Sir"
                    XCTAssertEqual(person.displayName, "Sir John")
                    
                    XCTAssertEqual(personSnapshot1.$name, "John")
                    XCTAssertEqual(personSnapshot1.$title, "Mr.")
                    XCTAssertEqual(personSnapshot1.$displayName, "Mr. John")

                    person.customField.string = "newCustomString"
                    XCTAssertEqual(person.customField.string, "newCustomString")

                    person.job = .engineer
                    XCTAssertEqual(person.job, .engineer)
                    
                    person.ownerInfosJson = ownerInfos
                    XCTAssertEqual(person.ownerInfosJson, ownerInfos)
                    
                    person.ownerInfosPlist = ownerInfos
                    XCTAssertEqual(person.ownerInfosPlist, ownerInfos)
                    
                    let personSnapshot2 = person.asSnapshot(in: transaction)!
                    XCTAssertEqual(person.name, personSnapshot2.$name)
                    XCTAssertEqual(person.title, personSnapshot2.$title)
                    XCTAssertEqual(person.displayName, personSnapshot2.$displayName)
                    XCTAssertEqual(person.job, personSnapshot2.$job)
                    XCTAssertEqual(person.ownerInfosJson, personSnapshot2.$ownerInfosJson)
                    XCTAssertEqual(person.ownerInfosPlist, personSnapshot2.$ownerInfosPlist)

                    var personSnapshot3 = personSnapshot2
                    personSnapshot3.$name = "James"
                    XCTAssertEqual(personSnapshot1.$name, "John")
                    XCTAssertEqual(personSnapshot1.$displayName, "Mr. John")
                    XCTAssertEqual(personSnapshot1.$job, initialJob)
                    XCTAssertEqual(personSnapshot2.$name, "John")
                    XCTAssertEqual(personSnapshot2.$displayName, "Sir John")
                    XCTAssertEqual(personSnapshot2.$job, .engineer)
                    XCTAssertEqual(personSnapshot3.$name, "James")
                    XCTAssertEqual(personSnapshot3.$displayName, "Sir John")
                    XCTAssertEqual(personSnapshot3.$job, .engineer)
                    XCTAssertEqual(personSnapshot3.$ownerInfosJson, ownerInfos)
                    XCTAssertEqual(personSnapshot3.$ownerInfosPlist, ownerInfos)
                    

                    
                    person.pets.insert(dog)
                    XCTAssertEqual(person.pets.count, 1)
                    XCTAssertEqual(person.pets.first, dog)
                    XCTAssertEqual(person.pets.first?.master, person)
                    XCTAssertEqual(dog.master, person)
                    XCTAssertEqual(dog.master?.pets.first, dog)
                },
                success: { _ in

                    let person = try! stack.fetchOne(From<Person>())
                    XCTAssertNotNil(person)

                    let personPublisher = person!.asPublisher(in: stack)
                    XCTAssertEqual(personPublisher.$name, "John")
                    XCTAssertEqual(personPublisher.$displayName, "Sir John")
                    XCTAssertEqual(personPublisher.$job, .engineer)
                    XCTAssertEqual(personPublisher.$ownerInfosJson, ownerInfos)
                    XCTAssertEqual(personPublisher.$ownerInfosPlist, ownerInfos)

                    updateDone.fulfill()
                },
                failure: { _ in
                    
                    XCTFail()
                }
            )
            stack.perform(
                asynchronous: { (transaction) in
                    
                    let p1 = Where<Animal>({ $0.$species == "Sparrow" })
                    XCTAssertEqual(p1.predicate, NSPredicate(format: "%K == %@", "species", "Sparrow"))
                    
                    let bird = try transaction.fetchOne(From<Animal>(), p1)
                    XCTAssertNotNil(bird)
                    XCTAssertEqual(bird!.species, "Sparrow")
                    XCTAssertEqual(bird!.color, Color.yellow)
                    
                    let p2 = Where<Dog>({ $0.$nickname == "Spot" })
                    XCTAssertEqual(p2.predicate, NSPredicate(format: "%K == %@", "nickname", "Spot"))
                    
                    let dog = try transaction.fetchOne(From<Dog>().where(\.$nickname == "Spot"))
                    XCTAssertNotNil(dog)
                    XCTAssertEqual(dog!.nickname, "Spot")
                    XCTAssertEqual(dog!.species, "Dog")
                    
                    let person = try transaction.fetchOne(From<Person>())
                    XCTAssertNotNil(person)
                    XCTAssertEqual(person!.name, "John")
                    XCTAssertEqual(person!.title, "Sir")
                    XCTAssertEqual(person!.displayName, "Sir John")
                    XCTAssertEqual(person!.customField.string, "customString")
                    XCTAssertEqual(person!.job, .engineer)
                    XCTAssertEqual(person!.pets.first, dog)
                    XCTAssertEqual(person!.ownerInfosJson, ownerInfos)
                    XCTAssertEqual(person!.ownerInfosPlist, ownerInfos)
                    
                    let p3 = Where<Dog>({ $0.$age == 10 })
                    XCTAssertEqual(p3.predicate, NSPredicate(format: "%K == %d", "age", 10))

                    let totalAge = try transaction.queryValue(
                        From<Dog>().select(Int.self, .sum(\.$age))
                    )
                    XCTAssertEqual(totalAge, 1)
                    
                    _ = try transaction.fetchAll(
                        From<Dog>()
                            .where(\Animal.$species == "Dog" && \Dog.$age == 10)
                    )
                    _ = try transaction.fetchAll(
                        From<Dog>()
                            .where(\Dog.$age == 10 && \Animal.$species == "Dog")
                            .orderBy(.ascending({ $0.$species }))
                    )
                    _ = try transaction.fetchAll(
                        From<Dog>(),
                        Where<Dog>({ $0.$age > 10 && $0.$age <= 15 })
                    )
                    _ = try transaction.fetchAll(
                        From<Dog>(),
                        Where<Dog>({ $0.$species == "Dog" && $0.$age == 10 })
                    )
                    _ = try transaction.fetchAll(
                        From<Dog>(),
                        Where<Dog>({ $0.$age == 10 && $0.$species == "Dog" })
                    )
                    _ = try transaction.fetchAll(
                        From<Dog>(),
                        Where<Dog>({ $0.$age > 10 && $0.$age <= 15 })
                    )
                    _ = try transaction.fetchAll(
                        From<Dog>(),
                        (\Dog.$age > 10 && \Dog.$age <= 15)
                    )
                },
                success: { _ in
            
                    fetchDone.fulfill()
                },
                failure: { _ in
                    
                    XCTFail()
                }
            )
        }
        
        self.waitForExpectations(timeout: 10, handler: { _ in })
        
        self.addTeardownBlock {
            dataStack.unsafeRemoveAllPersistentStoresAndWait()
        }
    }
    
    @objc
    dynamic func test_ThatDynamicModelKeyPaths_CanBeCreated() {
        
        XCTAssertEqual(String(keyPath: \Animal.$species), "species")
        XCTAssertEqual(String(keyPath: \Dog.$species), "species")
    }
    
    @nonobjc
    func prepareStack(_ dataStack: DataStack, configurations: [ModelConfiguration] = [nil], _ closure: (_ dataStack: DataStack) -> Void) {
        
        do {
            
            try configurations.forEach { (configuration) in
                
                try dataStack.addStorageAndWait(
                    SQLiteStore(
                        fileURL: SQLiteStore.defaultRootDirectory
                            .appendingPathComponent(UUID().uuidString)
                            .appendingPathComponent("\(Self.self)_\((configuration ?? "-null-")).sqlite"),
                        configuration: configuration,
                        localStorageOptions: .recreateStoreOnModelMismatch
                    )
                )
            }
        }
        catch let error as NSError {
            
            XCTFail(error.coreStoreDumpString)
        }
        closure(dataStack)
    }
}
