//
//  WebViewPluginTests.swift
//  Astro
//
//  Created by Jeremy Wiebe on 2015-06-04.
//  Copyright (c) 2015 Mobify Research & Development Inc. All rights reserved.
//

import XCTest
import WebKit
@testable import Astro

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

    func capturingCallback(_ rpcMethodResult: RPCMethodResult) {
        switch rpcMethodResult {
        case let .result(value):
            result = value
        case let .error(message):
            error = message
        }
    }

    func assertNoError() {
        XCTAssertNil(error)
    }
}

class TestWebViewAdaptor: AstroWebViewAdaptor {
    @objc var sendMessageCalled = 0

    override func setupNavigationTypeWrapper() { }

    override func sendMessage(to address: MessageAddress, data: String) {
        sendMessageCalled += 1
    }
}

class TestWebViewController: AstroWebViewController {
    @objc var reloadLastKnownURLCallCount = 0
    @objc var saveLastKnownUrlCount = 0

    override func reloadFromLastKnownURL(_ showLoader: Bool = true) {
        reloadLastKnownURLCallCount += 1
    }

    override func saveLastKnownUrl() {
        saveLastKnownUrlCount += 1
    }
}

private class WebViewTestMessageBus: MessageBus {
    var sentMessage: Message?

    override func send(_ message: Message) {
        self.sentMessage = message
    }
}

class MockLoaderPlugin: Plugin, LoaderPlugin {
    @objc var showHandler: ((Bool) -> Void)?
    @objc var hideHandler: ((Bool) -> Void)?
    @objc let viewController = UIViewController()

    internal required init(address: MessageAddress, messageBus: MessageBus, pluginResolver: PluginResolver, options: JSONObject?) {
        super.init(address: address, messageBus: messageBus, pluginResolver: pluginResolver, options: options)
    }

    @objc func show(animated: Bool, completion: ((Bool) -> Void)?) {
        showHandler?(animated)
    }

    @objc func hide(animated: Bool, completion: ((Bool) -> Void)?) {
        hideHandler?(animated)
    }
}

class WebViewPluginTests: AstroTestCase {
    let defaultWebViewPluginAddress = "WebViewPlugin:123"
    var messageBus: MessageBus!
    var pluginResolver: StubPluginResolver!
    var capture: RpcMethodResultCapture!

    override func setUp() {
        messageBus = MessageBus()
        pluginResolver = StubPluginResolver()
        capture = RpcMethodResultCapture()
    }

    func testSetLoaderPlugin() {
        let loaderAddress = "LoaderPlugin:0"
        let loaderPlugin = DefaultLoaderPlugin(address: loaderAddress, messageBus: self.messageBus, pluginResolver: self.pluginResolver, options: nil)
        let webViewPlugin = WebViewPlugin(address: "address", messageBus: messageBus, pluginResolver: pluginResolver, options: nil)

        self.pluginResolver.addPlugin(loaderPlugin)

        webViewPlugin.setLoaderPlugin(loaderAddress) { _ in }

        XCTAssert(webViewPlugin.loaderPlugin === loaderPlugin)
    }

    func testNavigateToWebUrl() {
        let plugin = WebViewPlugin(address: "address", messageBus: messageBus, pluginResolver: pluginResolver, options: nil)

        plugin.navigate("http://www.mobify.com", respond: capture.capturingCallback)

        capture.assertNoError()
    }

    func testNavigateToFileUrl() {
        let plugin = WebViewPlugin(address: "address", messageBus: messageBus, pluginResolver: pluginResolver, options: nil)

        plugin.navigate("file:///web/html/empty.html", respond: capture.capturingCallback)

        capture.assertNoError()
    }

