# coverage run -m pytest -rA tests/test_main.py --no-header --html=./reports/pytest/report.html --self-contained-html && coverage html #noqa E501

"""
    This module tests wox with .toml files, ensuring the expected interactions
    between the toml file and the codebase.
"""

import pytest

from functools import cached_property
from pathlib import Path

from tests.conftest import maintainer_only
from tests.resources.utils import extract_traceback_from_caplog

from wox.cli import main_cli
from wox.cli_control import cli_controller
from wox.constants import ExitCode
from wox.wox_exceptions import WoxException
from wox.wox_steward import WoxSteward

from tests.resources.utils import extract_dict_from_caplog


# python -B -m pytest -rA -k "test_nominal_run"
def test_nominal_run(monkeypatch, tmp_path, patched_wox_command):
    """
        Wox session must return a SUCCESS exit code.
    """

    wox_toml_path = Path('tests/resources/config-files/nominal-run/wox.toml')

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path / '.wox')

    with pytest.raises(SystemExit) as e:
        main_cli()

    assert e.value.code == ExitCode.SUCCESS.value

# python -B -m pytest -rA -k "test_nominal_run_with_uv"
def test_nominal_run_with_uv(monkeypatch, tmp_path, patched_wox_command):
    """
        Wox session must return a SUCCESS exit code.
    """

    wox_toml_path = Path('tests/resources/config-files/nominal-run-with-uv/wox.toml')

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path / '.wox')

    with pytest.raises(SystemExit) as e:
        main_cli()

    assert e.value.code == ExitCode.SUCCESS.value

# python -B -m pytest -rA -k "test_unhandled_exception_caught"
def test_unhandled_exception_caught(monkeypatch, tmp_path, patched_wox_command):
    """
        Wox session must return an Exception that is not a WoxException.

        Notes
        -----
        In pytest, exceptions raised with pytest.raises are wrapped in
        an ExceptionInfo object. The actual exception instance can be
        accessed via `exception.value`.
    """

    wox_toml_path = Path('tests/resources/config-files/nominal-run/wox.toml')

    def patched_main(*args, **kwargs):
        raise ValueError("Boom")

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path / '.wox')
    monkeypatch.setattr('wox.cli_control.main', patched_main)

    with pytest.raises(Exception) as e:
        main_cli()

    assert isinstance(e.value, ValueError)
    assert not isinstance(e.value, WoxException)

# python -B -m pytest -rA -k "test_no_config_file"
def test_no_config_file(cwd_tmp_path):
    """
        Wox session must return a CONFIG_ERROR exit code due to
        missing config file.
    """

    exit_code = cli_controller({})

    assert exit_code == ExitCode.CONFIG_ERROR.value

class TestMissingWoxKeywordError:
    """
        Wox session must return a CONFIG_ERROR exit code due to
        MissingWoxKeywordError.

        The error must be logged.

        Notes
        -----
        `wox_data` is a `cached_property`, which is a descriptor. Descriptors are
        normally bound to the class at class creation time, when Python automatically
        calls their `__set_name__` method.

        When monkeypatching such an attribute after class creation, it is necessary
        to:
        1. replace the entire `cached_property` descriptor, not just its returned
        value;
        2. manually call `__set_name__` on the new descriptor so that it behaves
        correctly (i.e. knows its owning class and attribute name).

        Patching only the return value is insufficient, because a `cached_property`
        stores its computed result in the instance’s `__dict__`, and this cached
        value may already exist.
    """

    wrong_wox_data = [
        # typo in wox_configuration
        {
            'wox_configuratin': {'recreate_envs': True},
            'showcase': {
                'envlist': ['default'],
                'deps': ['cowsay'],
                'commands': ['cowsay -t "Hello Wox!" -c "tux"']
            }
        },
        # typo in envlist called in venv_deletion()
        {
            'wox_configuration': {'recreate_envs': True},
            'showcase': {
                'envlst': ['default'],
                'deps': ['cowsay'],
                'commands': ['cowsay -t "Hello Wox!" -c "tux"']
            }
        },
        # typo in envlist called in main.py
        {
            'wox_configuration': {'recreate_envs': False},
            'showcase': {
                'envlst': ['default'],
                'deps': ['cowsay'],
                'commands': ['cowsay -t "Hello Wox!" -c "tux"']
            }
        }
    ]

    # python -B -m pytest -rA -k "test_missing_wox_keyword_error"
    @pytest.mark.parametrize('wrong_wox_data', wrong_wox_data)
    def test_missing_wox_keyword_error(self, monkeypatch, tmp_path, capsys, wrong_wox_data):

        def patched_wox_data(self):
            return wrong_wox_data

        patched_wox_data_cached_property = cached_property(patched_wox_data)
        patched_wox_data_cached_property.__set_name__(WoxSteward, 'wox_data')

        monkeypatch.setattr(WoxSteward, 'wox_data', patched_wox_data_cached_property)
        monkeypatch.setattr('wox.main.DOT_WOX', tmp_path)

        exit_code = cli_controller({})

        assert exit_code == ExitCode.CONFIG_ERROR.value

        out = capsys.readouterr().out

        assert 'MissingWoxKeywordError' in out

