"""Helper for ``tdd_tag_validation.robot`` — exercises TDD issue-capture tags.

Behave-side sub-commands (``validate_tags_*``, ``should_invert_*``,
``inversion_*``) test the logic in ``features/environment.py``.
Robot-side sub-commands run fixture ``.robot`` files as sub-processes
with the ``tdd_expected_fail_listener`` and inspect the output XML.

Exit 0 = check passed, 1 = unexpected outcome.
See CONTRIBUTING.md > TDD Issue Test Tags.
"""

from __future__ import annotations

__all__: list[str] = []

import importlib
import sys
from collections.abc import Callable
from pathlib import Path
from typing import cast

# Ensure the project root is importable so ``features.environment`` resolves.
_ROOT = str(Path(__file__).resolve().parents[1])
if _ROOT not in sys.path:
    sys.path.insert(0, _ROOT)

_ROBOT_DIR = str(Path(__file__).resolve().parent)
if _ROBOT_DIR not in sys.path:
    sys.path.insert(0, _ROBOT_DIR)

from _tdd_fixture_runner import run_fixture, run_multi_fixture  # noqa: E402
from behave.model import Status  # noqa: E402
from features.environment import (  # noqa: E402
    _UNEXPECTED_PASS_MSG,
    apply_tdd_inversion,
    should_invert_result,
    validate_tdd_tags,
)
from features.mocks.tdd_test_helpers import make_mock_scenario  # noqa: E402


def validate_tags_valid_combos() -> int:
    """Verify valid tag combinations raise no error."""
    valid_sets: list[set[str]] = [
        {"tdd_issue", "tdd_issue_42"},
        {"tdd_issue", "tdd_issue_42", "tdd_expected_fail"},
        {"tdd_issue"},
        {"some_other_tag"},
        set(),
    ]
    for tag_set in valid_sets:
        try:
            validate_tdd_tags(tag_set)
        except ValueError as exc:
            print(
                f"FAIL: valid tag set {tag_set} raised ValueError: {exc}",
                file=sys.stderr,
            )
            return 1
    print("validate-tags-valid-combos-ok")
    return 0


def validate_tags_issue_n_without_issue() -> int:
    """Verify @tdd_issue_<N> without @tdd_issue raises ValueError."""
    try:
        validate_tdd_tags({"tdd_issue_42"})
    except ValueError:
        print("validate-tags-issue-n-without-issue-ok")
        return 0
    print(
        "FAIL: expected ValueError for tdd_issue_42 without tdd_issue",
        file=sys.stderr,
    )
    return 1


def validate_tags_expected_fail_missing_issue() -> int:
    """Verify @tdd_expected_fail without @tdd_issue raises ValueError."""
    try:
        validate_tdd_tags({"tdd_expected_fail", "tdd_issue_42"})
    except ValueError:
        print("validate-tags-expected-fail-missing-issue-ok")
        return 0
    print(
        "FAIL: expected ValueError for tdd_expected_fail without tdd_issue",
        file=sys.stderr,
    )
    return 1


def validate_tags_expected_fail_missing_issue_n() -> int:
    """Verify @tdd_expected_fail without @tdd_issue_<N> raises ValueError."""
    try:
        validate_tdd_tags({"tdd_expected_fail", "tdd_issue"})
    except ValueError:
        print("validate-tags-expected-fail-missing-issue-n-ok")
        return 0
    print(
        "FAIL: expected ValueError for tdd_expected_fail without tdd_issue_<N>",
        file=sys.stderr,
    )
    return 1


def should_invert_with_expected_fail() -> int:
    """Verify should_invert_result returns True when @tdd_expected_fail present."""
    result = should_invert_result({"tdd_issue", "tdd_issue_42", "tdd_expected_fail"})
    if result is not True:
        print(f"FAIL: expected True, got {result}", file=sys.stderr)
        return 1
    print("should-invert-with-expected-fail-ok")
    return 0