    func testReceiveBridgeMessageToWorkerSucceeds() {
        let receiver = TestReceiver(address: WORKER_ADDRESS)

        let plugin = WebViewPlugin(address: defaultWebViewPluginAddress, messageBus: messageBus, pluginResolver: pluginResolver, options: nil)

        let testRpcJson: JSONObject = [
            "id": 5,
            "payload": [
                "method": "foo",
                "params": ["bar": "baz"]
            ]
        ]

        // Messages to the Application address get through
        messageBus.listen(on: WORKER_ADDRESS, receiver: receiver)
        let testBridgeRpcRequest = BridgeMessage(address: WORKER_ADDRESS, jsonObject: testRpcJson)
        let expectedRpcRequest = WebBridgeUtils.message(from: testBridgeRpcRequest, owner: plugin)!

        plugin.receiveMessageFromBridge(testBridgeRpcRequest)

        guard let message = receiver.receivedMessage else {
            XCTFail("Did not receive a message")
            return
        }
        XCTAssertEqual(message.from, expectedRpcRequest.from)
        XCTAssertEqual(message.to, expectedRpcRequest.to)
        XCTAssertEqual(message.payload as NSDictionary, expectedRpcRequest.payload as NSDictionary)
    }

    func testBridgeMessageToEventsSucceeds() {
        let receiver = TestReceiver(address: WORKER_ADDRESS)

        let plugin = WebViewPlugin(address: defaultWebViewPluginAddress, messageBus: messageBus, pluginResolver: pluginResolver, options: nil)

        let testEventJson = [
            "payload": [
                "eventName": "foo",
                "params": ["bar": "baz"]
            ]
        ]

        // Messages to the events address get through
        messageBus.listen(on: plugin.eventAddress, receiver: receiver)
        let testEventBridgeMessage = BridgeMessage(address: plugin.eventAddress, jsonObject: testEventJson)
        _ = WebBridgeUtils.message(from: testEventBridgeMessage, owner: plugin)

        plugin.receiveMessageFromBridge(testEventBridgeMessage)
    }

    func testBridgeMessageToSelfEventsSucceeds() {
        let receiver = TestReceiver(address: WORKER_ADDRESS)

        let plugin = WebViewPlugin(address: defaultWebViewPluginAddress, messageBus: messageBus, pluginResolver: pluginResolver, options: nil)

        let testEventJson = [
            "payload": [
                "eventName": "foo",
                "params": ["bar": "baz"]
            ]
        ]

        // Messages to the events address get through
        messageBus.listen(on: "self:events", receiver: receiver)
        let testEventBridgeMessage = BridgeMessage(address: plugin.eventAddress, jsonObject: testEventJson)
        _ = WebBridgeUtils.message(from: testEventBridgeMessage, owner: plugin)

        plugin.receiveMessageFromBridge(testEventBridgeMessage)
    }

    func testBridgeEventMessageReceive() {
        let receiver = TestReceiver(address: WORKER_ADDRESS)

        let plugin = WebViewPlugin(address: defaultWebViewPluginAddress, messageBus: messageBus, pluginResolver: pluginResolver, options: nil)
        let testAdaptor = TestWebViewAdaptor()
        testAdaptor.webBridgeDelegate = plugin
        plugin.webViewAdaptor = testAdaptor

        let testEventJson = [
            "payload": [
                "eventName": "foo",
                "params": ["bar": "baz"]
            ]
        ]

        // Messages to the events address get through
        messageBus.listen(on: "self:events", receiver: receiver)
        let testEventMessage = EventMessage(to: plugin.eventAddress, from: plugin.eventAddress, eventName: "TestEvent123", params: testEventJson)
        plugin.receive(testEventMessage)
        XCTAssertEqual(testAdaptor.sendMessageCalled, 1, "sendMessage wasn't called on the adaptor")
    }

