"""Step definitions for action YAML schema validation.

Tests for features/action_schema.feature — validates the ActionConfigSchema
Pydantic model, YAML loading, key normalization, invariant normalization,
environment variable interpolation, and error messages.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context  # type: ignore[import-untyped]
from pydantic import ValidationError

from cleveragents.action.schema import ActionConfigSchema

# ────────────────────────────────────────────────────────────
# Helpers
# ────────────────────────────────────────────────────────────

_MINIMAL_YAML = """\
name: local/simple-action
description: A simple action for testing
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: All tests pass
"""

_FULL_YAML = """\
schema_version: "1"
name: local/full-action
description: A fully populated action
long_description: |
  This action demonstrates all optional fields.
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
estimation_actor: local/estimator
review_actor: local/reviewer
apply_actor: local/applier
invariant_actor: local/invariant-resolver
definition_of_done: All tests pass and coverage is above 97%
reusable: true
read_only: false
state: available
automation_profile: local/cautious
invariants:
  - "No secrets in code"
  - "All tests must pass"
arguments:
  - name: target
    type: integer
    required: true
    description: Target coverage percentage
    min_value: 1
    max_value: 100
inputs_schema:
  type: object
  properties:
    config:
      type: string
"""

_ARGUMENTS_YAML = """\
name: local/with-args
description: Action with arguments
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: Coverage target reached
arguments:
  - name: target_coverage
    type: integer
    required: true
    description: Target coverage percentage
    min_value: 1
    max_value: 100
  - name: test_command
    type: string
    required: false
    description: Test framework command
    default: "pytest --cov"
"""


def _make_yaml_missing(field: str) -> str:
    """Return minimal YAML with one required field removed."""
    lines = _MINIMAL_YAML.strip().splitlines()
    return "\n".join(ln for ln in lines if not ln.startswith(f"{field}:"))


# ────────────────────────────────────────────────────────────
# Given steps
# ────────────────────────────────────────────────────────────


@given("an action YAML string with only required fields")
def step_given_minimal_yaml(context: Context) -> None:
    """Provide a minimal valid action YAML string."""
    context.action_yaml_string = _MINIMAL_YAML


@given("an action YAML string with all optional fields populated")
def step_given_full_yaml(context: Context) -> None:
    """Provide a fully populated action YAML string."""
    context.action_yaml_string = _FULL_YAML


@given("an action YAML string with typed arguments")
def step_given_arguments_yaml(context: Context) -> None:
    """Provide an action YAML with typed arguments."""
    context.action_yaml_string = _ARGUMENTS_YAML


@given('the action YAML file "{rel_path}"')
def step_given_action_yaml_file(context: Context, rel_path: str) -> None:
    """Set the path to an action YAML file relative to project root."""
    project_root = Path(__file__).resolve().parents[2]
    context.action_yaml_file = str(project_root / rel_path)


@given('an action YAML string missing the "{field}" field')
def step_given_yaml_missing_field(context: Context, field: str) -> None:
    """Provide YAML with a specific required field removed."""
    context.action_yaml_string = _make_yaml_missing(field)


@given('an action YAML string with name "{name}"')
def step_given_yaml_with_name(context: Context, name: str) -> None:
    """Provide YAML with a custom name value."""
    context.action_yaml_string = _MINIMAL_YAML.replace("local/simple-action", name)


@given('an action YAML string with an argument of type "{arg_type}"')
def step_given_yaml_with_bad_arg_type(context: Context, arg_type: str) -> None:
    """Provide YAML with an invalid argument type."""
    context.action_yaml_string = f"""\
name: local/bad-arg
description: Action with bad argument type
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: Done
arguments:
  - name: bad_arg
    type: {arg_type}
    required: true
"""


@given('an action YAML string with state "{state}"')
def step_given_yaml_with_state(context: Context, state: str) -> None:
    """Provide YAML with a specific state value."""
    context.action_yaml_string = _MINIMAL_YAML.rstrip() + f"\nstate: {state}\n"


@given('an action YAML string with an argument named "{name}"')
def step_given_yaml_with_bad_arg_name(context: Context, name: str) -> None:
    """Provide YAML with an invalid argument name."""
    context.action_yaml_string = f"""\
name: local/bad-argname
description: Action with bad argument name
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: Done
arguments:
  - name: "{name}"
    type: string
    required: true
