"""Step definitions for invariant enforcement in Strategize phase."""

from behave import given, then, use_step_matcher, when

from cleveragents.application.services.invariant_service import InvariantService
from cleveragents.core.exceptions import InvariantViolationError, ValidationError


@given("a fresh InvariantService for enforcement")
def step_fresh_invariant_service(context):
    """Create a fresh InvariantService for the test."""
    context.invariant_service = InvariantService()
    context.loaded_invariants = []


@given("a fresh PlanLifecycleService for enforcement")
def step_fresh_plan_lifecycle_service(context):
    """Create a fresh PlanLifecycleService for the test.

    PlanLifecycleService requires Settings and a database connection, so we
    skip full initialisation here and rely on InvariantService for testing.
    """
    context.plan_lifecycle_service = None


# NOTE: The @given steps for adding invariants are defined in
# invariant_reconciliation_actor_steps.py and shared across invariant feature files.
# They add invariants to context.invariant_service directly.


@given('a plan "{plan_id}" in Strategize phase')
def step_plan_in_strategize_phase(context, plan_id):
    """Set up a plan in Strategize phase (simulated in-memory)."""
    context.current_plan_id = plan_id
    context.plan_phase = "strategize"
    context.strategy_decisions: list[str] = []
    context.strategize_invariants: list[object] = []


use_step_matcher("re")


@when(r'I load active invariants for plan "(?P<plan_id>[^"]+)"')
def step_load_invariants_plan_only(context, plan_id):
    """Load active invariants for a plan (no project context).

    Uses the regex matcher; behave's regex matcher auto-anchors both
    ends, so this pattern does not collide with the parse-matched
    'with project' variant below.
    """
    context.loaded_invariants = context.invariant_service.load_active_invariants(
        plan_id=plan_id
    )


use_step_matcher("parse")


@when('I load active invariants for plan "{plan_id}" with project "{project}"')
def step_load_invariants_with_project(context, plan_id, project):
    """Load active invariants for a plan with project context."""
    context.loaded_invariants = context.invariant_service.load_active_invariants(
        plan_id=plan_id,
        project_name=project,
    )


@when('I deactivate the invariant "{text}"')
def step_deactivate_invariant(context, text):
    """Deactivate an invariant by text (looks up by text from the service)."""
    all_invs = context.invariant_service.list_invariants()
    for inv in all_invs:
        if inv.text == text:
            context.invariant_service.remove_invariant(inv.id)
            return


def _check_action_against_loaded(context, action_text):
    """Shared implementation: check action against loaded (or active) invariants.

    Captures any raised exception both into the specific
    ``violation_error`` / ``validation_error`` attributes (consumed by
    local Then steps) AND into ``context.error`` (consumed by the shared
    ``the error message should contain "..."`` step in service_steps.py).
    """
    context.violation_error = None
    context.validation_error = None
    context.error = None
    invariants = (
        context.loaded_invariants
        if context.loaded_invariants
        else context.invariant_service.list_invariants()
    )
    try:
        context.invariant_service.check_invariants(
            action_text=action_text,
            invariants=invariants,
        )
        context.action_accepted = True
    except InvariantViolationError as e:
        context.violation_error = e
        context.error = e
        context.action_accepted = False
    except ValidationError as e:
        context.validation_error = e
        context.error = e
        context.action_accepted = False


@when('I check action "{action_text}" against loaded invariants')
def step_check_action_against_invariants(context, action_text):
    """Check an action against loaded invariants.

    Uses explicitly loaded invariants when available; otherwise falls back to
    all active invariants from the service so that @check_invariants scenarios
    (which add invariants via Given steps without a separate load step) work
    correctly.
    """
    _check_action_against_loaded(context, action_text)


@when('I check action "" against loaded invariants')
def step_check_empty_action_against_invariants(context):
    """Check an empty action against loaded invariants.

    Behave's default parse field `{action_text}` does not match the empty
    string, so this literal-pattern step handles the empty case explicitly.
    """
    _check_action_against_loaded(context, "")


@when('I check action "{action_text}" against empty invariants')
def step_check_action_against_empty_invariants(context, action_text):
    """Check an action against empty invariants list."""
    context.violation_error = None
    context.validation_error = None
    try:
        context.invariant_service.check_invariants(
            action_text=action_text,
            invariants=[],
        )
        context.action_accepted = True
    except InvariantViolationError as e:
        context.violation_error = e
        context.action_accepted = False
    except ValidationError as e:
        context.validation_error = e
        context.action_accepted = False