    func testBridgeMessageToBadAddressFails() {
        let badAddress = "SomePlugin:321"
        let badReceiver = TestReceiver(address: badAddress)

        let plugin = WebViewPlugin(address: defaultWebViewPluginAddress, messageBus: messageBus, pluginResolver: pluginResolver, options: nil)

        let testRpcJson: JSONObject = [
            "id": 5,
            "payload": [
                "method": "foo",
                "params": ["bar": "baz"]
            ]
        ]

        // Messages to other addresses do not get through
        messageBus.listen(on: badAddress, receiver: badReceiver)
        let testBadBridgeMessage = BridgeMessage(address: badAddress, jsonObject: testRpcJson)
        _ = WebBridgeUtils.message(from: testBadBridgeMessage, owner: plugin)

        plugin.receiveMessageFromBridge(testBadBridgeMessage)

        XCTAssert(badReceiver.receivedMessage == nil)
    }

    func testIsDeallocedWhen_astroWebViewControllerOnPluginRemoved_IsCalled() {
        let controller = TestWebViewController(webViewConfiguration: WKWebViewConfiguration())
        var plugin: WebViewPlugin? = WebViewPlugin(
            address: "Plugin:0",
            messageBus: messageBus,
            pluginResolver: pluginResolver,
            options: ["disposeWhenPopped": true]
        )

        // Allows us to test ARC cleanup
        weak var weakPlugin: WebViewPlugin? = plugin

        plugin?.navigate(with: URLRequest(url: URL(string: "http://www.google.com")!))

        while plugin?.pageTimer == nil {
            RunLoop.current.run(until: Date().addingTimeInterval(0.01))
        }

        plugin?.astroWebViewControllerOnPluginRemoved(controller)

        plugin = nil

        AssertNil(weakPlugin, message: "WebViewPlugin was not dealloc'd! " +
            "There is most likely a retain cycle that needs to be broken " +
            "in astroWebViewControllerOnPluginRemoved:")
    }

    func testWebViewPlugin_WithPageTimeoutTrigger_ShouldFireNavigationFailedAndError() {
        let messageBus = WebViewTestMessageBus()
        let plugin = WebViewPlugin(address: defaultWebViewPluginAddress, messageBus: messageBus, pluginResolver: pluginResolver, options: nil)
        plugin.triggerPageTimeout()
        guard let message = messageBus.sentMessage,
            let params = message.payload["params"] as? NSDictionary,
            let error = params["error"] as? NSDictionary else {
            fatalError("Params or error was not in the message")
        }
        XCTAssertEqual(error["description"] as? String, "Page load timed out")
        XCTAssertEqual(error["code"] as? Int, NSURLErrorTimedOut)
    }

    func testWebViewPlugin_WithNoInternetTrigger_ShouldFireNavigationFailedAndError() {
        let messageBus = WebViewTestMessageBus()
        let plugin = WebViewPlugin(address: defaultWebViewPluginAddress, messageBus: messageBus, pluginResolver: pluginResolver, options: nil)
        plugin.notifyNoInternetConnection("https://www.mobify.com")
        guard let message = messageBus.sentMessage,
            let params = message.payload["params"] as? NSDictionary,
            let error = params["error"] as? NSDictionary else {
                fatalError("Params or error was not in the message")
        }
        XCTAssertEqual(error["description"] as? String, "No internet connection")
        XCTAssertEqual(error["code"] as? Int, NSURLErrorNotConnectedToInternet)
    }

    func testAppendUrlComponentFragment() {
        let plugin = WebViewPlugin(address: "address", messageBus: messageBus, pluginResolver: pluginResolver, options: nil)
        let expectedUrlString = "file://abc.html#b"

        let originalUrl = URL(string: "file://abc.html")!
        let resultUrlString = plugin.appendURLComponent(originalUrl, param: "b", separator: "#").absoluteString
        AssertEqual(resultUrlString, b: expectedUrlString)
    }

    func testAppendUrlComponentFragmentAlreadyPresent() {
        // NSURL only allows one fragment in a URL so we expect the append to fail and the original URL to be returned
        let plugin = WebViewPlugin(address: "address", messageBus: messageBus, pluginResolver: pluginResolver, options: nil)
        let expectedUrlString = "file://abc.html#a"

        let originalUrl = URL(string: "file://abc.html#a")!
        let resultUrlString = plugin.appendURLComponent(originalUrl, param: "b", separator: "#").absoluteString
        AssertEqual(resultUrlString, b: expectedUrlString)
    }

