//
//  RpcMethodTests.swift
//  Astro
//
//  Created by Mark Sandstrom on 6/10/15.
//  Copyright (c) 2015 Mobify Research & Development Inc. All rights reserved.
//

import XCTest
@testable import Astro

class CapturedRpcMethodResult {
    var result: Any?
    var error: String?

    var isEmpty: Bool {
        return !(hasError || hasResult)
    }

    var hasError: Bool {
        return error != nil
    }

    var hasResult: Bool {
        return result != nil
    }

    // RPCMethodCallback
    func callback(_ result: RPCMethodResult) {
        switch result {
        case let .result(value):
            self.result = value
        case let .error(error):
            self.error = error
        }
    }

    static func generateSuccessCallback(_ successCallback: @escaping (Any) -> Void) -> (RPCMethodResult) -> Void {
        return { result in
            switch result {
            case let .result(value):
                successCallback(value)
            case let .error(error):
                XCTFail("Expected success, got failure with value \(error)")
            }
        }
    }

    static func generateFailureCallback(_ failureCallback: @escaping (String) -> Void) -> (RPCMethodResult) -> Void {
        return { result in
            switch result {
            case let .result(value):
                XCTFail("Expected failure, got success with value \(value)")
            case let .error(error):
                failureCallback(error)
            }
        }
    }
}

class RpcMethodTests: AstroTestCase {
    var capturedResult: CapturedRpcMethodResult!

    override func setUp() {
        capturedResult = CapturedRpcMethodResult()
    }

    func testAnRpcMethodCanBeCalled() {
        let receiver = BaseMessageReceiver()

        receiver.addRpcMethodShim("method") { _, respond in
            respond(.result("OK"))
        }
        receiver.callRPCMethod("method", params: JSONObject(), respond: capturedResult.callback)

        AssertEqual(capturedResult.result as? String, b: "OK")
    }

    func testAnAsyncRpcMethodCanBeCalled() {
        let receiver = BaseMessageReceiver()

        receiver.addAsyncRpcMethodShim("method") { _, respond in
            respond(.result("OK"))
        }
        receiver.callRPCMethod("method", params: JSONObject(), respond: capturedResult.callback)

        AssertEqual(capturedResult.result as? String, b: "OK")
    }

    func testAnRpcMethodHasADefaultNullResponse() {
        let receiver = BaseMessageReceiver()

        receiver.addRpcMethodShim("method") { _, _ in }
        receiver.callRPCMethod("method", params: JSONObject(), respond: capturedResult.callback)

        AssertEqual(capturedResult.result as? NSNull, b: NSNull())
    }

    func testAnAsyncRpcMethodDoesNotHaveADefaultResponse() {
        let receiver = BaseMessageReceiver()

        receiver.addAsyncRpcMethodShim("method") { _, _ in }
        receiver.callRPCMethod("method", params: JSONObject(), respond: capturedResult.callback)

        XCTAssert(capturedResult.isEmpty)
    }

    func testAnRpcMethodCallThatDoesntMapToARegisteredMethodShimReturnsAnError() {
        let receiver = BaseMessageReceiver()

        receiver.callRPCMethod("nonexistantMethod", params: JSONObject(), respond: capturedResult.callback)

        XCTAssert(capturedResult.hasError)
    }

    func testMethodShimsAreInherited() {
        let inheritedReceiver = TestInheritedShimMessageReceiver()

        inheritedReceiver.callRPCMethod("method1", params: JSONObject()) { _ in }
        inheritedReceiver.callRPCMethod("method2", params: JSONObject()) { _ in }

        XCTAssertTrue(inheritedReceiver.method1Called)
        XCTAssertTrue(inheritedReceiver.method2Called)
    }
}

class MethodShimUtilsTests: AstroTestCase {
    let params: JSONObject = [
        "string": "stringValue",
        "null": NSNull()
    ]
    var capturedResult: CapturedRpcMethodResult!

    override func setUp() {
        capturedResult = CapturedRpcMethodResult()
    }

    func testGetArgDoesNotCallRespondOnSuccess() {
        let arg: String? = MethodShimUtils.getArg(params, key: "string", respond: capturedResult.callback)
        AssertEqual(arg, b: "stringValue")
        XCTAssert(capturedResult.isEmpty)
    }

    func testGetArgRespondsWithAnErrorForAMissingArg() {
        let arg: String? = MethodShimUtils.getArg(params, key: "nonexistent", respond: capturedResult.callback)
        AssertEqual(arg, b: nil)
        XCTAssert(capturedResult.hasError)
    }

    func testGetArgRespondsWithAnErrorForAnIncorrectArgType() {
        let arg: Int? = MethodShimUtils.getArg(params, key: "string", respond: capturedResult.callback)
        AssertEqual(arg, b: nil)
        XCTAssert(capturedResult.hasError)
    }

    func testGetOptionalArgTreatsNSNullAsNil() {
        // Note that when getOptionalArg(_:key:respond:) is used like this:
        //
        //     if let options: JSONObject? = MethodShimInstaller.getOptionalArg(params, key: "options", respond: respond)
        //
        // it returns a doubly-wrapped Optional, i.e. a JSONObject??

        let arg: JSONObject?? = MethodShimUtils.getOptionalArg(params, key: "null", respond: capturedResult.callback)

        // In the example above, the binding of `if let options = ...` should succeed, but the
        // bound value should be nil.
        XCTAssert(arg != nil && arg! == nil)
        XCTAssert(capturedResult.isEmpty)
    }
}
