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

import Foundation
import SQLite
import XCTest

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

final class StorageEngineTestsPostComment4V2Tests: StorageEngineTestsBase, SharedTestCasesPostComment4V2 {

    override func setUp() {
        super.setUp()
        Amplify.Logging.logLevel = .warn

        let validAPIPluginKey = "MockAPICategoryPlugin"
        let validAuthPluginKey = "MockAuthCategoryPlugin"
        do {
            connection = try Connection(.inMemory)
            storageAdapter = try SQLiteStorageEngineAdapter(connection: connection)
            try storageAdapter.setUp(modelSchemas: StorageEngine.systemModelSchemas)

            syncEngine = MockRemoteSyncEngine()
            storageEngine = StorageEngine(
                storageAdapter: storageAdapter,
                dataStoreConfiguration: .testDefault(),
                syncEngine: syncEngine,
                validAPIPluginKey: validAPIPluginKey,
                validAuthPluginKey: validAuthPluginKey
            )

            ModelListDecoderRegistry.registerDecoder(DataStoreListDecoder.self)
            ModelProviderRegistry.registerDecoder(DataStoreModelDecoder.self)

            ModelRegistry.register(modelType: ParentPost4V2.self)
            ModelRegistry.register(modelType: ChildComment4V2.self)
            do {
                try storageEngine.setUp(modelSchemas: [ParentPost4V2.schema])
                try storageEngine.setUp(modelSchemas: [ChildComment4V2.schema])
            } catch {
                XCTFail("Failed to setup storage engine")
            }
        } catch {
            XCTFail(String(describing: error))
            return
        }
    }

    func testSavePostAndSyncSuccess() async throws {
        let receivedMutationEvent = expectation(description: "Mutation Events submitted to sync engine")
        let expectedSuccess = expectation(description: "Simulated success on mutation event submitted to sync engine")
        let post = ParentPost4V2(
            id: "postId1",
            title: "title1"
        )

        syncEngine.setCallbackOnSubmit { submittedMutationEvent, completion in
            receivedMutationEvent.fulfill()
            if submittedMutationEvent.modelId == post.id {
                expectedSuccess.fulfill()
                completion(.success(submittedMutationEvent))
            } else {
                XCTFail("Unexpected submitted MutationEvent \(submittedMutationEvent)")
                completion(.failure(.internalOperation("mockError", "", nil)))
            }
        }
        try await saveAsync(post)
        await fulfillment(of: [receivedMutationEvent, expectedSuccess], timeout: 1)

    }

    /// A save should fail if the corresponding MutationEvent could not be submitted to the syncEngine.
    func testSavePostFailDueToSyncEngineMissing() async throws {
        storageEngine.syncEngine = nil
        do {
            try await saveAsync(
                ParentPost4V2(
                    id: "postId1",
                    title: "title1"
                ))
            XCTFail("Expected to fail when sync engine is `nil`")
        } catch {
            guard let dataStoreError = error as? DataStoreError else {
                XCTFail("Unexpected type of error \(error)")
                return
            }
            XCTAssertEqual(
                dataStoreError.errorDescription,
                "No SyncEngine available to sync mutation event, rollback save."
            )
        }
    }

    func testSaveCommentThenQueryComment() async throws {
        let comment = ChildComment4V2(content: "content")
        let savedComment = try await saveAsync(comment)
        XCTAssertEqual(savedComment.id, comment.id)

        guard let queriedComment = try await queryAsync(
            ChildComment4V2.self,
            byIdentifier: comment.id,
            eagerLoad: true
        ) else {
            XCTFail("Failed to query saved comment")
            return
        }
        XCTAssertEqual(queriedComment.id, comment.id)
    }

    func testSavePostThenQueryPost() async throws {
        let post = ParentPost4V2(title: "title")
        let savedPost = try await saveAsync(post)
        XCTAssertEqual(savedPost.id, post.id)

        guard let queriedPost = try await queryAsync(
            ParentPost4V2.self,
            byIdentifier: post.id,
            eagerLoad: true
        ) else {
            XCTFail("Failed to query saved post")
            return
        }
        XCTAssertEqual(queriedPost.id, post.id)
    }

