"""Defines how to run a single tox environment."""

from __future__ import annotations

import logging
import time
from typing import TYPE_CHECKING, NamedTuple, cast

from tox.execute.api import Outcome, StdinSource
from tox.report import HandledError
from tox.tox_env.errors import Fail, Skip
from tox.tox_env.python.virtual_env.package.pyproject import ToxBackendFailed

if TYPE_CHECKING:
    from pathlib import Path

    from tox.config.types import Command
    from tox.tox_env.api import ToxEnv
    from tox.tox_env.runner import RunToxEnv

LOGGER = logging.getLogger(__name__)


class ToxEnvRunResult(NamedTuple):
    name: str
    skipped: bool
    code: int
    outcomes: list[Outcome]
    duration: float
    ignore_outcome: bool = False
    fail_fast: bool = False
    unavailable: bool = False


def run_one(tox_env: RunToxEnv, no_test: bool, suspend_display: bool) -> ToxEnvRunResult:  # ruff:ignore[boolean-type-hint-positional-argument]
    start_one = time.monotonic()
    name = tox_env.conf.name
    with tox_env.display_context(suspend_display):
        skipped, code, outcomes = _evaluate(tox_env, no_test)
    duration = time.monotonic() - start_one
    return ToxEnvRunResult(
        name, skipped, code, outcomes, duration, tox_env.conf["ignore_outcome"], tox_env.conf["fail_fast"]
    )


def _evaluate(tox_env: RunToxEnv, no_test: bool) -> tuple[bool, int, list[Outcome]]:  # ruff:ignore[boolean-type-hint-positional-argument]
    try:
        return _run_with_teardown(tox_env, no_test)
    except SystemExit as exception:  # setup command fails (interrupted or via invocation)
        return False, cast("int", exception.code), []


def _run_with_teardown(tox_env: RunToxEnv, no_test: bool) -> tuple[bool, int, list[Outcome]]:  # ruff:ignore[boolean-type-hint-positional-argument]
    skipped = False
    code: int = 0
    outcomes: list[Outcome] = []
    try:
        tox_env.setup()
        code, outcomes = run_commands(tox_env, no_test)
    except Skip as exception:
        LOGGER.warning("skipped because %s", exception)
        code = 0
        skipped = True
    except ToxBackendFailed as exception:
        LOGGER.error("%s", exception)  # ruff:ignore[error-instead-of-exception]
        raise SystemExit(exception.code)  # ruff:ignore[raise-without-from-inside-except]
    except Fail as exception:
        LOGGER.error("failed with %s", exception)  # ruff:ignore[error-instead-of-exception]
        code = 1
    except HandledError as exception:
        LOGGER.error("%s", exception)  # ruff:ignore[error-instead-of-exception]
        code = 1
    except Exception:  # pragma: no cover
        LOGGER.exception("internal error")  # pragma: no cover
        code = 2  # pragma: no cover
    finally:
        tox_env.teardown()
    return skipped, code, outcomes


def run_commands(tox_env: RunToxEnv, no_test: bool) -> tuple[int, list[Outcome]]:  # ruff:ignore[boolean-type-hint-positional-argument]
    outcomes: list[Outcome] = []
    if no_test:
        exit_code = Outcome.OK
    else:
        # importing this here to avoid circular import
        from tox.plugin.manager import MANAGER  # ruff:ignore[import-outside-top-level]

        chdir: Path = tox_env.conf["change_dir"]
        chdir.mkdir(exist_ok=True, parents=True)
        ignore_errors: bool = tox_env.conf["ignore_errors"]
        retry_count: int = tox_env.conf["commands_retry"]
        interrupt_post_commands: bool = tox_env.conf["interrupt_post_commands"]
        MANAGER.tox_before_run_commands(tox_env)
        status_pre, status_main, status_post = -1, -1, -1
        try:
            status_pre = run_command_set(tox_env, "commands_pre", chdir, ignore_errors, outcomes, retry_count)
            if status_pre == Outcome.OK or ignore_errors:
                status_main = run_command_set(tox_env, "commands", chdir, ignore_errors, outcomes, retry_count)
            else:
                status_main = Outcome.OK
        finally:
            with tox_env.allow_post_commands_after_interrupt(interrupt_post_commands):
                status_post = run_command_set(tox_env, "commands_post", chdir, ignore_errors, outcomes, retry_count)
            exit_code = status_pre or status_main or status_post  # first non-success
            MANAGER.tox_after_run_commands(tox_env, exit_code, outcomes)
    return exit_code, outcomes


def run_command_set(  # ruff:ignore[too-many-arguments]
    tox_env: ToxEnv,
    key: str,
    cwd: Path,
    ignore_errors: bool,  # ruff:ignore[boolean-type-hint-positional-argument]
    outcomes: list[Outcome],
    retry_count: int = 0,
) -> int:
    exit_code = Outcome.OK
    command_set: list[Command] = tox_env.conf[key]
    for at, cmd in enumerate(command_set):
        max_attempts = 1 if cmd.ignore_exit_code else retry_count + 1
        for attempt in range(1, max_attempts + 1):
            current_outcome = tox_env.execute(
                cmd.args,
                cwd=cwd,
                stdin=StdinSource.USER if getattr(tox_env.options, "no_capture", False) else StdinSource.user_only(),
                show=True,
                run_id=f"{key}[{at}]",
            )
            outcomes.append(current_outcome)
            try:
                if cmd.invert_exit_code:
                    current_outcome.assert_failure()
                else:
                    current_outcome.assert_success()
            except SystemExit as exception:
                if cmd.ignore_exit_code:
                    logging.warning("command failed but is marked ignore outcome so handling it as success")
                    break
                if attempt < max_attempts:
                    logging.warning(
                        "command failed (attempt %d of %d), retrying ...: %s",
                        attempt,
                        max_attempts,
                        cmd.shell,
                    )
                    continue
                if ignore_errors:
                    if exit_code == Outcome.OK:
                        exit_code = cast("int", exception.code)
                    break
                return cast("int", exception.code)
            else:
                break
    return exit_code


__all__ = (
    "ToxEnvRunResult",
    "run_command_set",
    "run_one",
)
