"""Step definitions for plan_model_coverage.feature.

Covers ProjectLink validation, as_cli_dict output, APPLIED auto-correction,
and NamespacedName edge cases that were previously untested.
"""

from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError

from cleveragents.domain.models.core.plan import (
    AutomationProfileProvenance,
    AutomationProfileRef,
    InvariantSource,
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanInvariant,
    PlanPhase,
    ProcessingState,
    ProjectLink,
    SubplanStatus,
)

# Reusable ULID constants
ULID_MAIN = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
ULID_PARENT = "01ARZ3NDEKTSV4RRFFQ69G5FAW"
ULID_CHILD = "01ARZ3NDEKTSV4RRFFQ69G5FAX"


def _base_plan(**overrides: object) -> Plan:
    """Create a minimal Plan with overrides for testing."""
    defaults: dict[str, object] = {
        "identity": PlanIdentity(plan_id=ULID_MAIN),
        "namespaced_name": NamespacedName(
            server=None, namespace="local", name="test-plan"
        ),
        "description": "Test plan for CLI output",
        "action_name": "local/test-action",
        "phase": PlanPhase.STRATEGIZE,
        "processing_state": ProcessingState.QUEUED,
    }
    defaults.update(overrides)
    return Plan(**defaults)  # type: ignore[arg-type]


# ---- ProjectLink.validate_alias classmethod ----


@when('I validate the project link alias "{alias}"')
def step_validate_alias(context: Context, alias: str) -> None:
    """Call ProjectLink.validate_alias with the given alias."""
    context.alias_valid = ProjectLink.validate_alias(alias)


@when("I validate an empty project link alias")
def step_validate_empty_alias(context: Context) -> None:
    """Call ProjectLink.validate_alias with an empty string."""
    context.alias_valid = ProjectLink.validate_alias("")


@then("the alias should be valid")
def step_alias_valid(context: Context) -> None:
    """Assert the alias validation returned True."""
    assert context.alias_valid is True, "Expected alias to be valid"


@then("the alias should be invalid")
def step_alias_invalid(context: Context) -> None:
    """Assert the alias validation returned False."""
    assert context.alias_valid is False, "Expected alias to be invalid"


# ---- ProjectLink construction with alias validator ----


@when('I try to create a project link with alias "{alias}"')
def step_try_create_project_link_invalid(context: Context, alias: str) -> None:
    """Attempt to construct a ProjectLink that may fail validation."""
    context.error = None
    try:
        context.project_link = ProjectLink(
            project_name="local/api-service",
            alias=alias,
        )
    except ValidationError as e:
        context.error = e


@when('I create a project link with alias "{alias}"')
def step_create_project_link(context: Context, alias: str) -> None:
    """Construct a ProjectLink with a valid alias."""
    context.project_link = ProjectLink(
        project_name="local/api-service",
        alias=alias,
    )


@then('the project link alias should be "{expected}"')
def step_check_project_link_alias(context: Context, expected: str) -> None:
    """Assert the project link alias matches expected."""
    assert context.project_link.alias == expected, (
        f"Expected alias '{expected}', got '{context.project_link.alias}'"
    )


# ---- APPLY phase with APPLIED processing_state is terminal ----


@when("I create a plan in apply phase with applied processing state")
def step_create_apply_phase_applied_state(context: Context) -> None:
    """Create a plan in APPLY phase with APPLIED processing state (terminal)."""
    context.plan = _base_plan(
        phase=PlanPhase.APPLY,
        processing_state=ProcessingState.APPLIED,
    )


# ---- Duplicate project link alias uniqueness ----


@when("I try to create a plan with duplicate project link aliases")
def step_try_duplicate_aliases(context: Context) -> None:
    """Attempt to create a plan with two project links sharing the same alias."""
    context.error = None
    try:
        context.plan = _base_plan(
            project_links=[
                ProjectLink(project_name="local/svc-a", alias="main"),
                ProjectLink(project_name="local/svc-b", alias="main"),
            ],
        )
    except ValidationError as e:
        context.error = e


@when("I create a plan with distinct project link aliases")
def step_create_distinct_aliases(context: Context) -> None:
    """Create a plan with two project links that have distinct aliases."""
    context.plan = _base_plan(
        project_links=[
            ProjectLink(project_name="local/svc-a", alias="primary"),
            ProjectLink(project_name="local/svc-b", alias="secondary"),
        ],
    )


@then("the plan should have {count:d} project links")
def step_check_project_link_count(context: Context, count: int) -> None:
    """Assert the plan has exactly the expected number of project links."""
    actual = len(context.plan.project_links)
    assert actual == count, f"Expected {count} project links, got {actual}"


# ---- as_cli_dict — plan creation helpers ----


@given("I create a plan for CLI output")
def step_create_plan_cli_base(context: Context) -> None:
    """Create a minimal plan for CLI dict testing (no optional fields)."""
    context.plan = _base_plan()


@given('I create a plan with definition of done "{dod}"')
def step_create_plan_with_dod(context: Context, dod: str) -> None:
    """Create a plan with a definition_of_done."""
    context.plan = _base_plan(definition_of_done=dod)


