# -*- coding: utf-8 eval: (blacken-mode 1) -*-
#
# April 22 2022, Christian Hopps <chopps@gmail.com>
#
# Copyright (c) 2022, LabN Consulting, L.L.C
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; see the file COPYING; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
"""A module that implements pytest fixtures.

To use in your project, in your conftest.py add:

  from munet.testing.fixtures import *
"""
import asyncio
import logging
import os

from pathlib import Path

import pytest
import pytest_asyncio

from munet.base import Bridge
from munet.cleanup import cleanup_current
from munet.cleanup import cleanup_previous
from munet.native import L3Node
from munet.parser import async_build_topology
from munet.parser import get_config
from munet.testing.util import async_pause_test
from munet.testing.util import pause_test


def _cd_to_test_dir(request, tag):
    cwd = os.getcwd()
    sdir = os.path.dirname(os.path.realpath(request.fspath))
    logging.debug("conftest: %s: changing cwd from %s to %s", tag, cwd, sdir)
    os.chdir(sdir)

    yield

    os.chdir(cwd)


def get_test_logdir(nodeid=None, module=False):
    """Get log directory relative pathname."""
    xdist_worker = os.getenv("PYTEST_XDIST_WORKER", "")
    mode = os.getenv("PYTEST_XDIST_MODE", "no")

    # nodeid: all_protocol_startup/test_all_protocol_startup.py::test_router_running
    if not nodeid:
        nodeid = os.environ["PYTEST_CURRENT_TEST"].split(" ")[0]
    cur_test = nodeid.replace("[", "_").replace("]", "_")
    path, testname = cur_test.split("::")
    path = path[:-3].replace("/", ".")

    # We use different logdir paths based on how xdist is running.
    if mode == "each":
        return os.path.join(path, testname, xdist_worker)
    if mode == "load":
        return os.path.join(path, testname)
    assert mode in ("no", "loadfile", "loadscope"), f"Unknown dist mode {mode}"
    return path if module else os.path.join(path, testname)


# =================
# Sessions Fixtures
# =================


@pytest.fixture(autouse=True, scope="session")
def session_autouse():
    if "PYTEST_TOPOTEST_WORKER" not in os.environ:
        is_worker = False
    elif not os.environ["PYTEST_TOPOTEST_WORKER"]:
        is_worker = False
    else:
        is_worker = True

    if not is_worker:
        # This is unfriendly to multi-instance
        cleanup_previous()

    yield

    if not is_worker:
        cleanup_current()


# ===============
# Module Fixtures
# ===============


@pytest.fixture(autouse=True, scope="module")
def module_autouse(request):
    # _cd_to_test_dir(request, "module")
    cwd = os.getcwd()
    sdir = os.path.dirname(os.path.realpath(request.fspath))
    logging.debug("conftest: module: changing cwd from %s to %s", cwd, sdir)
    os.chdir(sdir)

    yield

    os.chdir(cwd)


@pytest.fixture(scope="module")
def event_loop():
    """Create an instance of the default event loop for the session."""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    logging.debug("conftest: got event loop")
    yield loop
    loop.close()


@pytest.fixture(scope="module")
def rundir_module():
    d = os.path.join("/tmp/unet-test", get_test_logdir(module=True))
    logging.debug("conftest: test module rundir %s", d)
    return d


async def _unet_impl(_rundir, _pytestconfig, param=None):
    try:
        _unet = await async_build_topology(
            config=get_config(basename=param) if param else None,
            rundir=_rundir,
            pytestconfig=_pytestconfig,
        )
    except Exception as error:
        logging.debug("unet fixture: unet build failed: %s", error, exc_info=True)
        pytest.skip(
            f"unet fixture: unet build failed: {error}", allow_module_level=True
        )
        raise

    try:
        tasks = await _unet.run()
    except Exception as error:
        logging.debug("unet fixture: unet run failed: %s", error, exc_info=True)
        await _unet.async_delete()
        pytest.skip(f"unet fixture: unet run failed: {error}", allow_module_level=True)
        raise

    logging.debug("unet fixture: containers running")

    # Pytest is supposed to always return even if exceptions
    try:
        yield _unet
    except Exception as error:
        logging.error("unet fixture: yield unet unexpected exception: %s", error)

    await _unet.async_delete()

    # No one ever awaits these so cancel them
    logging.debug("unet fixture: cleanup")
    for task in tasks:
        task.cancel()

    # Reset the class variables so auto number is predictable
    logging.debug("unet fixture: resetting ords to 1")
    L3Node.next_ord = 1
    Bridge.next_ord = 1


@pytest.fixture(scope="module")
async def unet(request, rundir_module, pytestconfig):  # pylint: disable=W0621
    param = request.param if hasattr(request, "param") else None
    async for x in _unet_impl(rundir_module, pytestconfig, param):
        yield x


# =================
# Function Fixtures
# =================


@pytest.fixture(autouse=True, scope="function")
def function_autouse(request):
    # u_cd_to_test_dir(request, "function")
    cwd = os.getcwd()
    sdir = os.path.dirname(os.path.realpath(request.fspath))
    logging.debug("conftest: function: changing cwd from %s to %s", cwd, sdir)
    os.chdir(sdir)

    yield

    os.chdir(cwd)


@pytest.fixture(scope="function")
def stepf(pytestconfig):
    class Stepnum:
        "Track the stepnum in closure"
        num = 0

        def inc(self):
            self.num += 1

    pause = pytestconfig.getoption("pause")
    stepnum = Stepnum()

    def stepfunction(desc=""):
        desc = f": {desc}" if desc else ""
        if pause:
            pause_test(f"before step {stepnum.num}{desc}")
        logging.info("STEP %s%s", stepnum.num, desc)
        stepnum.inc()

    return stepfunction


@pytest_asyncio.fixture(scope="function")
async def astepf(pytestconfig):
    class Stepnum:
        "Track the stepnum in closure"
        num = 0

        def inc(self):
            self.num += 1

    pause = pytestconfig.getoption("pause")
    stepnum = Stepnum()

    async def stepfunction(desc=""):
        desc = f": {desc}" if desc else ""
        if pause:
            await async_pause_test(f"before step {stepnum.num}{desc}")
        logging.info("STEP %s%s", stepnum.num, desc)
        stepnum.inc()

    return stepfunction


@pytest.fixture(scope="function")
def rundir():
    d = os.path.join("/tmp/unet-test", get_test_logdir(module=False))
    logging.debug("conftest: test function rundir %s", d)
    return d


# Configure logging
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_setup(item):
    d = os.path.join(
        "/tmp/unet-test", get_test_logdir(nodeid=item.nodeid, module=False)
    )
    config = item.config
    logging_plugin = config.pluginmanager.get_plugin("logging-plugin")
    filename = Path(d, "pytest-exec.log")
    logging_plugin.set_log_path(str(filename))
    logging.debug("conftest: test function setup: rundir %s", d)
    yield


@pytest.fixture
async def unet_perfunc(request, rundir, pytestconfig):  # pylint: disable=W0621
    """
    Build unet per test function with an optional topology basename parameter

    The fixture can be parameterized to choose different config files.
    For example, use as follows to run the test with unet_perfunc configured
    first with a config file named `cfg1.yaml` then with config file `cfg2.yaml`
    (where the actual files could end with `json` or `toml` rather than `yaml`).

        @pytest.mark.parametrize(
            "unet_perfunc", ["cfg1", "cfg2]", indirect=["unet_perfunc"]
        )
        def test_example(unet_perfunc)
    """
    param = request.param if hasattr(request, "param") else None
    async for x in _unet_impl(rundir, pytestconfig, param):
        yield x
