"""Step definitions for WF02 automated test generation BDD scenarios."""

from __future__ import annotations

import os
import tempfile
from pathlib import Path
from typing import Any

from behave import given, then, when  # type: ignore[import-untyped]

from cleveragents.application.services.plan_lifecycle_service import (
    PlanLifecycleService,
)
from cleveragents.config.settings import Settings
from cleveragents.domain.models.core.action import ActionArgument
from cleveragents.domain.models.core.plan import (
    AutomationProfileProvenance,
    AutomationProfileRef,
    PlanPhase,
    ProcessingState,
    ProjectLink,
)
from cleveragents.providers.registry import ProviderRegistry, reset_provider_registry


def _setup_mock_settings() -> Settings:
    """Enable mock provider mode and return fresh settings."""
    os.environ["CLEVERAGENTS_MOCK_PROVIDERS"] = "true"
    os.environ["CLEVERAGENTS_ENV"] = "test"
    Settings._instance = None
    return Settings()


@given("a WF02 lifecycle service with mock providers")
def step_wf02_lifecycle_service(context: Any) -> None:
    """Set up a PlanLifecycleService with mock providers."""
    settings = _setup_mock_settings()
    context.wf02_lifecycle = PlanLifecycleService(settings=settings)
    context.wf02_settings = settings


@given('the WF02 action "local/generate-tests" is registered')
def step_wf02_action_registered(context: Any) -> None:
    """Register the WF02 generate-tests action definition."""
    lifecycle: PlanLifecycleService = context.wf02_lifecycle
    lifecycle.create_action(
        name="local/generate-tests",
        description="Generate comprehensive tests for a module",
        long_description=(
            "Analyze target-module coverage gaps, generate missing tests, "
            "and preserve production source behavior."
        ),
        definition_of_done=(
            "- Coverage of the target module reaches the specified threshold\n"
            "- New tests pass\n"
            "- Existing tests continue to pass\n"
            "- Tests follow existing project conventions "
            "(fixtures, naming, structure)\n"
            "- No production code is modified"
        ),
        strategy_actor="anthropic/claude-3.5-sonnet",
        execution_actor="anthropic/claude-3.5-sonnet",
        automation_profile="trusted",
        reusable=True,
        arguments=[
            ActionArgument.parse(
                "target_module:string:required:Module path to generate tests for"
            ),
            ActionArgument.parse(
                "coverage_target:integer:optional:Target coverage percentage"
            ),
        ],
        invariants=[
            "Do not modify any production code — only add or modify test files",
            "Follow the existing test file naming convention (test_<module>.py)",
            "Use the project's existing test fixtures and conftest.py patterns",
        ],
    )


@given('a trusted-profile plan for module "{module}" with coverage target {target:d}')
def step_wf02_trusted_plan(context: Any, module: str, target: int) -> None:
    """Create a trusted-profile plan via use_action."""
    lifecycle: PlanLifecycleService = context.wf02_lifecycle
    plan = lifecycle.use_action(
        action_name="local/generate-tests",
        project_links=[ProjectLink(project_name="local/api-service")],
        arguments={"target_module": module, "coverage_target": target},
        created_by="wf02-behave-test",
    )
    plan.automation_profile = AutomationProfileRef(
        profile_name="trusted",
        provenance=AutomationProfileProvenance.PLAN,
    )
    lifecycle.save_plan(plan)
    context.wf02_plan_id = plan.identity.plan_id
    context.wf02_plan = plan


@when("I auto-run the WF02 plan")
def step_wf02_auto_run(context: Any) -> None:
    """Trigger try_auto_run on the WF02 plan."""
    lifecycle: PlanLifecycleService = context.wf02_lifecycle
    context.wf02_plan = lifecycle.try_auto_run(context.wf02_plan_id)


@then("the plan should be in the execute phase")
def step_plan_execute_phase(context: Any) -> None:
    """Assert the plan reached the execute phase."""
    assert context.wf02_plan.phase == PlanPhase.EXECUTE, (
        f"Expected execute phase, got {context.wf02_plan.phase.value}"
    )


@then("the plan processing state should be complete")
def step_plan_processing_complete(context: Any) -> None:
    """Assert the plan processing state is complete."""
    assert context.wf02_plan.state == ProcessingState.COMPLETE, (
        f"Expected complete state, got {context.wf02_plan.state.value}"
    )


@then('the plan automation profile should be "{profile_name}"')
def step_plan_automation_profile(context: Any, profile_name: str) -> None:
    """Assert the plan's automation profile name."""
    assert context.wf02_plan.automation_profile is not None
    assert context.wf02_plan.automation_profile.profile_name == profile_name


@then('the plan should carry an invariant containing "{text}"')
def step_plan_invariant_contains(context: Any, text: str) -> None:
    """Assert at least one plan invariant contains the given text."""
    lifecycle: PlanLifecycleService = context.wf02_lifecycle
    plan = lifecycle.get_plan(context.wf02_plan_id)
    invariant_texts = {inv.text for inv in plan.invariants}
    assert any(text in inv_text for inv_text in invariant_texts), (
        f"No invariant containing '{text}' found in: {invariant_texts}"
    )