# python -B -m pytest -rA -k "test_wox_session_interrupted"
def test_wox_session_interrupted(monkeypatch):
    """
        Wox session interrupted must return an INTERRUPTED exit code.
    """

    wox_toml_path = Path('tests/resources/config-files/nominal-run/wox.toml')

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)

    def interruption(*args, **kwargs):
        raise KeyboardInterrupt

    monkeypatch.setattr('wox.configurator.configuration_creation', interruption)

    exit_code = cli_controller({})

    assert exit_code == ExitCode.INTERRUPTED.value

# python -B -m pytest -rA -k "test_wox_session_default_setting"
def test_wox_session_default_setting(monkeypatch, caplog):
    """
        'wox' command must only run the tasks listed in wox_session_default.
    """

    wox_toml_path = Path(
        'tests/resources/config-files/wox_session_default-setting/wox.toml'
    )

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)

    def interruption(*args, **kwargs):
        raise KeyboardInterrupt

    monkeypatch.setattr('wox.venv_management.venv_deletion', interruption)

    exit_code = cli_controller({'verbose': True})

    assert exit_code == ExitCode.INTERRUPTED.value

    wox_configuration = extract_dict_from_caplog(caplog, '- wox configuration')

    wox_data = extract_dict_from_caplog(caplog, '- task settings')

    assert wox_configuration['wox_session_default'] == list(wox_data)

# python -B -m pytest -rA -k "test_task_priority_over_wox_session_default_setting"
def test_task_priority_over_wox_session_default_setting(monkeypatch, caplog):
    """
        'wox --task' command must be priority over wox_session_default.
    """

    task = 'doc'

    wox_toml_path = Path(
        'tests/resources/config-files/wox_session_default-setting/wox.toml'
    )

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)

    def interruption(*args, **kwargs):
        raise KeyboardInterrupt

    monkeypatch.setattr('wox.venv_management.venv_deletion', interruption)

    exit_code = cli_controller({'verbose': True, 'task': [task]})

    assert exit_code == ExitCode.INTERRUPTED.value

    wox_configuration = extract_dict_from_caplog(caplog, '- wox configuration')

    wox_data = extract_dict_from_caplog(caplog, '- task settings')

    assert task in wox_data
    assert task not in wox_configuration['wox_session_default']

# python -B -m pytest -rA -k "test_dependency_conflict_in_deps"
def test_dependency_conflict_in_deps(monkeypatch, tmp_path, caplog):
    """
        Wox session must return a RUNTIME_ERROR exit code due to
        DependencyConflictError.
    """

    wox_toml_path = Path(
        'tests/resources/config-files/dependency-conflict-in-deps/wox.toml'
    )

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path)

    exit_code = cli_controller({})

    assert exit_code == ExitCode.RUNTIME_ERROR.value

    log_record_message = caplog.records[-1].msg

    assert 'DependencyConflictError' == log_record_message.type

# python -B -m pytest -rA -k "test_command_not_found"
def test_command_not_found(monkeypatch, tmp_path, caplog):
    """
        Wox session must return a RUNTIME_ERROR exit code due to
        CommandNotFoundError.
    """

    wox_toml_path = Path('tests/resources/config-files/command-not-found/wox.toml')

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path)

    exit_code = cli_controller({})

    assert exit_code == ExitCode.RUNTIME_ERROR.value

    log_record_message = caplog.records[-1].msg

    assert 'CommandNotFoundError' == log_record_message.type

# python -B -m pytest -rA -k "test_python_interpreter_call"
def test_python_interpreter_call(monkeypatch, tmp_path, caplog):
    """
        Wox session must return a RUNTIME_ERROR exit code due to
        PythonInterpreterCallError.
    """

    wox_toml_path = Path(
        'tests/resources/config-files/python-interpreter-call/wox.toml'
    )

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path)

    exit_code = cli_controller({})

    assert exit_code == ExitCode.RUNTIME_ERROR.value

    log_record_message = caplog.records[-1].msg

    assert 'PythonInterpreterCallError' == log_record_message.type

# python -B -m pytest -rA -k "test_python_not_found"
def test_python_not_found(monkeypatch, tmp_path, caplog):
    """
        Wox session must return a RUNTIME_ERROR exit code due to
        PythonInterpreterNotFoundError.
    """

    wox_toml_path = Path('tests/resources/config-files/python-not-found/wox.toml')

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path)

    exit_code = cli_controller({})

    assert exit_code == ExitCode.RUNTIME_ERROR.value

    log_record_message = caplog.records[-1].msg

    assert 'PythonInterpreterNotFoundError' == log_record_message.type

