#-----------------------------------------------------------------------------
# Copyright (c) 2012 - 2021, Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------
''' Define Pytest plugins for Jupyter Notebook tests.

'''

#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations

import logging # isort:skip
log = logging.getLogger(__name__)

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
import os
import subprocess
import sys
import time
from os.path import (
    dirname,
    exists,
    join,
    pardir,
)
from typing import IO, Any, Callable

# External imports
import pytest
import requests
from requests.exceptions import ConnectionError

# Bokeh imports
from bokeh.util.terminal import write

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

pytest_plugins = (
    "bokeh._testing.plugins.log_file",
)

__all__ = (
    'jupyter_notebook',
)

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------

@pytest.fixture(scope="session")
def jupyter_notebook(request: pytest.FixtureRequest, log_file: IO[str]) -> str:
    """
    Starts a jupyter notebook server at the beginning of a session, and
    closes at the end of a session.

    Adds custom.js that runs all the cells on notebook opening. Cleans out
    this custom.js at the end of the test run.

    Returns the url that the jupyter notebook is running at.

    """

    # First - set-up the notebooks to run all cells when they're opened
    #
    # Can be cleaned up further to remember the user's existing customJS
    # and then restore it after the test run.
    from jupyter_core import paths
    config_dir = paths.jupyter_config_dir()

    body = """
require(["base/js/namespace", "base/js/events"], function (IPython, events) {
    events.on("kernel_ready.Kernel", function () {
        IPython.notebook.execute_all_cells();
    });
});
"""
    custom = join(config_dir, "custom")
    if not exists(custom):
        os.makedirs(custom)

    customjs = join(custom, "custom.js")

    old_customjs = None
    if exists(customjs):
        with open(customjs) as f:
            old_customjs = f.read()

    with open(customjs, "w") as f:
        f.write(body)

    # Add in the clean-up code
    def clean_up_customjs() -> None:
        text = old_customjs if old_customjs is not None else ""
        with open(customjs, "w") as f:
            f.write(text)

    request.addfinalizer(clean_up_customjs)

    # Second - Run a notebook server at the examples directory
    #

    notebook_port = request.config.option.notebook_port

    env = os.environ.copy()
    env['BOKEH_RESOURCES'] = 'server'

    # Launch from the base directory of bokeh repo
    notebook_dir = join(dirname(__file__), pardir, pardir)

    cmd = ["jupyter", "notebook"]
    argv = ["--no-browser", f"--port={notebook_port}", f"--notebook-dir={notebook_dir}"]
    jupter_notebook_url = f"http://localhost:{notebook_port}"

    try:
        proc = subprocess.Popen(cmd + argv, env=env, stdout=log_file, stderr=log_file)
    except OSError:
        write(f"Failed to run: {' '.join(cmd + argv)}")
        sys.exit(1)
    else:
        # Add in the clean-up code
        def stop_jupyter_notebook() -> None:
            write("Shutting down jupyter-notebook ...")
            proc.kill()

        request.addfinalizer(stop_jupyter_notebook)

        def wait_until(func: Callable[[], Any], timeout: float = 5.0, interval: float = 0.01) -> bool:
            start = time.time()

            while True:
                if func():
                    return True
                if time.time() - start > timeout:
                    return False
                time.sleep(interval)

        def wait_for_jupyter_notebook() -> bool:
            def helper() -> Any:
                if proc.returncode is not None:
                    return True
                try: # type: ignore[unreachable] # XXX: typeshed bug, proc.returncode: int
                    return requests.get(jupter_notebook_url)
                except ConnectionError:
                    return False

            return wait_until(helper)

        if not wait_for_jupyter_notebook():
            write(f"Timeout when running: {' '.join(cmd + argv)}")
            sys.exit(1)

        if proc.returncode is not None:
            write(f"Jupyter notebook exited with code {proc.returncode}")
            sys.exit(1)

        return jupter_notebook_url # type: ignore[unreachable] # XXX: typeshed bug, proc.returncode: int

#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