"""


@given("a None action YAML string")
def step_given_none_yaml(context: Context) -> None:
    """Set YAML string to None for None guard testing."""
    context.action_yaml_string = None  # type: ignore[assignment]


@given("an empty action YAML string")
def step_given_empty_yaml(context: Context) -> None:
    """Set YAML string to empty for empty guard testing."""
    context.action_yaml_string = ""


@given("an action YAML string that is a list")
def step_given_yaml_list(context: Context) -> None:
    """Set YAML string to a list instead of a mapping."""
    context.action_yaml_string = "- item1\n- item2\n"


@given("a None action YAML file path")
def step_given_none_file_path(context: Context) -> None:
    """Set file path to None for None guard testing."""
    context.action_yaml_file = None  # type: ignore[assignment]


@given('the action YAML directory path "{dir_path}"')
def step_given_yaml_directory_path(context: Context, dir_path: str) -> None:
    """Set file path to a directory for directory guard testing."""
    project_root = Path(__file__).resolve().parents[2]
    context.action_yaml_file = str(project_root / dir_path)


@given("an action YAML string with empty invariant strings")
def step_given_yaml_with_empty_invariants(context: Context) -> None:
    """Provide YAML with invariants that are empty or blank."""
    context.action_yaml_string = (
        _MINIMAL_YAML.rstrip() + '\ninvariants:\n  - ""\n  - "   "\n'
    )


@given("an action YAML string with duplicate invariants")
def step_given_yaml_with_duplicate_invariants(context: Context) -> None:
    """Provide YAML with duplicate invariant entries."""
    context.action_yaml_string = (
        _MINIMAL_YAML.rstrip()
        + '\ninvariants:\n  - "No secrets in code"\n  - "All tests must pass"\n  - "No secrets in code"\n'
    )


@given("an action YAML string with whitespace-padded invariants")
def step_given_yaml_with_whitespace_invariants(context: Context) -> None:
    """Provide YAML with invariants that have leading/trailing whitespace."""
    context.action_yaml_string = (
        _MINIMAL_YAML.rstrip() + '\ninvariants:\n  - "  Keep it clean  "\n'
    )


@given("an action YAML string with camelCase keys")
def step_given_yaml_with_camel_case(context: Context) -> None:
    """Provide YAML using camelCase key names."""
    context.action_yaml_string = """\
name: local/camel-action
description: Action with camelCase keys
strategyActor: openai/gpt-4
executionActor: openai/gpt-4
definitionOfDone: All tests pass
"""


@given('an action YAML string using env var "${{{var}}}" for strategy_actor')
def step_given_yaml_with_env_var(context: Context, var: str) -> None:
    """Provide YAML with environment variable reference."""
    context.action_yaml_string = f"""\