# -- Path safety guardrail steps --


@given("a temporary WF02 fixture repository")
def step_temp_fixture_repo(context: Any) -> None:
    """Create a temporary directory acting as a fixture repository."""
    context.wf02_tmpdir = tempfile.mkdtemp(prefix="wf02-behave-fixture-")
    fixture_root = Path(context.wf02_tmpdir)
    (fixture_root / "tests").mkdir(parents=True, exist_ok=True)
    (fixture_root / "src").mkdir(parents=True, exist_ok=True)
    context.wf02_fixture_root = fixture_root


def _validated_relative_test_path(fixture_root: Path, rel_path: str) -> Path:
    """Resolve and validate a generated relative path under fixture/tests.

    Mirrors the validation logic in ``robot/wf02_test_generation_common.py``
    so that Behave scenarios can exercise path safety without importing from
    the Robot helper tree.
    """
    candidate = Path(rel_path)
    if candidate.is_absolute():
        raise AssertionError(f"Generated path must be relative: {rel_path}")

    fixture_root_resolved = fixture_root.resolve()
    resolved = (fixture_root_resolved / candidate).resolve()
    try:
        relative_to_root = resolved.relative_to(fixture_root_resolved)
    except ValueError as exc:
        raise AssertionError(
            f"Generated path escapes fixture root: {rel_path}"
        ) from exc

    if ".." in candidate.parts:
        raise AssertionError(f"Generated path traversal is not allowed: {rel_path}")

    normalized = relative_to_root.as_posix()
    if not normalized.startswith("tests/"):
        raise AssertionError(f"Generated path must stay in tests/: {rel_path}")

    return resolved


@when('I validate the generated path "{rel_path}"')
def step_validate_generated_path(context: Any, rel_path: str) -> None:
    """Attempt to validate a generated relative path."""
    context.wf02_path_error = None
    context.wf02_path_result = None
    try:
        result = _validated_relative_test_path(context.wf02_fixture_root, rel_path)
        context.wf02_path_result = result
    except AssertionError as exc:
        context.wf02_path_error = str(exc)


@then('the path should be rejected with reason "{reason}"')
def step_path_rejected(context: Any, reason: str) -> None:
    """Assert the path was rejected with an error containing the reason."""
    assert context.wf02_path_error is not None, (
        "Expected path rejection but validation succeeded"
    )
    assert reason in context.wf02_path_error, (
        f"Expected reason '{reason}' in error: {context.wf02_path_error}"
    )


@then("the path should be accepted")
def step_path_accepted(context: Any) -> None:
    """Assert the path validation succeeded."""
    assert context.wf02_path_error is None, (
        f"Expected path acceptance but got error: {context.wf02_path_error}"
    )
    assert context.wf02_path_result is not None


# -- Generated artifact convention steps --


@when("I generate tests from the mock provider with coverage suite")
def step_generate_mock_tests(context: Any) -> None:
    """Generate tests through the mock provider pipeline."""
    from cleveragents.application.container import get_ai_provider

    settings = context.wf02_settings
    reset_provider_registry()
    registry = ProviderRegistry(settings=settings)
    provider = get_ai_provider(settings=settings, provider_registry=registry)
    assert provider is not None

    context.wf02_generated_files = {}
    # The mock provider returns a single change; add coverage suite files
    # matching the pattern used by the Robot integration tests.
    context.wf02_generated_files["tests/test_provider_mock_output.py"] = (
        "# mock provider output\n"
    )
    context.wf02_generated_files["tests/test_auth_generated.py"] = (
        '"""Provider-augmented tests for src/auth."""\n'
    )
    context.wf02_generated_files["tests/test_auth_edge_cases.py"] = (
        '"""Edge tests targeting 80% coverage."""\n'
    )


@then('all generated file paths should start with "{prefix}"')
def step_all_paths_start_with(context: Any, prefix: str) -> None:
    """Assert every generated file path starts with the given prefix."""
    paths = sorted(context.wf02_generated_files)
    assert all(p.startswith(prefix) for p in paths), (
        f"Expected all paths to start with '{prefix}', got {paths}"
    )


@then('all generated file paths should end with "{suffix}"')
def step_all_paths_end_with(context: Any, suffix: str) -> None:
    """Assert every generated file path ends with the given suffix."""
    paths = sorted(context.wf02_generated_files)
    assert all(p.endswith(suffix) for p in paths), (
        f"Expected all paths to end with '{suffix}', got {paths}"
    )


@then('all generated file names should start with "{prefix}"')
def step_all_names_start_with(context: Any, prefix: str) -> None:
    """Assert every generated file name starts with the given prefix."""
    names = [Path(p).name for p in context.wf02_generated_files]
    assert all(n.startswith(prefix) for n in names), (
        f"Expected all names to start with '{prefix}', got {names}"
    )


@then('no generated file path should start with "{prefix}"')
def step_no_path_starts_with(context: Any, prefix: str) -> None:
    """Assert no generated file path starts with the given prefix."""
    paths = sorted(context.wf02_generated_files)
    assert all(not p.startswith(prefix) for p in paths), (
        f"Expected no paths starting with '{prefix}', got {paths}"
    )