    func testSaveMultipleThenQueryComments() async throws {
        try await saveAsync(ChildComment4V2(content: "content"))
        try await saveAsync(ChildComment4V2(content: "content"))

        let comments = try await queryAsync(ChildComment4V2.self, eagerLoad: true)
        XCTAssertEqual(comments.count, 2)
    }

    func testSaveMultipleThenQueryPosts() async throws {
        try await saveAsync(ParentPost4V2(title: "title"))
        try await saveAsync(ParentPost4V2(title: "title"))

        let comments = try await queryAsync(ParentPost4V2.self, eagerLoad: true)
        XCTAssertEqual(comments.count, 2)
    }

    // TODO: clean up this test
    func testCommentWithPost_TranslateToStorageValues() async throws {
        let post = ParentPost4V2(id: "postId", title: "title")
        _ = try await saveAsync(post)
        var comment = ChildComment4V2(content: "content", post: post)

        // Model.sqlValues testing
        let sqlValues = comment.sqlValues(
            for: ChildComment4V2.schema.columns,
            modelSchema: ChildComment4V2.schema
        )
        XCTAssertEqual(sqlValues[0] as? String, comment.id)
        XCTAssertEqual(sqlValues[1] as? String, comment.content)
        XCTAssertNil(sqlValues[2]) // createdAt
        XCTAssertNil(sqlValues[3]) // updatedAt
        XCTAssertEqual(sqlValues[4] as? String, post.id)

        // InsertStatement testing
        let insertStatement = InsertStatement(
            model: comment,
            modelSchema: ChildComment4V2.schema
        )
        XCTAssertEqual(insertStatement.variables[0] as? String, comment.id)
        XCTAssertEqual(insertStatement.variables[1] as? String, comment.content)
        XCTAssertNil(insertStatement.variables[2]) // createdAt
        XCTAssertNil(insertStatement.variables[3]) // updatedAt
        XCTAssertEqual(insertStatement.variables[4] as? String, post.id)
        _ = try connection.prepare(insertStatement.stringValue).run(insertStatement.variables)

        // UpdateStatement testing
        comment.content = "updatedContent"
        let updateStatement = UpdateStatement(
            model: comment,
            modelSchema: ChildComment4V2.schema,
            condition: nil
        )
        _ = try connection.prepare(updateStatement.stringValue).run(updateStatement.variables)


        // Select
        let selectStatement = SelectStatement(
            from: ChildComment4V2.schema,
            predicate: field("id").eq(comment.id),
            sort: nil,
            paginationInput: nil,
            eagerLoad: true
        )
        let rows = try connection.prepare(selectStatement.stringValue).run(selectStatement.variables)
        print(rows)
        let result: [ModelValues] = try rows.convertToModelValues(
            to: ChildComment4V2.self,
            withSchema: ChildComment4V2.schema,
            using: selectStatement
        )
        print(result)
        XCTAssertEqual(result.count, 1)
        // asert content is "updatedContent"
    }

    func testSaveCommentWithPostThenQueryCommentAndAccessPost() async throws {
        let post = ParentPost4V2(title: "title")
        _ = try await saveAsync(post)
        let comment = ChildComment4V2(content: "content", post: post)
        _ = try await saveAsync(comment)

        guard let queriedComment = try await queryAsync(
            ChildComment4V2.self,
            byIdentifier: comment.id,
            eagerLoad: true
        ) else {
            XCTFail("Failed to query saved comment")
            return
        }
        XCTAssertEqual(queriedComment.id, comment.id)
        guard let eagerLoadedPost = queriedComment.post else {
            XCTFail("post should be eager loaded")
            return
        }
        XCTAssertEqual(eagerLoadedPost.id, post.id)
    }

