""" pytest support functions. """

import os
import threading
import json
import re
import tempfile
import logging
import urllib.request
from urllib.error import URLError
import pytest

from flask import url_for

from vasl_templates.webapp import app
from vasl_templates.webapp.tests import utils
from vasl_templates.webapp.tests.control_tests import ControlTests

FLASK_WEBAPP_PORT = 5011

_pytest_options = None

# ---------------------------------------------------------------------

def pytest_addoption( parser ):
    """Configure pytest options."""

    # NOTE: This file needs to be in the project root for this to work :-/

    # add test options
    parser.addoption(
        "--webapp", action="store", dest="webapp_url", default=None,
        help="Webapp server to test against."
    )
    # NOTE: Chrome seems to be ~15% faster than Firefox, headless ~5% faster than headful.
    parser.addoption(
        "--webdriver", action="store", dest="webdriver", default="chrome",
        help="Webdriver to use."
    )
    parser.addoption(
        "--headless", action="store_true", dest="headless", default=False,
        help="Run the tests headless."
    )
    parser.addoption(
        "--window-size", action="store", dest="window_size", default="1000x700",
        help="Browser window size."
    )

    # add test options
    parser.addoption(
        "--short-tests", action="store_true", dest="short_tests", default=False,
        help="Run a shorter version of the test suite."
    )

    # NOTE: It's not good to have the code run differently to how it will normally,
    # but using the clipboard to retrieve snippets causes more trouble than it's worth :-/
    # since any kind of clipboard activity while the tests are running could cause them to fail
    # (even when running in a VM, if it's configured to share the physical host's clipboard :wall:).
    parser.addoption(
        "--use-clipboard", action="store_true", dest="use_clipboard", default=False,
        help="Use the clipboard to get snippets."
    )

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

def pytest_configure( config ):
    """Called after command-line options have been parsed."""
    global _pytest_options
    _pytest_options = config.option
    import vasl_templates.webapp.tests
    vasl_templates.webapp.tests.pytest_options = config.option

# ---------------------------------------------------------------------

_webapp = None

@pytest.fixture( scope="function" )
def webapp():
    """Launch the webapp."""

    # get the global webapp fixture
    global _webapp
    if _webapp is None:
        _webapp = _make_webapp()

    # reset the remote webapp server
    _webapp.control_tests.start_tests()

    # return the webapp to the caller
    yield _webapp

    # reset the remote webapp server
    _webapp.control_tests.end_tests()