def should_not_invert_without_expected_fail() -> int:
    """Verify should_invert_result returns False without @tdd_expected_fail."""
    result = should_invert_result({"tdd_issue", "tdd_issue_42"})
    if result is not False:
        print(f"FAIL: expected False, got {result}", file=sys.stderr)
        return 1
    print("should-not-invert-without-expected-fail-ok")
    return 0


def inversion_expected_fail() -> int:
    """Verify expected failure (failed=True) is inverted to passed."""
    scenario = make_mock_scenario(
        tags=["tdd_issue", "tdd_issue_42", "tdd_expected_fail"],
        steps_passed=False,
    )
    result = apply_tdd_inversion(scenario, failed=True)
    if result is not False:
        print(f"FAIL: expected False (inverted), got {result}", file=sys.stderr)
        return 1
    scenario.set_status.assert_called_with(Status.passed)
    print("inversion-expected-fail-ok")
    return 0


def inversion_unexpected_pass() -> int:
    """Verify unexpected pass (failed=False) is inverted to failure."""
    scenario = make_mock_scenario(
        tags=["tdd_issue", "tdd_issue_42", "tdd_expected_fail"],
        steps_passed=True,
    )
    result = apply_tdd_inversion(scenario, failed=False)
    if result is not True:
        print(f"FAIL: expected True (forced failure), got {result}", file=sys.stderr)
        return 1
    scenario.set_status.assert_called_with(Status.failed)
    # Verify synthetic error is attached to last step
    last_step = scenario.all_steps[-1]
    if last_step.status != Status.failed:
        print(
            f"FAIL: last step status should be failed, got {last_step.status}",
            file=sys.stderr,
        )
        return 1
    if not isinstance(last_step.exception, AssertionError):
        print(
            f"FAIL: expected AssertionError, got {type(last_step.exception)}",
            file=sys.stderr,
        )
        return 1
    if str(last_step.exception) != _UNEXPECTED_PASS_MSG:
        print(f"FAIL: unexpected message: {last_step.exception}", file=sys.stderr)
        return 1
    print("inversion-unexpected-pass-ok")
    return 0


def inversion_hook_error_guard() -> int:
    """Verify hook_failed prevents inversion."""
    scenario = make_mock_scenario(
        tags=["tdd_issue", "tdd_issue_42", "tdd_expected_fail"],
        steps_passed=False,
        hook_failed=True,
    )
    result = apply_tdd_inversion(scenario, failed=True)
    if result is not True:
        print(f"FAIL: expected True (not inverted), got {result}", file=sys.stderr)
        return 1
    print("inversion-hook-error-guard-ok")
    return 0


def inversion_dry_run_guard() -> int:
    """Verify dry-run mode prevents inversion."""
    scenario = make_mock_scenario(
        tags=["tdd_issue", "tdd_issue_42", "tdd_expected_fail"],
        steps_passed=True,
        was_dry_run=True,
    )
    result = apply_tdd_inversion(scenario, failed=False)
    if result is not False:
        print(f"FAIL: expected False (not inverted), got {result}", file=sys.stderr)
        return 1
    print("inversion-dry-run-guard-ok")
    return 0


def inversion_non_assertion_guard() -> int:
    """Verify non-AssertionError exceptions prevent inversion."""
    scenario = make_mock_scenario(
        tags=["tdd_issue", "tdd_issue_42", "tdd_expected_fail"],
        steps_passed=False,
        step_exception=RuntimeError("connection lost"),
    )
    result = apply_tdd_inversion(scenario, failed=True)
    if result is not True:
        print(f"FAIL: expected True (not inverted), got {result}", file=sys.stderr)
        return 1
    print("inversion-non-assertion-guard-ok")
    return 0


def inversion_no_tag_passthrough() -> int:
    """Verify scenarios without @tdd_expected_fail are not modified."""
    scenario = make_mock_scenario(
        tags=["tdd_issue", "tdd_issue_42"],
        steps_passed=False,
    )
    result = apply_tdd_inversion(scenario, failed=True)
    if result is not True:
        print(f"FAIL: expected True (passthrough), got {result}", file=sys.stderr)
        return 1
    # set_status should NOT have been called
    scenario.set_status.assert_not_called()
    print("inversion-no-tag-passthrough-ok")
    return 0


