"""Step definitions for TDD issue-capture tag validation scenarios.

Tests the ``validate_tdd_tags()``, ``should_invert_result()``, and
``apply_tdd_inversion()`` helper functions defined in
``features/environment.py``.

All step names are prefixed with ``tdd tags`` or ``tdd inversion`` to avoid
``AmbiguousStep`` conflicts with existing steps.

See CONTRIBUTING.md > TDD Issue Test Tags for the three-tag specification.
"""

from __future__ import annotations

from unittest.mock import MagicMock

from behave import given, then, when
from behave.model import Status
from behave.runner import Context

from features.environment import (
    apply_tdd_inversion,
    should_invert_result,
    validate_tdd_tags,
)
from features.mocks.tdd_test_helpers import make_mock_scenario

# ---------------------------------------------------------------------------
# Tag-set construction helpers
# ---------------------------------------------------------------------------


@given('tdd tags a tag set with "{tags_csv}"')
def step_given_tag_set(context: Context, tags_csv: str) -> None:
    """Parse a comma-separated tag list into a set on the context."""
    context.tdd_tag_set = {t.strip() for t in tags_csv.split(",") if t.strip()}


@given("tdd tags an empty tag set")
def step_given_empty_tag_set(context: Context) -> None:
    context.tdd_tag_set = set()


# ---------------------------------------------------------------------------
# Validation execution
# ---------------------------------------------------------------------------


@when("tdd tags I validate the tag set")
def step_when_validate(context: Context) -> None:
    """Run ``validate_tdd_tags`` and capture any raised ``ValueError``."""
    context.tdd_validation_error = None
    try:
        validate_tdd_tags(context.tdd_tag_set)
    except ValueError as exc:
        context.tdd_validation_error = str(exc)


# ---------------------------------------------------------------------------
# Validation outcome assertions
# ---------------------------------------------------------------------------


@then("tdd tags validation should pass")
def step_then_validation_passes(context: Context) -> None:
    assert context.tdd_validation_error is None, (
        f"Expected validation to pass but got error: {context.tdd_validation_error}"
    )


@then('tdd tags validation should fail with error containing "{fragment}"')
def step_then_validation_fails_with(context: Context, fragment: str) -> None:
    assert context.tdd_validation_error is not None, (
        "Expected validation to fail but it passed"
    )
    assert fragment in context.tdd_validation_error, (
        f"Expected error to contain '{fragment}' but got: "
        f"{context.tdd_validation_error}"
    )


# ---------------------------------------------------------------------------
# should_invert_result assertions
# ---------------------------------------------------------------------------


@then("tdd tags should_invert_result should return true")
def step_then_invert_true(context: Context) -> None:
    assert should_invert_result(context.tdd_tag_set) is True


@then("tdd tags should_invert_result should return false")
def step_then_invert_false(context: Context) -> None:
    assert should_invert_result(context.tdd_tag_set) is False


# ---------------------------------------------------------------------------
# apply_tdd_inversion unit-test helpers
# ---------------------------------------------------------------------------
# The mock scenario builder is in ``features/mocks/tdd_test_helpers.py``
# (imported above as ``make_mock_scenario``) to avoid duplication with
# ``robot/helper_tdd_tag_validation.py``.


# ---------------------------------------------------------------------------
# apply_tdd_inversion — expected failure inverted to pass (happy path)
# ---------------------------------------------------------------------------


@given(
    "tdd inversion a mock scenario that fails with AssertionError"
    " and tdd_expected_fail tags"
)
def step_given_expected_failure_scenario(context: Context) -> None:
    """Create a mock scenario with a failed step (AssertionError) and TDD tags."""
    context.tdd_mock_scenario = make_mock_scenario(
        tags=["tdd_issue", "tdd_issue_998", "tdd_expected_fail"],
        steps_passed=False,
        step_exception=AssertionError("simulated bug assertion failure"),
    )


@then(
    "tdd inversion the result should be False indicating expected failure was inverted"
)
def step_then_result_inverted_to_pass(context: Context) -> None:
    assert context.tdd_inversion_result is False, (
        f"Expected False (failure inverted to pass) but got "
        f"{context.tdd_inversion_result}"
    )


@then("tdd inversion the scenario status should be passed")
def step_then_scenario_status_passed(context: Context) -> None:
    context.tdd_mock_scenario.set_status.assert_called_with(Status.passed)


@then("tdd inversion all failed and skipped steps should be reset to passed")
def step_then_steps_reset_to_passed(context: Context) -> None:
    for step in context.tdd_mock_scenario.all_steps:
        assert step.status == Status.passed, (
            f"Expected step status to be passed but got {step.status}"
        )
        assert step.exception is None, (
            f"Expected step exception to be None but got {step.exception}"
        )
        assert step.exc_traceback is None, (
            f"Expected step exc_traceback to be None but got {step.exc_traceback}"
        )


@then("tdd inversion all step error_messages should be cleared")
def step_then_error_messages_cleared(context: Context) -> None:
    """Assert every step's ``error_message`` is ``None`` after inversion."""
    for step in context.tdd_mock_scenario.all_steps:
        assert step.error_message is None, (
            f"Expected step error_message to be None but got {step.error_message!r}"
        )