    func testSaveCommentWithPostThenQueryPostAndAccessComments() async throws {
        let post = ParentPost4V2(title: "title")
        _ = try await saveAsync(post)
        let comment = ChildComment4V2(content: "content", post: post)
        _ = try await saveAsync(comment)

        guard let queriedPost = try await queryAsync(
            ParentPost4V2.self,
            byIdentifier: post.id,
            eagerLoad: true
        ) else {
            XCTFail("Failed to query saved post")
            return
        }
        XCTAssertEqual(queriedPost.id, post.id)
        guard let comments = queriedPost.comments else {
            XCTFail("Failed to get comments from queried post")
            return
        }

        switch comments.listProvider.getState() {
        case .notLoaded(let associatedIdentifiers, let associatedFields):
            XCTAssertEqual(associatedIdentifiers, [post.id])
            XCTAssertEqual(associatedFields, ["post"])
        case .loaded(let comments):
            print("loaded comments \(comments)")
            XCTFail("Should not be loaded")
        }
    }

    func testSaveMultipleCommentWithPostThenQueryCommentsAndAccessPost() async throws {
        let post1 = try await saveAsync(ParentPost4V2(id: "postId1", title: "title1"))
        _ = try await saveAsync(ChildComment4V2(id: "id1", content: "content", post: post1))
        let post2 = try await saveAsync(ParentPost4V2(id: "postId2", title: "title2"))
        _ = try await saveAsync(ChildComment4V2(id: "id2", content: "content", post: post2))
        let comments = try await queryAsync(ChildComment4V2.self, eagerLoad: true)
        XCTAssertEqual(comments.count, 2)
        guard let comment1 = comments.first(where: { $0.id == "id1" }) else {
            XCTFail("Couldn't find comment with `id1`")
            return
        }
        guard let comment2 = comments.first(where: { $0.id == "id2" }) else {
            XCTFail("Couldn't find comment with `id2`")
            return
        }
        guard let post1 = comment1.post else {
            XCTFail("missing post on comment1")
            return
        }
        XCTAssertEqual(post1.id, "postId1")
        XCTAssertEqual(post1.title, "title1")
        guard let post2 = comment2.post else {
            XCTFail("missing post on comment2")
            return
        }
        XCTAssertEqual(post2.id, "postId2")
        XCTAssertEqual(post2.title, "title2")
    }

    func testSaveMultipleCommentWithPostThenQueryPostAndAccessComments() async throws {
        let post1 = try await saveAsync(ParentPost4V2(id: "postId1", title: "title1"))
        _ = try await saveAsync(ChildComment4V2(id: "id1", content: "content", post: post1))
        let post2 = try await saveAsync(ParentPost4V2(id: "postId2", title: "title2"))
        _ = try await saveAsync(ChildComment4V2(id: "id2", content: "content", post: post2))
        let posts = try await queryAsync(ParentPost4V2.self, eagerLoad: true)
        XCTAssertEqual(posts.count, 2)
        guard let postId1 = posts.first(where: { $0.id == "postId1" }) else {
            XCTFail("Couldn't find comment with `id1`")
            return
        }
        guard let postId2 = posts.first(where: { $0.id == "postId2" }) else {
            XCTFail("Couldn't find comment with `id2`")
            return
        }
        switch postId1.comments?.listProvider.getState() {
        case .notLoaded(let associatedIdentifiers, let associatedFields):
            XCTAssertEqual(associatedIdentifiers, [postId1.id])
            XCTAssertEqual(associatedFields, ["post"])
        case .loaded:
            XCTFail("Should not be loaded")
        default:
            XCTFail("missing comments")
        }
        switch postId2.comments?.listProvider.getState() {
        case .notLoaded(let associatedIdentifiers, let associatedFields):
            XCTAssertEqual(associatedIdentifiers, [postId2.id])
            XCTAssertEqual(associatedFields, ["post"])
        case .loaded:
            XCTFail("Should not be loaded")
        default:
            XCTFail("missing comments")
        }
    }
}