def cmd_expected_fail_inverted() -> int:
    """Verify that a tdd_expected_fail test that fails is inverted to PASS."""
    status, message = run_fixture("tdd_expected_fail_fails")
    if status == "PASS" and "TDD expected failure" in message:
        print("tdd-expected-fail-inverted-ok")
        return 0
    print(
        f"FAIL: Expected PASS with 'TDD expected failure' message "
        f"but got {status}. Message: {message}",
        file=sys.stderr,
    )
    return 1


def cmd_unexpected_pass_inverted() -> int:
    """Verify that a tdd_expected_fail test that passes is inverted to FAIL."""
    status, message = run_fixture("tdd_expected_fail_passes")
    if status == "FAIL" and "Bug appears to be fixed" in message:
        print("tdd-unexpected-pass-inverted-ok")
        return 0
    print(
        f"FAIL: Expected FAIL with guidance but got {status}. Message: {message}",
        file=sys.stderr,
    )
    return 1


def cmd_missing_tdd_issue_validation() -> int:
    """Verify that tdd_issue_N without tdd_issue causes a validation error."""
    status, message = run_fixture("tdd_missing_tdd_issue")
    if status == "FAIL" and "missing the required tdd_issue tag" in message:
        print("tdd-missing-tdd-issue-validation-ok")
        return 0
    print(
        f"FAIL: Expected FAIL with 'missing the required tdd_issue tag' "
        f"but got {status}. Message: {message}",
        file=sys.stderr,
    )
    return 1


def cmd_missing_issue_n_validation() -> int:
    """Verify that tdd_expected_fail without tdd_issue_N causes validation error."""
    status, message = run_fixture("tdd_expected_fail_missing_issue_n")
    if status == "FAIL" and "tdd_issue_<N>" in message:
        print("tdd-missing-issue-n-validation-ok")
        return 0
    print(
        f"FAIL: Expected FAIL with validation error mentioning "
        f"tdd_issue_<N> but got {status}. Message: {message}",
        file=sys.stderr,
    )
    return 1


def _check_companion_inverted(
    results: dict[str, tuple[str, str]],
) -> int | None:
    """Verify the companion expected-fail fixture was inverted to PASS.

    Args:
        results: Mapping of test name to ``(status, message)`` tuples.

    Returns:
        ``None`` if the companion fixture was inverted successfully, or ``1`` if
        validation failed (after printing the error message).
    """
    inverted_name = "Bug 999 Expected Failure Is Inverted To Pass"
    if inverted_name not in results:
        print(
            f"FAIL: Companion '{inverted_name}' not found. "
            f"Available: {list(results.keys())}",
            file=sys.stderr,
        )
        return 1
    inv_status, _ = results[inverted_name]
    if inv_status != "PASS":
        print(
            f"FAIL: Companion not inverted to PASS (got {inv_status}). "
            f"Listener may not be loaded.",
            file=sys.stderr,
        )
        return 1
    return None


def cmd_normal_test_unaffected() -> int:
    """Verify normal test (no TDD tags) is unaffected by the listener."""
    results = run_multi_fixture("tdd_normal_test", "tdd_expected_fail_fails")
    normal_name = "Normal Test Unaffected By Listener"
    if normal_name not in results:
        print(f"FAIL: '{normal_name}' not found.", file=sys.stderr)
        return 1
    normal_status, normal_msg = results[normal_name]
    if normal_status != "PASS":
        print(
            f"FAIL: Expected PASS but got {normal_status}. Message: {normal_msg}",
            file=sys.stderr,
        )
        return 1
    err = _check_companion_inverted(results)
    if err is not None:
        return err
    print("tdd-normal-test-unaffected-ok")
    return 0