@when("I start the Strategize phase for the plan")
def step_start_strategize_phase(context):
    """Simulate starting the Strategize phase by loading invariants."""
    context.strategize_invariants = context.invariant_service.load_active_invariants(
        plan_id=context.current_plan_id
    )
    context.loaded_invariants = list(context.strategize_invariants)


def _strategize_active_invariants(context):
    """Return invariants to enforce during Strategize.

    Prefer explicitly loaded `strategize_invariants` (set by `I start the
    Strategize phase for the plan`); otherwise fall back to all active
    invariants from the service. The fallback lets scenarios that add a
    global invariant via a Given step but skip the explicit "start
    Strategize" step still exercise invariant enforcement.
    """
    return (
        context.strategize_invariants
        if context.strategize_invariants
        else context.invariant_service.list_invariants()
    )


@when('I attempt to create a strategy decision "{decision_text}"')
def step_attempt_strategy_decision(context, decision_text):
    """Attempt to create a strategy decision, checking invariants first."""
    context.violation_error = None
    context.validation_error = None
    try:
        context.invariant_service.check_invariants(
            action_text=decision_text,
            invariants=_strategize_active_invariants(context),
        )
        context.strategy_decisions.append(decision_text)
        context.action_accepted = True
    except InvariantViolationError as e:
        context.violation_error = e
        context.action_accepted = False
    except ValidationError as e:
        context.validation_error = e
        context.action_accepted = False


@when('I create a strategy decision "{decision_text}"')
def step_create_strategy_decision(context, decision_text):
    """Create a strategy decision (expects no invariant violation)."""
    context.violation_error = None
    context.validation_error = None
    context.invariant_service.check_invariants(
        action_text=decision_text,
        invariants=_strategize_active_invariants(context),
    )
    context.strategy_decisions.append(decision_text)
    context.action_accepted = True


@when("I restart the plan")
def step_restart_plan(context):
    """Simulate restarting the plan (clears in-memory state)."""
    context.strategize_invariants = []
    context.strategy_decisions = []


@when("I start the Strategize phase again")
def step_start_strategize_again(context):
    """Simulate starting the Strategize phase a second time."""
    context.strategize_invariants = context.invariant_service.load_active_invariants(
        plan_id=context.current_plan_id
    )
    context.loaded_invariants = list(context.strategize_invariants)


@then("{count:d} invariants should be loaded")
def step_verify_invariant_count_plural(context, count):
    """Verify the number of loaded invariants (plural form)."""
    assert len(context.loaded_invariants) == count, (
        f"Expected {count} invariants, got {len(context.loaded_invariants)}"
    )


@then("{count:d} invariant should be loaded")
def step_verify_invariant_count_singular(context, count):
    """Verify the number of loaded invariants (singular form)."""
    assert len(context.loaded_invariants) == count, (
        f"Expected {count} invariant, got {len(context.loaded_invariants)}"
    )


@then('the loaded set should contain "{text}"')
def step_verify_invariant_in_set(context, text):
    """Verify an invariant is in the loaded set."""
    texts = [inv.text for inv in context.loaded_invariants]
    assert text in texts, f"Invariant '{text}' not found in loaded set: {texts}"


@then('the loaded set should not contain "{text}"')
def step_verify_invariant_not_in_set(context, text):
    """Verify an invariant is not in the loaded set."""
    texts = [inv.text for inv in context.loaded_invariants]
    assert text not in texts, f"Invariant '{text}' should not be in loaded set: {texts}"


# NOTE: 'the winning invariant for "{key}" should be from "{scope}" scope'
# is defined in invariant_reconciliation_actor_steps.py.


@then("an InvariantViolationError should be raised")
def step_verify_violation_error_raised(context):
    """Verify an InvariantViolationError was raised."""
    assert context.violation_error is not None, "Expected InvariantViolationError"


@then("an inv ValidationError should be raised")
def step_verify_validation_error_raised(context):
    """Verify a ValidationError was raised."""
    assert context.validation_error is not None, "Expected ValidationError"


@then("no inv error should be raised")
def step_verify_no_error(context):
    """Verify no error was raised."""
    assert context.violation_error is None, (
        f"Unexpected error: {context.violation_error}"
    )
    assert context.validation_error is None, (
        f"Unexpected error: {context.validation_error}"
    )


@then("the action should be accepted")
def step_verify_action_accepted(context):
    """Verify the action was accepted."""
    assert context.action_accepted is True, "Action should be accepted"


@then("the error should include invariant ID")
def step_verify_error_has_invariant_id(context):
    """Verify the error includes invariant ID."""
    assert context.violation_error is not None
    assert hasattr(context.violation_error, "invariant_id")
    assert context.violation_error.invariant_id


@then('the error should include the violated text "{text}"')
def step_verify_error_has_violated_text(context, text):
    """Verify the error includes the violated text."""
    assert context.violation_error is not None
    assert context.violation_error.violated_text == text