def _make_webapp():
    """Create the global webapp fixture."""

    # initialize
    webapp_url = _pytest_options.webapp_url
    if webapp_url and not webapp_url.startswith( "http://" ):
        webapp_url = "http://" + webapp_url
    app.base_url = webapp_url if webapp_url else "http://localhost:{}".format( FLASK_WEBAPP_PORT )
    _disable_console_logging()

    # initialize
    # WTF?! https://github.com/pallets/flask/issues/824
    def make_webapp_url( endpoint, **kwargs ):
        """Generate a webapp URL."""
        with app.test_request_context():
            # NOTE: When we perform actions at high speed, the notification balloons can build up
            # very quickly, causing problems by obscuring other elements and making them non-clickable :-/
            # We used to explicitly dismiss them, but it's simpler to just always disable them.
            kwargs["store_msgs"] = 1
            # stop the browser from checking for a dirty scenario when leaving the page
            kwargs["disable_close_window_check"] = 1
            # check if the tests are being run headless
            if _pytest_options.headless:
                # yup - there is no clipboard support :-/
                _pytest_options.use_clipboard = False
            # check if we should disable using the clipboard for snippets
            if not _pytest_options.use_clipboard:
                # NOTE: It's not a bad idea to bypass the clipboard, even when running in a browser,
                # to avoid problems if something else uses the clipboard while the tests are running.
                kwargs["store_clipboard"] = 1
            url = url_for( endpoint, _external=True, **kwargs )
            url = url.replace( "http://localhost", app.base_url )
            return url
    app.url_for = make_webapp_url

    # check if we need to start a local webapp server
    if not webapp_url:
        # yup - make it so
        # NOTE: We run the server thread as a daemon so that it won't prevent the tests from finishing
        # when they're done. We used to call $/shutdown after yielding the webapp fixture, but when
        # we changed it from being per-session to per-function, we can no longer do that.
        # This means that the webapp doesn't get a chance to shutdown properly (in particular,
        # clean up the gRPC service), but since we send an EndTests message at the of each test,
        # the remote server gets a chance to clean up then. It's not perfect (e.g. if the tests fail
        # or otherwise finish early before they get a chance to send the EndTests message), but
        # we can live with it.
        thread = threading.Thread(
            target = lambda: app.run( host="0.0.0.0", port=FLASK_WEBAPP_PORT, use_reloader=False ),
            daemon = True
        )
        thread.start()
        # wait for the server to start up
        def is_ready():
            """Try to connect to the webapp server."""
            try:
                url = app.url_for( "ping" )
                with urllib.request.urlopen( url ) as resp:
                    assert resp.read().startswith( b"pong: " )
                return True
            except URLError:
                return False
            except Exception as ex: #pylint: disable=broad-except
                assert False, "Unexpected exception: {}".format(ex)
        utils.wait_for( 5, is_ready )

    # set up control of the remote webapp server
    try:
        url = app.url_for( "get_control_tests" )
        with urllib.request.urlopen( url ) as resp:
            resp_data = json.load( resp )
    except urllib.error.HTTPError as ex:
        if ex.code == 404:
            raise RuntimeError( "Can't get the test control port - has remote test control been enabled?" ) from ex
        raise
    port_no = resp_data.get( "port" )
    if not port_no:
        raise RuntimeError( "The webapp server is not running the test control service." )
    mo = re.search( r"^http://(.+):\d+$", app.base_url )
    addr = "{}:{}".format( mo.group(1), port_no )
    app.control_tests = ControlTests( addr )

    # NOTE: We set the back-end webdriver to be the of the same type (Firefox or Chrome) as the browser
    # being used to drive the tests, which, strictly speaking, doesn't make sense, since the two things
    # don't have anything to do with each other. However, this is a convenient way to switch the backend
    # webdriver's and exercise both of them. The webdriver binary must be on the path, but if it's not,
    # we won't have even got this far, since it needs to be there to drive the browser.
    # NOTE: This will have no effect if we're talking to a remote server, but we can live with that.
    if _pytest_options.webdriver == "firefox":
        app.config[ "WEBDRIVER_PATH" ] = "geckodriver"
    elif _pytest_options.webdriver == "chrome":
        app.config[ "WEBDRIVER_PATH" ] = "chromedriver"

    return app

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

@pytest.fixture( scope="session" )
def test_client():
    """Return a test client that can be used to connect to the webapp."""
    _disable_console_logging()
    return app.test_client()

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

@pytest.fixture( scope="session" )
def webdriver( request ):
    """Return a webdriver that can be used to control a browser.

    It would be nice to be able to drive the embedded browser in the wrapper
    Qt app with e.g. this:
        https://github.com/cisco-open-source/qtwebdriver
    but it only works with the old QtWebKit, which was removed in Qt 5.6 :-/
    """

    # initialize
    driver = request.config.getoption( "--webdriver" )
    from selenium import webdriver as wb
    log_fname = os.path.join( tempfile.gettempdir(), "webdriver-pytest.log" )
    if driver == "firefox":
        options = wb.FirefoxOptions()
        options.headless = _pytest_options.headless
        service = wb.firefox.service.Service( log_path=log_fname )
        driver = wb.Firefox( options=options, service=service )
    elif driver == "chrome":
        options = wb.ChromeOptions()
        options.headless = _pytest_options.headless
        options.add_argument( "--disable-gpu" )
        driver = wb.Chrome( options=options )
    else:
        raise RuntimeError( "Unknown webdriver: {}".format( driver ) )

    # set the browser size
    words = _pytest_options.window_size.split( "x" )
    driver.set_window_size( int(words[0]), int(words[1]) )

    # return the webdriver to the caller
    try:
        yield driver
    finally:
        driver.quit()

# ---------------------------------------------------------------------

def _disable_console_logging():
    """Disable Python logging to the console.

    We do this when running tests because:
    (1) pytest's output is voluminous enough without including our stuff in there as well (and it tends to be
        not that helpful, anyway)
    (2) pytest captures all output and shows it when the test ends i.e. we don't get to see messages in real-time.
    """
    for logger in utils.get_all_loggers():
        # NOTE: FileHandler derives from StreamHandler, and we want to keep those, so we can't use isinstance().
        handlers = [ h for h in logger.handlers if type( h ) is logging.StreamHandler ] #pylint: disable=unidiomatic-typecheck
        for h in handlers:
            logger.removeHandler( h )
