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

import XCTest
import WebKit
@testable import Astro

extension BridgeMessage: Equatable {}

public func ==(lhs: BridgeMessage, rhs: BridgeMessage) -> Bool {
    return lhs.address == rhs.address
        && (lhs.jsonObject as NSDictionary == rhs.jsonObject as NSDictionary)
}

class TestWebBridgeDelegate: WebBridgeDelegate {
    var receivedMessage: BridgeMessage?
    var webBridgeOwner: Addressable {
        return TestAddressable(address: "source")
    }

    func receiveMessageFromBridge(_ message: BridgeMessage) {
        receivedMessage = message
    }
}

class WebBridgeTests: AstroTestCase {
    let webView = UIWebView()
    let wkWebView = WKWebView()

    func setUpUIWebViewBridge(_ test: (WebBridge, XCTestExpectation) -> Void) {
        expectAssertion { expectation in
            let bridge = UIWebViewAdaptor(webView: self.webView)
            guard let js = self.getTestFileString() else {
                XCTAssertTrue(false, "Failed to get test file string")
                return
            }
            self.webView.stringByEvaluatingJavaScript(from: js)
            test(bridge, expectation)
        }
    }

    func setUpWKWebViewBridge(_ test: (WebBridge, XCTestExpectation) -> Void) {
        expectAssertionWithDelay { expectation in
            let bridge = WKWebViewAdaptor(webView: self.wkWebView)
            guard let js = self.getTestFileString() else {
                XCTAssertTrue(false, "Failed to get test file string")
                return
            }
            self.wkWebView.evaluateJavaScript(js, completionHandler: nil)
            test(bridge, expectation)
        }
    }

    func getTestFileString() -> String? {
        var js: String? = nil
        guard let appJsPath = Bundle(for: WebBridgeTests.self).path(forResource: "app", ofType: "js", inDirectory: "web/js") else {
            XCTAssertTrue(false, "Failed to get appJsPath")
            return js
        }

        do {
            js = try String(contentsOfFile: appJsPath, encoding: .utf8)
        } catch let error as NSError {
            XCTAssertFalse(true, "Failed to setup bridge with error \(error)")
        }
        return js
    }

    func testUIWebViewBridgeSendMessageCallsJsReceiveMessage() {
        setUpUIWebViewBridge { bridge, assertionRunExpectation in
            bridge.sendMessage(to: "destination", data: "{ payload: {} }")

            if let receivedMessageJson = self.webView.stringByEvaluatingJavaScript(from: "JSON.stringify({ receivedMessages: testData.receivedMessages })") {
                guard let receivedMessages = JSON.deserialize(receivedMessageJson) else {
                    XCTAssertTrue(false, "couldn't convert string to JSONObject: \(receivedMessageJson)")
                    return
                }
                let expected = ["receivedMessages": [["address": "destination", "data": ["payload": [:]]]]]
                XCTAssertEqual(receivedMessages as NSDictionary, expected as NSDictionary)
                assertionRunExpectation.fulfill()
            }
        }
    }

    func testWKWebViewBridgeSendMessageCallsJsReceiveMessage() {
        setUpWKWebViewBridge { bridge, assertionRunExpectation in
            bridge.sendMessage(to: "destination", data: "{ payload: {} }")

            self.wkWebView.evaluateJavaScript("JSON.stringify({ receivedMessages: testData.receivedMessages })") { (receivedJsonString: Any?, _) in
                guard let json = receivedJsonString as? String else {
                    XCTFail("json string couldn't be converted to String: \(receivedJsonString ?? "No JSON string")")
                    return
                }
                guard let receivedMessages = JSON.deserialize(json) else {
                    XCTFail("couldn't convert string to JSONObject: \(json)")
                    return
                }
                let expected = ["receivedMessages": [["address": "destination", "data": ["payload": [:]]]]]
                XCTAssertEqual(receivedMessages as NSDictionary, expected as NSDictionary)
                assertionRunExpectation.fulfill()
            }
        }
    }

    func testJsFetchMessagesCallsBridgeReceiveMessage() {
        setUpUIWebViewBridge { bridge, assertionRunExpectation in
            let delegate = TestWebBridgeDelegate()
            bridge.webBridgeDelegate = delegate
            bridge.sendMessage(to: "destination", data: "{ payload: {} }")

            self.webView.stringByEvaluatingJavaScript(from: "testData.messagesToFetch.push({address: \"destination\", requestJson: \"{}\"});")

            // The worker cannot be told to load URLs, we will manually instruct it to process messages
            (bridge as! UIWebViewAdaptor).processBridgeMessages()
            self.helper(delegate, expectation: assertionRunExpectation)
        }
    }

    func testWKJsFetchMessagesCallsBridgeReceiveMessage() {
        setUpWKWebViewBridge { bridge, assertionRunExpectation in
            let delegate = TestWebBridgeDelegate()
            bridge.webBridgeDelegate = delegate
            bridge.sendMessage(to: "destination", data: "{ payload: {} }")

            self.wkWebView.evaluateJavaScript("testData.messagesToFetch.push({address: \"destination\", requestJson: \"{}\"});", completionHandler: nil)

            // The worker cannot be told to load URLs, we will manually instruct it to process messages
            (bridge as! WKWebViewAdaptor).processBridgeMessages()
            self.helper(delegate, expectation: assertionRunExpectation)
        }
    }

    func helper(_ delegate: TestWebBridgeDelegate, expectation assertionRunExpectation: XCTestExpectation) {
        let waitDuration: TimeInterval = 5
        let startDate = Date()

        // The WebView loads URLs asynchronously and so we need to drain the run loop
        // while we wait for the delegate method to be called.
        while Date().timeIntervalSince(startDate) < waitDuration && delegate.receivedMessage == nil {
            RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.01))
        }

        if let receivedMessage = delegate.receivedMessage {
            XCTAssertEqual(receivedMessage, BridgeMessage(address: "destination", jsonObject: JSONObject()))
            assertionRunExpectation.fulfill()
        }
    }
}
