"""Module with functions around testing. You can run tests including doctest with generating coverage,
you can generate tests from readme or you can configure tests in conftest with single call."""

from __future__ import annotations
import subprocess
from pathlib import Path
import sys
import warnings

from typing_extensions import Literal
import numpy as np

import mylogging

from .paths import PROJECT_PATHS, validate_path
from . import venvs
from . import misc


def setup_tests(
    generate_readme_tests: bool = True,
    matplotlib_test_backend: bool = False,
    set_numpy_random_seed: int | None = 2,
) -> None:
    """Add paths to be able to import local version of library as well as other test files.

    Value Mylogging.config.COLORIZE = 0 changed globally.

    Note:
        Function expect `tests` folder on root. If not, test folder will not be added to sys path and
        imports from tests will not work.

    Args:
        generate_readme_tests (bool, optional): If True, generete new tests from readme if there are new changes.
            Defaults to True.
        matplotlib_test_backend (bool, optional): If using matlplotlib, it need to be
            closed to continue tests. Change backend to agg. Defaults to False.
        set_numpy_random_seed (int | None): If using numpy random numbers, it will be each time the same.
            Defaults to 2.

    """
    mylogging.config.COLORIZE = False

    PROJECT_PATHS.add_ROOT_PATH_to_sys_path()

    # Find paths and add to sys.path to be able to import local modules
    test_dir_path = PROJECT_PATHS.TEST_PATH

    if test_dir_path.as_posix() not in sys.path:
        sys.path.insert(0, test_dir_path.as_posix())

    if matplotlib_test_backend:
        import matplotlib

        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            matplotlib.use("agg")

    if generate_readme_tests:
        add_readme_tests()

    if set_numpy_random_seed:
        np.random.seed(2)


def run_tests(
    tested_path: str | Path = "infer",
    tests_path: str | Path = "infer",
    test_coverage: bool = True,
    stop_on_first_error: bool = True,
    use_virutalenv: bool = True,
    remove_venv: bool = False,
    requirements: list | str | Path = "infer",
    verbose: Literal[0, 1, 2] = 1,
    extra_args: list = [],
) -> None:
    """Run tests. If any test fails, raise an error.

    Args:
        tested_path (str | Path, optional): If "infer", ROOT_PATH is used. ROOT_PATH is necessary if using doctests
            'Tests' folder not works for doctests in modules. Defaults to None.
        tests_path (str | Path, optional): If "infer", TEST_PATH is used. It means where venv will be stored etc. Defaults to "infer".
        test_coverage (bool, optional): Whether run test coverage plugin. If True, pytest-cov must be installed. Defaults to True.
        stop_on_first_error (bool, optional): Whether stop on first error. Defaults to True.
        use_virutalenv (bool, optional): Whether run new virtualenv and install all libraries from requirements.txt. Defaults to True.
        remove_venv (bool, optional): Whether remove created venv. It's usually not necessary, because packages not in requirements are updated. Defaults to True
        requirements (list | str | Path, optional): If using `use_virutalenv` define what libraries will be installed by path to requirements.txt.
           Can also be a list of more files e.g `["requirements.txt", "requirements_dev.txt"]`. If "infer", autodetected (all requirements). Defaults to "infer".
        verbose (Literal[0, 1, 2], optional): Whether print details on errors or keep silent. If 0, no details, parameters `-q and `--tb=no` are added.
            if 1, some details are added --tb=short. If 2, more details are printed (default --tb=auto)
            Defaults to 1.
        extra_args (list, optional): List of args passed to pytest. Defaults to []

    Raises:
        Exception: If any test fail, it will raise exception (git hook do not continue...).

    Note:
        By default args to quiet mode and no traceback are passed. Usually this just runs automatic tests. If some of them fail,
        it's further analyzed in some other tool in IDE.

    Example:
        >>> run_tests(verbose=2)
    """
    tested_path = PROJECT_PATHS.ROOT_PATH if tested_path == "infer" else validate_path(tested_path)
    tests_path = PROJECT_PATHS.TEST_PATH if tests_path == "infer" else validate_path(tests_path)
    tested_path_str = misc.get_console_str_with_quotes(tested_path)

    if not test_coverage:
        pytest_args = [tested_path_str]
    else:
        pytest_args = [
            tested_path_str,
            "--cov",
            misc.get_console_str_with_quotes(PROJECT_PATHS.APP_PATH),
            "--cov-report",
            "xml:.coverage.xml",
        ]

    if stop_on_first_error:
        extra_args.append("-x")

    if verbose == 0:
        extra_args.append("-q")
        extra_args.append("--tb=no")
    elif verbose == 1:
        extra_args.append("--tb=short")

    complete_args = [
        "pytest",
        *pytest_args,
        *extra_args,
    ]

    test_command = " ".join(complete_args)

    if use_virutalenv:
        my_venv = venvs.MyVenv(PROJECT_PATHS.ROOT_PATH / "venv")
        my_venv.create()
        my_venv.sync_requirements(requirements)

        test_command = f"{my_venv.activate_command} && {test_command}"

    try:
        pytested = subprocess.run(test_command, cwd=tested_path.as_posix())  # , shell=True
    except Exception:
        mylogging.traceback(
            f"Tests failed and did not run. Try this command in terminal to know why it failed.\n{test_command}"
        )
        raise

    if test_coverage and Path(".coverage").exists():
        Path(".coverage").unlink()

    if use_virutalenv and remove_venv:
        my_venv.remove()

    if pytested.returncode != 0:
        raise RuntimeError(mylogging.return_str("Pytest failed"))