# python -B -m pytest -rA -k "test_python_build_keyword"
@maintainer_only
def test_python_build_keyword(monkeypatch, tmp_path, caplog, subtests):
    """
        Wox must discover Python interpreter for versions 3.10, 3.12, 3.13 and 3.14 and
        use the python_build provided for versions 3.11 and 3.13.
    """

    expected_logs = [
        "Looking for an interpreter version 3.10",
        "Use of specified python build '~/python313/bin/python3'",
        "Looking for an interpreter version 3.12",
        "Use of specified python build '~/python311/bin/python3'"
    ]

    caplog.set_level('DEBUG')

    wox_toml_path = Path('tests/resources/config-files/python_build-keyword/wox.toml')

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path)

    exit_code = cli_controller({'verbose': True})

    assert exit_code == ExitCode.SUCCESS.value

    for log in expected_logs:
        with subtests.test(log = log):
            assert log in caplog.text

# python -B -m pytest -rA -k "test_python_build_typo"
def test_python_build_typo(monkeypatch, tmp_path, caplog):
    """
        Wox must return PythonBuildNotFoundError if there is a typo
        in python_build setting in .toml file.
    """

    wox_toml_path = Path('tests/resources/config-files/python_build-typo/wox.toml')

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path)

    exit_code = cli_controller({'verbose': True})

    assert exit_code == ExitCode.RUNTIME_ERROR.value

    log_record_message = caplog.records[-1].msg

    assert 'PythonBuildNotFoundError' == log_record_message.type

# python -B -m pytest -rA -k "test_subprocess_traceback_display"
def test_subprocess_traceback_display(monkeypatch, tmp_path, caplog):
    """
        If failing, the subprocess must show a traceback as it would
        be shown in the terminal.
    """

    expected_lines_in_traceback = [
        'Traceback (most recent call last):',
        "NameError: name 'a' is not defined"
    ]

    wox_toml_path = Path('tests/resources/config-files/traceback-display/wox.toml')

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path)

    exit_code = cli_controller({})

    assert exit_code == ExitCode.RUNTIME_ERROR.value

    traceback_from_logs = extract_traceback_from_caplog(caplog.text)

    lines_in_traceback = []

    for line in expected_lines_in_traceback:
        if line in traceback_from_logs:
            lines_in_traceback.append(line)

    assert lines_in_traceback == expected_lines_in_traceback

# python -B -m pytest -rA -k "test_wrong_toml_file"
def test_wrong_toml_file(monkeypatch, tmp_path, capsys):
    """
        Wox must return PythonInterpreterNotFoundError if there is a typo
        in python_build setting in .toml file.
    """

    wox_toml_path = Path('tests/resources/config-files/wrong-toml-file/wox.toml')

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path)

    exit_code = cli_controller({'verbose': True})

    assert exit_code == ExitCode.CONFIG_ERROR.value

    out = capsys.readouterr().out

    assert 'TOMLDecodeError' in out

# python -B -m pytest -rA -k "test_unstable_python_py36_with_uv"
@maintainer_only
def test_unstable_python_py36_with_uv(monkeypatch, tmp_path, caplog):
    """
        Wox with Python 3.6 and ``with_uv`` setting declared must use venv
        for environment creation and pip for packages installation.

        OR

        Wox running with a corrupted Python must throw PythonInterpreterUnstableError.
    """

    wox_toml_path = Path('tests/resources/config-files/python-unstable/wox.toml')

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path)

    exit_code = cli_controller({'verbose': True})

    if exit_code == ExitCode.SUCCESS.value:

        assert exit_code == ExitCode.SUCCESS.value

    elif exit_code == ExitCode.RUNTIME_ERROR.value:

        log_record_message = caplog.records[-1].msg

        assert 'PythonInterpreterUnstableError' == log_record_message.type

# python -B -m pytest -rA -k "test_no_task_in_toml_file"
def test_no_task_in_toml_file(monkeypatch, tmp_path, capsys):
    """
        Wox must return NoTaskError if there is no tasks in .toml file.
    """

    wox_toml_path = Path('tests/resources/config-files/no-task-in-toml/wox.toml')

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path)

    exit_code = cli_controller({})

    assert exit_code == ExitCode.CONFIG_ERROR.value

    out = capsys.readouterr().out

    assert 'NoTaskError' in out

# python -B -m pytest -rA -k "test_no_python_venv_warning"
def test_no_python_venv_warning(monkeypatch, tmp_path, caplog):
    """
        If an environment is declared in an envlist and does not have pyvenv.cfg,
        wox must consider it as a non-Python environment and warn the user
        thanks to NotPythonVenvWarning.
    """

    env_dir = tmp_path / 'project'
    env_dir.mkdir()

    wox_toml_path = Path('tests/resources/config-files/not-python-venv-warning/wox.toml')

    monkeypatch.setattr(WoxSteward, 'wox_toml', wox_toml_path)
    monkeypatch.setattr('wox.main.DOT_WOX', tmp_path)

    exit_code = cli_controller({})

    assert exit_code == ExitCode.SUCCESS.value

    assert 'NotPythonVenvWarning' in caplog.text