@given('I create a plan with automation profile "{profile}" from "{source}"')
def step_create_plan_with_automation_profile(
    context: Context, profile: str, source: str
) -> None:
    """Create a plan with an automation profile and provenance tag."""
    context.plan = _base_plan(
        automation_profile=AutomationProfileRef(
            profile_name=profile,
            provenance=AutomationProfileProvenance(source),
        ),
    )


@given("I create a plan with project links")
def step_create_plan_with_project_links(context: Context) -> None:
    """Create a plan with two project links (one with alias, one read_only)."""
    context.plan = _base_plan(
        project_links=[
            ProjectLink(project_name="local/api-svc", alias="api", read_only=False),
            ProjectLink(project_name="local/docs", read_only=True),
        ],
    )


@given("I create a plan with invariants")
def step_create_plan_with_invariants(context: Context) -> None:
    """Create a plan with two invariants from different sources."""
    context.plan = _base_plan(
        invariants=[
            PlanInvariant(
                text="Do not modify public API", source=InvariantSource.ACTION
            ),
            PlanInvariant(text="Keep backward compat", source=InvariantSource.PROJECT),
        ],
    )


@given("I create a plan with arguments")
def step_create_plan_with_arguments(context: Context) -> None:
    """Create a plan with arguments and argument ordering."""
    context.plan = _base_plan(
        arguments={"target": "src/main.py", "mode": "strict"},
        arguments_order=["target", "mode"],
    )


@given('I create a plan with strategy actor "{actor}"')
def step_create_plan_with_strategy_actor(context: Context, actor: str) -> None:
    """Create a plan with strategy_actor set."""
    context.plan = _base_plan(strategy_actor=actor)


@given('I create a plan with execution actor "{actor}"')
def step_create_plan_with_execution_actor(context: Context, actor: str) -> None:
    """Create a plan with execution_actor set."""
    context.plan = _base_plan(execution_actor=actor)


@given('I create a plan with error message "{msg}"')
def step_create_plan_with_error(context: Context, msg: str) -> None:
    """Create a plan with error_message set."""
    context.plan = _base_plan(
        processing_state=ProcessingState.ERRORED,
        error_message=msg,
    )


@given("I create a subplan for CLI output")
def step_create_subplan_cli(context: Context) -> None:
    """Create a plan that is a subplan (has parent_plan_id)."""
    context.plan = _base_plan(
        identity=PlanIdentity(
            plan_id=ULID_CHILD,
            parent_plan_id=ULID_PARENT,
        ),
    )


@given("I create a plan with subplan statuses")
def step_create_plan_with_subplan_statuses(context: Context) -> None:
    """Create a plan that has tracked subplan statuses."""
    context.plan = _base_plan(
        subplan_statuses=[
            SubplanStatus(
                subplan_id=ULID_CHILD,
                action_name="local/sub-action",
            ),
        ],
    )


# ---- as_cli_dict invocation ----


@when("I call as_cli_dict")
def step_call_as_cli_dict(context: Context) -> None:
    """Call as_cli_dict() and store the result."""
    context.cli_dict = context.plan.as_cli_dict()


# ---- as_cli_dict assertions ----


@then('the CLI dict should contain key "{key}"')
def step_cli_dict_has_key(context: Context, key: str) -> None:
    """Assert the CLI dict contains the specified key."""
    assert key in context.cli_dict, (
        f"Expected key '{key}' in CLI dict, got keys: {list(context.cli_dict.keys())}"
    )


@then('the CLI dict should not contain key "{key}"')
def step_cli_dict_missing_key(context: Context, key: str) -> None:
    """Assert the CLI dict does not contain the specified key."""
    assert key not in context.cli_dict, (
        f"Key '{key}' should not be in CLI dict but it is: {context.cli_dict[key]}"
    )


@then('the CLI dict key "{key}" should be "{expected}"')
def step_cli_dict_key_equals(context: Context, key: str, expected: str) -> None:
    """Assert a CLI dict string value matches expected."""
    actual = context.cli_dict[key]
    assert str(actual) == expected, (
        f"Expected CLI dict['{key}'] == '{expected}', got '{actual}'"
    )


@then('the CLI dict key "{key}" should be {expected:d}')
def step_cli_dict_key_equals_int(context: Context, key: str, expected: int) -> None:
    """Assert a CLI dict integer value matches expected."""
    actual = context.cli_dict[key]
    assert actual == expected, f"Expected CLI dict['{key}'] == {expected}, got {actual}"


@then('the CLI dict key "{key}" should be a list of length {length:d}')
def step_cli_dict_list_length(context: Context, key: str, length: int) -> None:
    """Assert a CLI dict value is a list with the expected length."""
    value = context.cli_dict[key]
    assert isinstance(value, list), f"Expected list for '{key}', got {type(value)}"
    assert len(value) == length, (
        f"Expected '{key}' list length {length}, got {len(value)}"
    )


@then("the CLI dict arguments should preserve order")
def step_cli_dict_arguments_order(context: Context) -> None:
    """Assert arguments dict preserves the defined order."""
    args = context.cli_dict["arguments"]
    keys = list(args.keys())
    assert keys == ["target", "mode"], f"Expected order ['target', 'mode'], got {keys}"