    func testAppendUrlComponentNilParam() {
        let plugin = WebViewPlugin(address: "address", messageBus: messageBus, pluginResolver: pluginResolver, options: nil)
        let expectedUrlString = "file://abc.html"

        let originalUrl = URL(string: "file://abc.html")!
        let resultUrlString = plugin.appendURLComponent(originalUrl, param: nil, separator: "#").absoluteString
        AssertEqual(resultUrlString, b: expectedUrlString)
    }

    func testAppendUrlComponentQuery() {
        let plugin = WebViewPlugin(address: "address", messageBus: messageBus, pluginResolver: pluginResolver, options: nil)
        let expectedUrlString = "file://abc.html?a=1"

        let originalUrl = URL(string: "file://abc.html")!
        let resultUrlString = plugin.appendURLComponent(originalUrl, param: "a=1", separator: "?").absoluteString
        AssertEqual(resultUrlString, b: expectedUrlString)
    }

    func testAppendUrlComponentQueryAlreadyPresent() {
        let plugin = WebViewPlugin(address: "address", messageBus: messageBus, pluginResolver: pluginResolver, options: nil)
        let expectedUrlString = "file://abc.html?a=1?b=2"

        let originalUrl = URL(string: "file://abc.html?a=1")!
        let resultUrlString = plugin.appendURLComponent(originalUrl, param: "b=2", separator: "?").absoluteString
        AssertEqual(resultUrlString, b: expectedUrlString)
    }

    func testDisablingLoader() {
        let webViewPlugin = WebViewPlugin(address: "address", messageBus: messageBus, pluginResolver: pluginResolver, options: nil)
        let mockLoaderAddress = "LoaderPlugin:0"
        let mockLoaderPlugin = MockLoaderPlugin(address: mockLoaderAddress, messageBus: messageBus, pluginResolver: pluginResolver, options: nil)

        self.pluginResolver.addPlugin(mockLoaderPlugin)
        webViewPlugin.setLoaderPlugin(mockLoaderAddress) { _ in }
        XCTAssert(webViewPlugin.loaderPlugin === mockLoaderPlugin)

        // Test disabling loader
        mockLoaderPlugin.showHandler = { animated in
            XCTFail("The loader should not be shown")
        }
        webViewPlugin.disableLoader(capture.capturingCallback)
        webViewPlugin.navigate("http://www.google.com", respond: capture.capturingCallback)

        // Test enabling loader after being disabled
        var showCalled = false
        mockLoaderPlugin.showHandler = { animated in
            showCalled = true
        }
        webViewPlugin.enableLoader(capture.capturingCallback)
        webViewPlugin.navigate("http://www.mobify.com", respond: capture.capturingCallback)
        XCTAssertTrue(showCalled)

        capture.assertNoError()
    }

    func testConfigurationsAreTheSameForAllWebViews() {
        let webViewPlugin = WebViewPlugin(address: "address", messageBus: messageBus, pluginResolver: pluginResolver, options: nil)
        let webViewPlugin2 = WebViewPlugin(address: "address2", messageBus: messageBus, pluginResolver: pluginResolver, options: nil)
        XCTAssertNotEqual(webViewPlugin, webViewPlugin2)

        guard let webView = webViewPlugin.typedViewController?.webView,
            let webView2 = webViewPlugin2.typedViewController?.webView else {
            XCTFail("Webview is not a WKWebView")
            return
        }

        XCTAssertNotEqual(webView, webView2)
        //WKWebView copies the configuration on set 
        //(https://developer.apple.com/reference/webkit/wkwebview/1414979-configuration), 
        //so we check the process pool instead
        XCTAssertEqual(webView.configuration.processPool, webView2.configuration.processPool)
    }
}