# ---------------------------------------------------------------------------
# apply_tdd_inversion — unexpected-pass path
# ---------------------------------------------------------------------------


@given("tdd inversion a mock scenario that passes with tdd_expected_fail tags")
def step_given_unexpected_pass_scenario(context: Context) -> None:
    """Create a mock scenario where steps pass but @tdd_expected_fail is set."""
    context.tdd_mock_scenario = make_mock_scenario(
        tags=["tdd_issue", "tdd_issue_998", "tdd_expected_fail"],
        steps_passed=True,
    )


@when("tdd inversion apply_tdd_inversion is called with failed False")
def step_when_apply_inversion_not_failed(context: Context) -> None:
    context.tdd_inversion_result = apply_tdd_inversion(
        context.tdd_mock_scenario, failed=False
    )


@then("tdd inversion the result should be True indicating forced failure")
def step_then_result_is_forced_failure(context: Context) -> None:
    assert context.tdd_inversion_result is True, (
        f"Expected True (forced failure) but got {context.tdd_inversion_result}"
    )


@then("tdd inversion the scenario status should be failed")
def step_then_scenario_status_failed(context: Context) -> None:
    context.tdd_mock_scenario.set_status.assert_called_with(Status.failed)


@then("tdd inversion the last step should have a synthetic error message")
def step_then_last_step_has_error(context: Context) -> None:
    last_step = context.tdd_mock_scenario.all_steps[-1]
    assert last_step.status == Status.failed, (
        f"Expected last step status to be failed but got {last_step.status}"
    )
    assert isinstance(last_step.exception, AssertionError), (
        f"Expected AssertionError but got {type(last_step.exception)}"
    )
    assert "Bug appears to be fixed" in str(last_step.exception)


@then("tdd inversion the last step should have a synthetic error_message text")
def step_then_last_step_has_error_message(context: Context) -> None:
    """Assert the last step's ``error_message`` contains the unexpected-pass text."""
    last_step = context.tdd_mock_scenario.all_steps[-1]
    assert last_step.error_message is not None, (
        "Expected last step to have an error_message set, got None"
    )
    assert "Bug appears to be fixed" in last_step.error_message, (
        f"Expected error_message to contain 'Bug appears to be fixed', "
        f"got {last_step.error_message!r}"
    )


# ---------------------------------------------------------------------------
# apply_tdd_inversion — hook-error guard
# ---------------------------------------------------------------------------


@given("tdd inversion a mock scenario with hook_failed and tdd_expected_fail tags")
def step_given_hook_failed_scenario(context: Context) -> None:
    context.tdd_mock_scenario = make_mock_scenario(
        tags=["tdd_issue", "tdd_issue_998", "tdd_expected_fail"],
        steps_passed=False,
        hook_failed=True,
    )


@when("tdd inversion apply_tdd_inversion is called with failed True")
def step_when_apply_inversion_failed(context: Context) -> None:
    context.tdd_inversion_result = apply_tdd_inversion(
        context.tdd_mock_scenario, failed=True
    )


@then("tdd inversion the result should be True indicating failure was not inverted")
def step_then_result_not_inverted(context: Context) -> None:
    assert context.tdd_inversion_result is True, (
        f"Expected True (not inverted) but got {context.tdd_inversion_result}"
    )


# ---------------------------------------------------------------------------
# apply_tdd_inversion — dry-run guard
# ---------------------------------------------------------------------------


@given("tdd inversion a mock scenario in dry-run mode with tdd_expected_fail tags")
def step_given_dry_run_scenario(context: Context) -> None:
    context.tdd_mock_scenario = make_mock_scenario(
        tags=["tdd_issue", "tdd_issue_998", "tdd_expected_fail"],
        steps_passed=True,
        was_dry_run=True,
    )


@then("tdd inversion the result should be False indicating no inversion occurred")
def step_then_result_no_inversion(context: Context) -> None:
    assert context.tdd_inversion_result is False, (
        f"Expected False (no inversion) but got {context.tdd_inversion_result}"
    )


# ---------------------------------------------------------------------------
# apply_tdd_inversion — non-assertion exception guard
# ---------------------------------------------------------------------------


@given("tdd inversion a mock scenario with a RuntimeError and tdd_expected_fail tags")
def step_given_non_assertion_scenario(context: Context) -> None:
    context.tdd_mock_scenario = make_mock_scenario(
        tags=["tdd_issue", "tdd_issue_998", "tdd_expected_fail"],
        steps_passed=False,
        step_exception=RuntimeError("connection lost"),
    )


# ---------------------------------------------------------------------------
# apply_tdd_inversion — empty steps edge case
# ---------------------------------------------------------------------------


@given("tdd inversion a mock scenario with no steps and tdd_expected_fail tags")
def step_given_no_steps_scenario(context: Context) -> None:
    """Create a mock scenario with no steps and @tdd_expected_fail tags."""
    scenario = MagicMock()
    scenario.effective_tags = ["tdd_issue", "tdd_issue_998", "tdd_expected_fail"]
    scenario.name = "mock-scenario-no-steps"
    scenario.hook_failed = False
    scenario.was_dry_run = False
    scenario.all_steps = []
    context.tdd_mock_scenario = scenario
