"use strict";

const path = require("path");
const fs = require("fs");
const EventEmitter = require("events");

const glob = require("glob");
const WebSocket = require("ws");
const JasmineConsoleReporter = require("jasmine-console-reporter");

const webdriver = require("selenium-webdriver");
const firefox = require("selenium-webdriver/firefox");
const chrome = require("selenium-webdriver/chrome");

// Webdriver shorthands
const { By, until } = webdriver;

const extensionArchivePath = glob.sync("*.xpi", {
    cwd: path.join(__dirname, "../dist/ext"),
    absolute: true
})[0];

if (!fs.existsSync(extensionArchivePath)) {
    console.error("Extension archive not found.");
    process.exit(1);
}

const TEST_PAGE_URL = `file:///${__dirname}/SpecRunner.html`;
const POLLING_PAGE_URL = `file:///${__dirname}/polling.html`;

const firefoxOptions = new firefox.Options()
    .setBinary(firefox.Channel.NIGHTLY)
    .addExtensions(extensionArchivePath)
    .setPreference("xpinstall.signatures.required", false);

const chromeOptions = new chrome.Options().excludeSwitches([
    "disable-background-networking",
    "disable-default-apps"
]);

/**
 * Chrome doesn't load the media router extension immediately
 * and there doesn't seem to be a consistent way of
 * determining when it has loaded.

 * Workaround is to poll every 100ms, refresh the page, and
 * check whether the chrome.cast API objects are defined.
 */
function waitUntilDefined(
    driver,
    pollingTimeout = 10000,
    pollingFrequency = 100
) {
    return new Promise(async (resolve, reject) => {
        let elapsedTime = pollingFrequency;

        // Navigate to polling page
        await driver.get(POLLING_PAGE_URL);

        const interval = setInterval(async () => {
            await driver.navigate().refresh();

            const isDefined = await driver.executeScript(() => {
                return window.chrome.cast !== undefined;
            });

            elapsedTime += pollingFrequency;

            if (isDefined) {
                clearInterval(interval);
                resolve();
            } else if (elapsedTime >= pollingTimeout) {
                clearInterval(interval);
                reject("Timed out");
            }
        }, pollingFrequency);
    });
}

/**
 * Jasmine runs in browser context, but reporters for terminal
 * output run in node context. Need to forward reporter output
 * from Jasmine in the browser context to node context to use
 * reporters here.
 *
 * Creates a WebSocket server. messageProxy.js creates a client
 * socket to this server and facilitates a communication
 * channel.
 */
class MessageProxy extends EventEmitter {
    constructor() {
        super();

        const wss = new WebSocket.Server({
            port: 8080
        });

        wss.on("connection", socket => {
            socket.on("message", message => {
                const messageContent = JSON.parse(message);
                this.emit(messageContent.subject, messageContent.data);
            });
        });
    }
}

/**
 * Ensures cast API is loaded before finding and injecting spec
 * file scripts into the test page.
 */
async function injectSpecs(driver) {
    const [loaded, errorInfo] = await driver.executeAsyncScript(() => {
        const callback = arguments[arguments.length - 1];

        // If already loaded, return immediately
        if (chrome.cast !== undefined) {
            callback([true]);
        }

        // Set Cast API callback
        window.__onGCastApiAvailable = function (...args) {
            callback(args);
        };
    });

    // Return error if API unavailable
    if (!loaded) {
        console.error("Failed to load cast API", errorInfo);
        return;
    }

    // Get spec files
    const specFiles = glob.sync("spec/**/*.spec.js", {
        cwd: __dirname
    });

    // Inject spec files
    for (const specFile of specFiles) {
        await driver.executeScript(`
            const scriptElement = document.createElement("script");
            scriptElement.src = "${specFile}";
            document.head.appendChild(scriptElement);
        `);
    }

    // Trigger Jasmine spec execution
    await driver.executeScript(() => {
        jasmine.getEnv().execute();
    });
}

(async () => {
    const driver = new webdriver.Builder()
        .forBrowser("firefox")
        .setFirefoxOptions(firefoxOptions)
        .setChromeOptions(chromeOptions)
        .build();

    const capabilities = await driver.getCapabilities();
    const browserName = capabilities.get("browserName");

    // Need to wait for cast extension on Chrome
    if (browserName === "chrome") {
        console.log("Waiting for cast extension...");
        await waitUntilDefined(driver);
        console.log("Cast extension loaded!");
    }

    // Create console reporter and message proxy.
    const reporter = new JasmineConsoleReporter();
    const messageProxy = new MessageProxy();

    // Inject specs when ready
    messageProxy.on("injectSpecs", () => {
        injectSpecs(driver);
    });

    /**
     * Forward events from Jasmine standlone via message proxy to
     * console reporter.
     */
    messageProxy.on("jasmineDone", result => reporter.jasmineDone(result));
    messageProxy.on("jasmineStarted", result =>
        reporter.jasmineStarted(result)
    );
    messageProxy.on("specDone", result => reporter.specDone(result));
    messageProxy.on("specStarted", result => reporter.specStarted(result));
    messageProxy.on("suiteDone", result => reporter.suiteDone(result));
    messageProxy.on("suiteStarted", result => reporter.suiteStarted(result));

    // Load Jasmine test page
    driver.get(TEST_PAGE_URL);
})();

// Keep process alive
process.stdin.resume();