@then('the error should include the action text "{text}"')
def step_verify_error_has_action_text(context, text):
    """Verify the error includes the action text."""
    assert context.violation_error is not None
    assert context.violation_error.action_text == text


@then("the error should have scope information")
def step_verify_error_has_scope(context):
    """Verify the error has scope information."""
    assert context.violation_error is not None
    assert context.violation_error.details is not None
    assert "scope" in context.violation_error.details


@then("the error should have source_name information")
def step_verify_error_has_source_name(context):
    """Verify the error has source_name information."""
    assert context.violation_error is not None
    assert context.violation_error.details is not None
    assert "source_name" in context.violation_error.details


@then("the error message should contain {text}")
def step_verify_error_message_contains(context, text):
    """Verify the error message contains specific text (strips surrounding quotes)."""
    # Strip surrounding quotes that Behave passes through from the feature file.
    text = text.strip("\"'")
    if context.violation_error:
        assert text in str(context.violation_error), (
            f"Expected '{text}' in violation error: {context.violation_error}"
        )
    elif context.validation_error:
        assert text in str(context.validation_error), (
            f"Expected '{text}' in validation error: {context.validation_error}"
        )
    else:
        raise AssertionError("No error was raised")


@then("the error message should include the invariant ID")
def step_verify_error_message_has_invariant_id(context):
    """Verify the error message includes the invariant ID."""
    assert context.violation_error is not None
    message = str(context.violation_error)
    assert context.violation_error.invariant_id in message, (
        f"Invariant ID '{context.violation_error.invariant_id}' not in: {message}"
    )


@then("the error message should include the invariant text")
def step_verify_error_message_has_invariant_text(context):
    """Verify the error message includes the invariant text."""
    assert context.violation_error is not None
    message = str(context.violation_error)
    assert context.violation_error.violated_text in message, (
        f"Invariant text '{context.violation_error.violated_text}' not in: {message}"
    )


@then("the error message should include the action text")
def step_verify_error_message_has_action_text(context):
    """Verify the error message includes the action text."""
    assert context.violation_error is not None
    message = str(context.violation_error)
    assert context.violation_error.action_text in message, (
        f"Action text '{context.violation_error.action_text}' not in: {message}"
    )


@then("the error message should clearly identify which invariant was violated")
def step_verify_error_identifies_invariant(context):
    """Verify the error message clearly identifies the violated invariant."""
    assert context.violation_error is not None
    message = str(context.violation_error)
    assert "Invariant violation" in message or "invariant" in message.lower()


@then("the error message should explain why the action violates the invariant")
def step_verify_error_explains_violation(context):
    """Verify the error message explains the violation."""
    assert context.violation_error is not None
    message = str(context.violation_error)
    # The message should include both the invariant text and action text
    assert (
        context.violation_error.violated_text in message
        or "violated" in message.lower()
    )


@then("the error should include one of the violated invariants")
def step_verify_error_includes_one_violation(context):
    """Verify the error includes one of the violated invariants."""
    assert context.violation_error is not None
    assert context.violation_error.invariant_id
    assert context.violation_error.violated_text


@then("invariants should be loaded")
def step_invariants_should_be_loaded(context):
    """Verify invariants were loaded during Strategize phase startup."""
    assert context.strategize_invariants is not None


@then("{count:d} invariant should be active for the plan")
def step_invariant_count_active_for_plan(context, count):
    """Verify the number of active invariants for the plan."""
    assert len(context.strategize_invariants) == count, (
        f"Expected {count} active invariant(s), "
        f"got {len(context.strategize_invariants)}"
    )


@then("the plan should remain in Strategize phase")
def step_plan_remains_in_strategize(context):
    """Verify the plan remains in Strategize phase after a violation."""
    assert context.plan_phase == "strategize"


@then("the decision should not be recorded")
def step_decision_not_recorded(context):
    """Verify the decision was not recorded due to a violation."""
    assert context.action_accepted is False, "Decision should not have been recorded"


@then("the decision should be recorded")
def step_decision_recorded(context):
    """Verify the decision was recorded successfully."""
    assert context.action_accepted is True, "Decision should have been recorded"
    assert len(context.strategy_decisions) > 0


@then("the plan should progress normally")
def step_plan_progresses_normally(context):
    """Verify the plan can progress normally (no violation)."""
    assert context.violation_error is None
    assert context.action_accepted is True


@then("invariants should be loaded fresh from database")
def step_invariants_loaded_fresh(context):
    """Verify invariants were reloaded fresh after a plan restart."""
    assert context.strategize_invariants is not None