name: local/env-action
description: Action using env vars
strategy_actor: ${{{var}}}
execution_actor: openai/gpt-4
definition_of_done: All tests pass
"""


# ────────────────────────────────────────────────────────────
# When steps
# ────────────────────────────────────────────────────────────


@when("I validate the action schema")
def step_when_validate_action_schema(context: Context) -> None:
    """Validate the action YAML string via ActionConfigSchema."""
    try:
        context.action_config = ActionConfigSchema.from_yaml(context.action_yaml_string)
        context.action_schema_error = None
    except (ValidationError, ValueError) as exc:
        context.action_config = None
        context.action_schema_error = str(exc)


@when("I validate the action schema from file")
def step_when_validate_from_file(context: Context) -> None:
    """Validate an action YAML file via ActionConfigSchema."""
    try:
        context.action_config = ActionConfigSchema.from_yaml_file(
            context.action_yaml_file
        )
        context.action_schema_error = None
    except (ValidationError, ValueError, FileNotFoundError) as exc:
        context.action_config = None
        context.action_schema_error = str(exc)


@when("I validate the action schema expecting failure")
def step_when_validate_expecting_failure(context: Context) -> None:
    """Validate the action YAML, expecting it to fail."""
    try:
        context.action_config = ActionConfigSchema.from_yaml(context.action_yaml_string)
        context.action_schema_error = None
    except (ValidationError, ValueError) as exc:
        context.action_config = None
        context.action_schema_error = str(exc)


@when("I validate the action schema from file expecting failure")
def step_when_validate_file_expecting_failure(context: Context) -> None:
    """Validate an action YAML file, expecting it to fail."""
    try:
        context.action_config = ActionConfigSchema.from_yaml_file(
            context.action_yaml_file
        )
        context.action_schema_error = None
    except (ValidationError, ValueError, FileNotFoundError, TypeError) as exc:
        context.action_config = None
        context.action_schema_error = str(exc)


# ────────────────────────────────────────────────────────────
# Then steps
# ────────────────────────────────────────────────────────────


@then("the action schema validation should succeed")
def step_then_validation_succeeds(context: Context) -> None:
    """Assert that schema validation succeeded."""
    assert context.action_config is not None, (
        f"Expected validation to succeed but got error: {context.action_schema_error}"
    )


@then("the action schema validation should fail")
def step_then_validation_fails(context: Context) -> None:
    """Assert that schema validation failed."""
    assert context.action_config is None, "Expected validation to fail but it succeeded"
    assert context.action_schema_error is not None


@then('the action schema error should mention "{text}"')
def step_then_error_mentions(context: Context, text: str) -> None:
    """Assert the error message contains expected text."""
    assert context.action_schema_error is not None, "No error was captured"
    assert text.lower() in context.action_schema_error.lower(), (
        f"Expected error to mention '{text}', got: {context.action_schema_error}"
    )


@then('the action config name should be "{expected}"')
def step_then_config_name(context: Context, expected: str) -> None:
    """Assert the parsed config name."""
    assert context.action_config is not None
    assert context.action_config.name == expected


@then('the action config strategy_actor should be "{expected}"')
def step_then_config_strategy_actor(context: Context, expected: str) -> None:
    """Assert the parsed strategy_actor."""
    assert context.action_config is not None
    assert context.action_config.strategy_actor == expected


@then('the action config execution_actor should be "{expected}"')
def step_then_config_execution_actor(context: Context, expected: str) -> None:
    """Assert the parsed execution_actor."""
    assert context.action_config is not None
    assert context.action_config.execution_actor == expected


@then('the action config estimation_actor should be "{expected}"')
def step_then_config_estimation_actor(context: Context, expected: str) -> None:
    """Assert the parsed estimation_actor."""
    assert context.action_config is not None
    assert context.action_config.estimation_actor == expected


@then('the action config review_actor should be "{expected}"')
def step_then_config_review_actor(context: Context, expected: str) -> None:
    """Assert the parsed review_actor."""
    assert context.action_config is not None
    assert context.action_config.review_actor == expected


@then('the action config apply_actor should be "{expected}"')
def step_then_config_apply_actor(context: Context, expected: str) -> None:
    """Assert the parsed apply_actor."""
    assert context.action_config is not None
    assert context.action_config.apply_actor == expected


@then('the action config invariant_actor should be "{expected}"')
def step_then_config_invariant_actor(context: Context, expected: str) -> None:
    """Assert the parsed invariant_actor."""
    assert context.action_config is not None
    assert context.action_config.invariant_actor == expected


@then("the action config reusable should be true")
def step_then_reusable_true(context: Context) -> None:
    """Assert reusable is True."""
    assert context.action_config is not None
    assert context.action_config.reusable is True


@then("the action config read_only should be false")
def step_then_read_only_false(context: Context) -> None:
    """Assert read_only is False."""
    assert context.action_config is not None
    assert context.action_config.read_only is False


@then('the action config state should be "{expected}"')
def step_then_config_state(context: Context, expected: str) -> None:
    """Assert the parsed state."""
    assert context.action_config is not None
    assert context.action_config.state == expected


@then("the action config invariants list should be empty")
def step_then_invariants_empty(context: Context) -> None:
    """Assert invariants list is empty."""
    assert context.action_config is not None
    assert context.action_config.invariants == []


@then("the action config arguments list should be empty")
def step_then_arguments_empty(context: Context) -> None:
    """Assert arguments list is empty."""
    assert context.action_config is not None
    assert context.action_config.arguments == []


@then("the action config should have {count:d} arguments")
def step_then_argument_count(context: Context, count: int) -> None:
    """Assert the number of parsed arguments."""
    assert context.action_config is not None
    assert len(context.action_config.arguments) == count


@then('action argument {idx:d} name should be "{expected}"')
def step_then_argument_name(context: Context, idx: int, expected: str) -> None:
    """Assert an argument name by index."""
    assert context.action_config is not None
    assert context.action_config.arguments[idx].name == expected


@then('action argument {idx:d} type should be "{expected}"')
def step_then_argument_type(context: Context, idx: int, expected: str) -> None:
    """Assert an argument type by index."""
    assert context.action_config is not None
    assert context.action_config.arguments[idx].type == expected


@then("the action config should have {count:d} invariants")
def step_then_invariant_count(context: Context, count: int) -> None:
    """Assert the number of parsed invariants."""
    assert context.action_config is not None
    assert len(context.action_config.invariants) == count


@then('action invariant {idx:d} should be "{expected}"')
def step_then_invariant_value(context: Context, idx: int, expected: str) -> None:
    """Assert an invariant value by index."""
    assert context.action_config is not None
    assert context.action_config.invariants[idx] == expected


@then('the action config model_dump should contain key "{key}"')
def step_then_model_dump_contains_key(context: Context, key: str) -> None:
    """Assert the model_dump dict contains a key."""
    assert context.action_config is not None
    data: dict[str, Any] = context.action_config.model_dump()
    assert key in data, f"Key '{key}' not found in model_dump: {list(data.keys())}"