def cmd_skip_status_unchanged() -> int:
    """Verify skipped tdd_expected_fail test stays SKIP."""
    results = run_multi_fixture(
        "tdd_expected_fail_skip",
        "tdd_expected_fail_fails",
    )
    skip_name = "Bug 997 Skipped Expected Fail Test Stays Skip"
    if skip_name not in results:
        print(f"FAIL: '{skip_name}' not found.", file=sys.stderr)
        return 1
    skip_status, skip_msg = results[skip_name]
    if skip_status != "SKIP":
        print(
            f"FAIL: Expected SKIP but got {skip_status}. Message: {skip_msg}",
            file=sys.stderr,
        )
        return 1
    err = _check_companion_inverted(results)
    if err is not None:
        return err
    print("tdd-skip-status-unchanged-ok")
    return 0


def cmd_expected_fail_alone_validation() -> int:
    """Verify tdd_expected_fail alone fails validation."""
    status, message = run_fixture("tdd_expected_fail_alone")
    if status == "FAIL" and "tdd_issue" in message and "tdd_issue_<N>" in message:
        print("tdd-expected-fail-alone-validation-ok")
        return 0
    print(
        f"FAIL: Expected FAIL mentioning tdd_issue and tdd_issue_<N> "
        f"but got {status}. Message: {message}",
        file=sys.stderr,
    )
    return 1


def cmd_tdd_issue_alone_valid() -> int:
    """Verify tdd_issue alone is valid (listener does not interfere)."""
    results = run_multi_fixture("tdd_issue_alone", "tdd_expected_fail_fails")
    alone_name = "TDD Issue Tag Alone Is Valid"
    if alone_name not in results:
        print(f"FAIL: '{alone_name}' not found.", file=sys.stderr)
        return 1
    alone_status, alone_msg = results[alone_name]
    if alone_status != "PASS":
        print(
            f"FAIL: Expected PASS but got {alone_status}. Message: {alone_msg}",
            file=sys.stderr,
        )
        return 1
    err = _check_companion_inverted(results)
    if err is not None:
        return err
    print("tdd-issue-alone-valid-ok")
    return 0


_helper_guard_module = importlib.import_module("helper_tdd_guard_commands")
GUARD_COMMANDS = cast(
    dict[str, Callable[[], int]],
    _helper_guard_module.GUARD_COMMANDS,
)

_COMMANDS: dict[str, Callable[[], int]] = {
    # Behave-side commands
    "validate_tags_valid_combos": validate_tags_valid_combos,
    "validate_tags_issue_n_without_issue": validate_tags_issue_n_without_issue,
    "validate_tags_expected_fail_missing_issue": (
        validate_tags_expected_fail_missing_issue
    ),
    "validate_tags_expected_fail_missing_issue_n": (
        validate_tags_expected_fail_missing_issue_n
    ),
    "should_invert_with_expected_fail": should_invert_with_expected_fail,
    "should_not_invert_without_expected_fail": should_not_invert_without_expected_fail,
    "inversion_expected_fail": inversion_expected_fail,
    "inversion_unexpected_pass": inversion_unexpected_pass,
    "inversion_hook_error_guard": inversion_hook_error_guard,
    "inversion_dry_run_guard": inversion_dry_run_guard,
    "inversion_non_assertion_guard": inversion_non_assertion_guard,
    "inversion_no_tag_passthrough": inversion_no_tag_passthrough,
    # Robot-side commands
    "expected-fail-inverted": cmd_expected_fail_inverted,
    "unexpected-pass-inverted": cmd_unexpected_pass_inverted,
    "missing-tdd-issue-validation": cmd_missing_tdd_issue_validation,
    "missing-issue-n-validation": cmd_missing_issue_n_validation,
    "normal-test-unaffected": cmd_normal_test_unaffected,
    "skip-status-unchanged": cmd_skip_status_unchanged,
    "expected-fail-alone-validation": cmd_expected_fail_alone_validation,
    "tdd-issue-alone-valid": cmd_tdd_issue_alone_valid,
}

_COMMANDS.update(GUARD_COMMANDS)


def main() -> None:
    """Dispatch to the requested sub-command."""
    if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
        print(
            f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>",
            file=sys.stderr,
        )
        sys.exit(1)

    handler = _COMMANDS[sys.argv[1]]
    sys.exit(handler())


if __name__ == "__main__":
    main()
