//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
import XCTest

import AmplifyTestCommon
@testable import Amplify
@testable import AWSDataStorePlugin
@testable import SQLite

class SQLModelValueConverterTests: BaseDataStoreTests {

    private let testId = "df5dd4a4-34f8-4974-8a37-2617cf8dafe1"

    private let testDateString = "2020-02-02Z"

    private var testDate: Temporal.Date {
        // swiftlint:disable:next force_try
        return try! Temporal.Date(iso8601String: testDateString)
    }

    private var exampleModel: ExampleWithEveryType {
        // non-model type
        let nonModelExample = ExampleNonModelType(someString: "string", someEnum: .foo)

        // example model
        let example = ExampleWithEveryType(
            id: testId,
            stringField: "string",
            intField: 20,
            doubleField: 6.5,
            boolField: true,
            dateField: testDate,
            enumField: .bar,
            nonModelField: nonModelExample,
            arrayOfStringsField: ["foo", "bar"]
        )
        return example
    }

    /// - Given: a `ExampleWithEveryType` model instance
    /// - When:
    ///   - the `sqlValues()` is called
    /// - Then:
    ///   - the result should be a SQLite `[Binding]` with the expected types:
    ///     - enum values must be strings
    ///     - arrays must be strings
    ///     - non-model types must be strings
    ///     - bool must be `Int` (1 or 0)
    ///     - the remaining types should not change
    func testModelWithEveryTypeConversionToBindings() throws {
        let nonModelJSON = "{\"someString\":\"string\",\"someEnum\":\"foo\"}"
        let example = exampleModel

        // convert model to SQLite Bindings
        let bindings = example.sqlValues(modelSchema: example.schema)

        // columns are ordered, so check if the values are correct and in the right index
        XCTAssertEqual(bindings.count, ExampleWithEveryType.schema.columns.count)
        XCTAssertEqual(bindings[0] as? String, example.id) // id
        XCTAssertEqual(bindings[1] as? String, "[\"foo\",\"bar\"]") // arrayOfStringsField
        XCTAssertEqual(bindings[2] as? Int, 1) // boolField
        XCTAssertEqual(bindings[3] as? String, testDateString) // dateField
        XCTAssertEqual(bindings[4] as? Double, 6.5) // doubleField
        XCTAssertEqual(bindings[5] as? String, "bar") // enumField
        XCTAssertEqual(bindings[6] as? Int, 20) // intField

        let jsonBinding = try (bindings[7] as? String)
            .map { Data($0.utf8) }
            .flatMap {
                try JSONSerialization
                    .jsonObject(with: $0, options: []) as? [String: String]
            }
        let expectedJSONBinding = try JSONSerialization
            .jsonObject(with: Data(nonModelJSON.utf8)) as? [String: String]

        XCTAssertEqual(jsonBinding, expectedJSONBinding) // nonModelField
    }

    /// - Given: a `ExampleWithEveryType` model instance
    /// - When:
    ///   - the `Amplify.DataStore.save(example)` is called, followed by a
    ///   `Amplify.DataStore.query(ExampleWithEveryType, byId: id)`
    /// - Then:
    ///   - the returning object must contain exatly the same values as the saved object,
    ///   including correct enums and non-model types
    func testInsertAndSelectExampleWithEveryType() async throws {
        let example = exampleModel

        // save it
        _ = try await Amplify.DataStore.save(exampleModel)
        let result = try await Amplify.DataStore.query(ExampleWithEveryType.self, byId: example.id)
        // then check if the queried version has the correct values
        guard let savedExample = result else {
            XCTFail("ExampleWithEveryType with id \(example.id) not found")
            return
        }
        XCTAssertEqual(savedExample.arrayOfStringsField, example.arrayOfStringsField)
        XCTAssertEqual(savedExample.boolField, example.boolField)
        XCTAssertEqual(savedExample.dateField.iso8601String, example.dateField.iso8601String)
        XCTAssertEqual(savedExample.doubleField, example.doubleField)
        XCTAssertEqual(savedExample.enumField, example.enumField)
        XCTAssertEqual(savedExample.id, example.id)
        XCTAssertEqual(savedExample.intField, example.intField)
        XCTAssertEqual(savedExample.stringField, example.stringField)
        // non-model fields
        XCTAssertEqual(savedExample.nonModelField.someEnum, example.nonModelField.someEnum)
        XCTAssertEqual(savedExample.nonModelField.someString, example.nonModelField.someString)
    }

    /// - Given: a `CommentWithCompositeKey` model as JSONValue
    /// - When:
    ///   - the `sqlValues()` is called
    /// - Then:
    ///   - the returning array of Bindings should include all the `CommentWithCompositeKey` model fields
    ///   plus the identifier value of the associated `PostWithCompositeKey` model
    func testJSONModelConversionToBindings() {
        ModelRegistry.register(modelType: CommentWithCompositeKey.self)
        ModelRegistry.register(modelType: PostWithCompositeKey.self)

        let commentId = "comment-id-123"
        let postId = "post-id-123"
        let postTitle = "post-title"
        let commentContent = "a comment"
        let createdAt = Temporal.DateTime.now().iso8601String
        let updatedAt = Temporal.DateTime.now().iso8601String

        let model: [String: JSONValue] = [
            "id": .string(commentId),
            "content": .string(commentContent),
            "post": .object([
                "id": .string(postId),
                "title": .string(postTitle),
                "__typename": "PostWithCompositeKey"
            ]),
            "createdAt": .string(createdAt),
            "updatedAt": .string(updatedAt),
            "__typename": "CommentWithCompositeKey"
        ]
        let bindings = DynamicModel(id: commentId, values: model).sqlValues(modelSchema: CommentWithCompositeKey.schema)
        let expectedPostIdentifier = PostWithCompositeKey.IdentifierProtocol.identifier(id: postId, title: postTitle)

        XCTAssertEqual(bindings[0] as? String, commentId)
        XCTAssertEqual(bindings[1] as? String, commentContent)
        XCTAssertEqual(bindings[2] as? String, createdAt)
        XCTAssertEqual(bindings[3] as? String, updatedAt)
        XCTAssertEqual(bindings[4] as? String, expectedPostIdentifier.stringValue)

    }

}