def add_readme_tests(readme_path: str | Path = "infer", test_folder_path: str | Path = "infer") -> None:
    """Generate pytest tests script file from README.md and save it to tests folder. Can be called from conftest.

    Args:
        readme_path (str | Path, optional): If 'infer', autodetected (README.md, Readme.md or readme.md on root).
            Defaults to 'infer'.
        test_folder_path (str | Path, optional): If 'infer', autodetected (if ROOT_PATH / tests). Defaults to 'infer.

    Raises:
        FileNotFoundError: If Readme not found.

    Example:
        >>> for i in PROJECT_PATHS.TEST_PATH.glob("*"):
        ...     if i.name.startswith("test_readme_generated"):
        ...         i.unlink()
        ...
        >>> add_readme_tests()
        >>> for i in PROJECT_PATHS.TEST_PATH.glob("*"):
        ...     if i.name.startswith("test_readme_generated"):
        ...         print("Readme tests found.")
        Readme tests found.

    Note:
        Only blocks with python defined syntax will be evaluated. Example:

            ```python
            import numpy
            ```

        If you want to import modules and use some global variables, add `<!--phmdoctest-setup-->` this directive above
        block with setup code.
        If you want to skip some test, add `<!--phmdoctest-mark.skip-->`
    """

    readme_path = PROJECT_PATHS.README_PATH if readme_path == "infer" else validate_path(readme_path)
    test_folder_path = (
        PROJECT_PATHS.TEST_PATH if test_folder_path == "infer" else validate_path(test_folder_path)
    )

    readme_date_modified = str(readme_path.stat().st_mtime).split(".")[0]  # Keep only seconds
    readme_tests_name = f"test_readme_generated-{readme_date_modified}.py"

    test_file_path = test_folder_path / readme_tests_name

    # File not changed from last tests
    if test_file_path.exists():
        return

    for i in test_folder_path.glob("*"):
        if i.name.startswith("test_readme_generated"):
            i.unlink()

    generate_readme_test_command = [
        "phmdoctest",
        readme_path.as_posix(),
        "--outfile",
        test_file_path.as_posix(),
    ]
    try:
        subprocess.run(generate_readme_test_command)
    except Exception:
        mylogging.traceback(
            f"Readme test creation failed. Try \n\n{generate_readme_test_command}\n\n in on your root."
        )
        raise


def deactivate_test_settings() -> None:
    """Sometimess you want to run test just in normal mode (enable plots etc.). Usually at the end of test file in `if __name__ = "__main__":` block."""
    mylogging.config.COLORIZE = True

    if "matplotlib" in sys.modules:

        import matplotlib
        from importlib import reload

        reload(matplotlib)