# ---- is_subplan / is_root_plan / depth properties ----


@given("I create a child plan with a parent")
def step_create_child_plan(context: Context) -> None:
    """Create a plan that has a parent_plan_id set."""
    context.plan = _base_plan(
        identity=PlanIdentity(
            plan_id=ULID_CHILD,
            parent_plan_id=ULID_PARENT,
            root_plan_id=ULID_MAIN,
        ),
    )


@then("the plan should not be a subplan")
def step_plan_not_subplan(context: Context) -> None:
    assert not context.plan.is_subplan


@then("the plan should be a subplan")
def step_plan_is_subplan(context: Context) -> None:
    assert context.plan.is_subplan


@then("the plan should be a root plan")
def step_plan_is_root(context: Context) -> None:
    assert context.plan.is_root_plan


@then("the plan should not be a root plan")
def step_plan_not_root(context: Context) -> None:
    assert not context.plan.is_root_plan


@then("the plan depth should be {expected:d}")
def step_plan_depth(context: Context, expected: int) -> None:
    assert context.plan.depth == expected, (
        f"Expected depth {expected}, got {context.plan.depth}"
    )


# ---- SubplanFailureHandler ----


@given("a subplan config with fail_fast enabled and sequential mode")
def step_config_failfast_sequential(context: Context) -> None:
    from cleveragents.domain.models.core.plan import ExecutionMode, SubplanConfig

    context.subplan_config = SubplanConfig(
        execution_mode=ExecutionMode.SEQUENTIAL,
        fail_fast=True,
    )


@given("a subplan config with fail_fast disabled and sequential mode")
def step_config_no_failfast_sequential(context: Context) -> None:
    from cleveragents.domain.models.core.plan import ExecutionMode, SubplanConfig

    context.subplan_config = SubplanConfig(
        execution_mode=ExecutionMode.SEQUENTIAL,
        fail_fast=False,
    )


@given("a subplan config with fail_fast disabled and parallel mode")
def step_config_no_failfast_parallel(context: Context) -> None:
    from cleveragents.domain.models.core.plan import ExecutionMode, SubplanConfig

    context.subplan_config = SubplanConfig(
        execution_mode=ExecutionMode.PARALLEL,
        fail_fast=False,
    )


@given("a failed subplan status")
def step_failed_subplan_status(context: Context) -> None:
    context.subplan_status = SubplanStatus(
        subplan_id=ULID_CHILD,
        action_name="local/test-action",
        status=ProcessingState.ERRORED,
        error="SomeError",
    )


@given('a failed subplan status with error "{error}"')
def step_failed_subplan_status_with_error(context: Context, error: str) -> None:
    context.subplan_status = SubplanStatus(
        subplan_id=ULID_CHILD,
        action_name="local/test-action",
        status=ProcessingState.ERRORED,
        error=error,
    )


@given('a failed subplan status on attempt {attempt:d} with error "{error}"')
def step_failed_subplan_status_attempt(
    context: Context, attempt: int, error: str
) -> None:
    context.subplan_status = SubplanStatus(
        subplan_id=ULID_CHILD,
        action_name="local/test-action",
        status=ProcessingState.ERRORED,
        error=error,
        attempt_number=attempt,
    )


@given("a subplan config with retry disabled")
def step_config_retry_disabled(context: Context) -> None:
    from cleveragents.domain.models.core.plan import SubplanConfig

    context.subplan_config = SubplanConfig(retry_failed=False)


@given("a subplan config with max retries {n:d}")
def step_config_max_retries(context: Context, n: int) -> None:
    from cleveragents.domain.models.core.plan import SubplanConfig

    context.subplan_config = SubplanConfig(retry_failed=True, max_retries=n)


@given("a subplan config with retry enabled and max retries {n:d}")
def step_config_retry_enabled(context: Context, n: int) -> None:
    from cleveragents.domain.models.core.plan import SubplanConfig

    context.subplan_config = SubplanConfig(retry_failed=True, max_retries=n)


@when("I check should_stop_others")
def step_check_should_stop_others(context: Context) -> None:
    from cleveragents.domain.models.core.plan import SubplanFailureHandler

    handler = SubplanFailureHandler()
    context.stop_result = handler.should_stop_others(
        context.subplan_config, context.subplan_status
    )


@when("I check should_retry")
def step_check_should_retry(context: Context) -> None:
    from cleveragents.domain.models.core.plan import SubplanFailureHandler

    handler = SubplanFailureHandler()
    context.retry_result = handler.should_retry(
        context.subplan_config, context.subplan_status
    )


@then("should_stop_others should be true")
def step_stop_others_true(context: Context) -> None:
    assert context.stop_result is True


@then("should_stop_others should be false")
def step_stop_others_false(context: Context) -> None:
    assert context.stop_result is False


@then("should_retry should be true")
def step_retry_true(context: Context) -> None:
    assert context.retry_result is True


@then("should_retry should be false")
def step_retry_false(context: Context) -> None:
    assert context.retry_result is False
