"""Step definitions for plan diff and artifacts feature tests."""

from __future__ import annotations

import json

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

from cleveragents.application.services.plan_apply_service import (
    PlanApplyService,
)
from cleveragents.application.services.plan_lifecycle_service import (
    PlanLifecycleService,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import PlanError, ValidationError
from cleveragents.domain.models.core.change import (
    ChangeEntry,
    ChangeOperation,
    InMemoryChangeSetStore,
    SpecChangeSet,
)
from cleveragents.domain.models.core.plan import (
    PlanPhase,
    ProcessingState,
)


def _create_lifecycle_service() -> PlanLifecycleService:
    """Create a lifecycle service for testing."""
    settings = Settings()
    return PlanLifecycleService(settings=settings)


def _create_test_action_and_plan(
    service: PlanLifecycleService,
) -> str:
    """Create a test action and plan, returning the plan_id."""
    service.create_action(
        name="local/test-diff",
        description="Test diff action",
        definition_of_done="Tests pass",
        strategy_actor="local/planner",
        execution_actor="local/executor",
    )
    plan = service.use_action(action_name="local/test-diff")
    return plan.identity.plan_id


def _advance_plan_to_execute_complete(
    service: PlanLifecycleService,
    plan_id: str,
) -> None:
    """Advance a plan through strategize and execute to complete."""
    service.start_strategize(plan_id)
    service.complete_strategize(plan_id)
    plan = service.get_plan(plan_id)
    if plan.phase == PlanPhase.STRATEGIZE:
        service.execute_plan(plan_id)
    plan = service.get_plan(plan_id)
    if plan.phase == PlanPhase.EXECUTE:
        service.start_execute(plan_id)
        service.complete_execute(plan_id)


def _advance_plan_to_apply_processing(
    service: PlanLifecycleService,
    plan_id: str,
) -> None:
    """Advance a plan to apply/processing state."""
    _advance_plan_to_execute_complete(service, plan_id)
    plan = service.get_plan(plan_id)
    if plan.phase == PlanPhase.EXECUTE:
        service.apply_plan(plan_id)
    plan = service.get_plan(plan_id)
    if plan.phase == PlanPhase.APPLY and plan.state == ProcessingState.QUEUED:
        service.start_apply(plan_id)


def _create_test_changeset(plan_id: str) -> SpecChangeSet:
    """Create a test changeset with sample entries."""
    return SpecChangeSet(
        changeset_id="test-changeset-001",
        plan_id=plan_id,
        entries=[
            ChangeEntry(
                plan_id=plan_id,
                resource_id="res-001",
                tool_name="file-write",
                operation=ChangeOperation.CREATE,
                path="src/new_file.py",
                before_hash=None,
                after_hash="abc123def456",
            ),
            ChangeEntry(
                plan_id=plan_id,
                resource_id="res-002",
                tool_name="file-edit",
                operation=ChangeOperation.MODIFY,
                path="src/existing.py",
                before_hash="old111222333",
                after_hash="new444555666",
            ),
            ChangeEntry(
                plan_id=plan_id,
                resource_id="res-003",
                tool_name="file-delete",
                operation=ChangeOperation.DELETE,
                path="src/obsolete.py",
                before_hash="del789aaa",
                after_hash=None,
            ),
        ],
    )


# -- Background ---


@given("I have a plan apply service with an in-memory changeset store")
def step_have_apply_service(context: Context) -> None:
    context.lifecycle_service = _create_lifecycle_service()
    context.changeset_store = InMemoryChangeSetStore()
    context.apply_service = PlanApplyService(
        lifecycle_service=context.lifecycle_service,
        changeset_store=context.changeset_store,
    )
    context.plan_id = None
    context.changeset = None
    context.diff_output = None
    context.artifacts_output = None
    context.error = None
    context.guard_result = None


# -- Given steps ---


@given("a plan with a changeset containing file changes")
def step_plan_with_changeset(context: Context) -> None:
    plan_id = _create_test_action_and_plan(context.lifecycle_service)
    _advance_plan_to_execute_complete(context.lifecycle_service, plan_id)
    plan = context.lifecycle_service.get_plan(plan_id)
    changeset = _create_test_changeset(plan_id)
    # Store the changeset
    context.changeset_store._store[changeset.changeset_id] = changeset
    # Set changeset_id on plan
    plan.changeset_id = changeset.changeset_id
    plan.sandbox_refs = ["sandbox-ref-001", "sandbox-ref-002"]
    context.lifecycle_service._commit_plan(plan)
    context.plan_id = plan_id
    context.changeset = changeset


@given("a plan with no changeset")
def step_plan_with_no_changeset(context: Context) -> None:
    plan_id = _create_test_action_and_plan(context.lifecycle_service)
    context.plan_id = plan_id


@given("a plan with an empty changeset")
def step_plan_with_empty_changeset(context: Context) -> None:
    plan_id = _create_test_action_and_plan(context.lifecycle_service)
    _advance_plan_to_execute_complete(context.lifecycle_service, plan_id)
    plan = context.lifecycle_service.get_plan(plan_id)
    empty_cs = SpecChangeSet(
        changeset_id="empty-changeset-001",
        plan_id=plan_id,
        entries=[],
    )
    context.changeset_store._store[empty_cs.changeset_id] = empty_cs
    plan.changeset_id = empty_cs.changeset_id
    context.lifecycle_service._commit_plan(plan)
    context.plan_id = plan_id


@given("a plan with a changeset and validation summary")
def step_plan_with_validation(context: Context) -> None:
    plan_id = _create_test_action_and_plan(context.lifecycle_service)
    _advance_plan_to_execute_complete(context.lifecycle_service, plan_id)
    plan = context.lifecycle_service.get_plan(plan_id)
    changeset = _create_test_changeset(plan_id)
    context.changeset_store._store[changeset.changeset_id] = changeset
    plan.changeset_id = changeset.changeset_id
    plan.validation_summary = {
        "total": 3,
        "passed": 2,
        "failed": 1,
        "skipped": 0,
    }
    context.lifecycle_service._commit_plan(plan)
    context.plan_id = plan_id


@given("a plan in apply processing state")
def step_plan_in_apply_processing(context: Context) -> None:
    plan_id = _create_test_action_and_plan(context.lifecycle_service)
    _advance_plan_to_apply_processing(context.lifecycle_service, plan_id)
    context.plan_id = plan_id


# -- When steps ---


@when("I request the diff in rich format")
def step_request_diff_rich(context: Context) -> None:
    context.diff_output = context.apply_service.diff(context.plan_id, fmt="rich")


@when("I request the diff in plain format")
def step_request_diff_plain(context: Context) -> None:
    context.diff_output = _capture_service_output(
        context.apply_service.diff, context.plan_id, fmt="plain"
    )


@when("I request the diff in JSON format")
def step_request_diff_json(context: Context) -> None:
    context.diff_output = _capture_service_output(
        context.apply_service.diff, context.plan_id, fmt="json"
    )


@when("I request the diff in YAML format")
def step_request_diff_yaml(context: Context) -> None:
    context.diff_output = _capture_service_output(
        context.apply_service.diff, context.plan_id, fmt="yaml"
    )


@when("I try to get the diff")
def step_try_get_diff(context: Context) -> None:
    try:
        context.diff_output = context.apply_service.diff(context.plan_id)
        context.error = None
    except PlanError as e:
        context.error = e


@when("I request the artifacts")
def step_request_artifacts(context: Context) -> None:
    context.artifacts_output = _capture_service_output(
        context.apply_service.artifacts, context.plan_id
    )


def _capture_service_output(func, *args, **kwargs):
    """Call a service method capturing stdout (format_output writes there)."""
    from contextlib import redirect_stdout
    from io import StringIO

    buf = StringIO()
    with redirect_stdout(buf):
        result = func(*args, **kwargs)
    return result or buf.getvalue().rstrip("\n")


@when("I request the artifacts in JSON format")
def step_request_artifacts_json(context: Context) -> None:
    context.artifacts_output = _capture_service_output(
        context.apply_service.artifacts, context.plan_id, fmt="json"
    )


@when("I request the artifacts in plain format")
def step_request_artifacts_plain(context: Context) -> None:
    context.artifacts_output = _capture_service_output(
        context.apply_service.artifacts, context.plan_id, fmt="plain"
    )


@when("I request the artifacts in YAML format")
def step_request_artifacts_yaml(context: Context) -> None:
    context.artifacts_output = _capture_service_output(
        context.apply_service.artifacts, context.plan_id, fmt="yaml"
    )


@when("I check the empty changeset guard")
def step_check_guard(context: Context) -> None:
    try:
        context.guard_result = context.apply_service.guard_empty_changeset(
            context.plan_id
        )
        context.error = None
    except PlanError as e:
        context.error = e


@when("I check the empty changeset guard with allow_empty")
def step_check_guard_allow(context: Context) -> None:
    context.guard_result = context.apply_service.guard_empty_changeset(
        context.plan_id, allow_empty=True
    )


@when(
    "I persist the apply summary with {files:d} files and {validations:d} validations"
)
def step_persist_summary(context: Context, files: int, validations: int) -> None:
    context.plan = context.apply_service.persist_apply_summary(
        context.plan_id,
        files_changed=files,
        validations_run=validations,
    )


@when("a merge failure occurs with conflict details")
def step_merge_failure(context: Context) -> None:
    context.plan = context.apply_service.handle_merge_failure(
        context.plan_id,
        conflict_details="Conflict in src/main.py lines 10-20: both sides modified",
    )


@when("I try to create PlanApplyService with None lifecycle")
def step_create_with_none(context: Context) -> None:
    try:
        PlanApplyService(lifecycle_service=None)  # type: ignore[arg-type]
        context.error = None
    except ValidationError as e:
        context.error = e


# -- Then steps ---


@then("the diff output should contain the changeset ID")
def step_diff_contains_changeset_id(context: Context) -> None:
    assert context.diff_output is not None
    assert "test-changeset-001" in context.diff_output


@then("the diff output should contain file paths")
def step_diff_contains_paths(context: Context) -> None:
    assert "src/new_file.py" in context.diff_output
    assert "src/existing.py" in context.diff_output


@then("the diff output should contain operation labels")
def step_diff_contains_ops(context: Context) -> None:
    assert context.diff_output is not None
    # Rich format uses operation labels
    has_label = (
        "new file" in context.diff_output
        or "modified" in context.diff_output
        or "deleted" in context.diff_output
    )
    assert has_label, f"No operation label found in: {context.diff_output[:200]}"


@then("the diff output should contain unified diff markers")
def step_diff_contains_markers(context: Context) -> None:
    assert "---" in context.diff_output
    assert "+++" in context.diff_output
    assert "@@" in context.diff_output


@then("the diff output should contain the plan ID")
def step_diff_contains_plan_id(context: Context) -> None:
    assert context.plan_id is not None
    assert context.plan_id in context.diff_output


@then("the diff output should be valid JSON")
def step_diff_valid_json(context: Context) -> None:
    parsed = json.loads(context.diff_output)
    assert isinstance(parsed, dict)
    context.parsed_json = parsed


@then("the JSON diff should contain entries list")
def step_json_has_entries(context: Context) -> None:
    parsed = json.loads(context.diff_output)
    assert "entries" in parsed
    assert isinstance(parsed["entries"], list)
    assert len(parsed["entries"]) > 0


@then("the JSON diff should contain summary counts")
def step_json_has_summary(context: Context) -> None:
    parsed = json.loads(context.diff_output)
    assert "summary" in parsed
    summary = parsed["summary"]
    assert "total" in summary
    assert "creates" in summary


@then("the diff output should contain YAML keys")
def step_diff_yaml_keys(context: Context) -> None:
    assert "changeset_id" in context.diff_output
    assert "plan_id" in context.diff_output


@then("a plan error about missing changeset should be raised")
def step_error_missing_changeset(context: Context) -> None:
    assert context.error is not None
    assert isinstance(context.error, PlanError)
    assert "ChangeSet" in context.error.message


@then("the diff output should indicate no changes")
def step_diff_no_changes(context: Context) -> None:
    assert context.diff_output is not None
    assert (
        "No changes" in context.diff_output
        or "no changes" in context.diff_output.lower()
    )


@then("the artifacts should contain the plan ID")
def step_artifacts_has_plan_id(context: Context) -> None:
    assert context.plan_id in context.artifacts_output


@then("the artifacts should contain the changeset ID")
def step_artifacts_has_changeset_id(context: Context) -> None:
    assert "test-changeset-001" in context.artifacts_output


@then("the artifacts should contain sandbox refs")
def step_artifacts_has_sandbox_refs(context: Context) -> None:
    assert "sandbox-ref-001" in context.artifacts_output


@then("the artifacts should contain files changed list")
def step_artifacts_has_files(context: Context) -> None:
    assert (
        "src/new_file.py" in context.artifacts_output
        or "new_file" in context.artifacts_output
    )


@then("the artifacts JSON should contain validation summary")
def step_artifacts_json_validation(context: Context) -> None:
    parsed = json.loads(context.artifacts_output)
    assert "validation_summary" in parsed
    assert parsed["validation_summary"]["total"] == 3


@then("the artifacts should show null changeset")
def step_artifacts_null_changeset(context: Context) -> None:
    assert context.artifacts_output is not None
    # Should show null/None for changeset summary
    assert "null" in context.artifacts_output or "None" in context.artifacts_output


@then("a plan error about empty changeset should be raised")
def step_error_empty_changeset(context: Context) -> None:
    assert context.error is not None
    assert isinstance(context.error, PlanError)
    assert "empty" in context.error.message.lower()


@then("the guard should return True")
def step_guard_true(context: Context) -> None:
    assert context.guard_result is True


@then("the plan metadata should contain apply_files_changed of {count:d}")
def step_metadata_files(context: Context, count: int) -> None:
    plan = context.lifecycle_service.get_plan(context.plan_id)
    assert plan.error_details is not None
    assert plan.error_details["apply_files_changed"] == str(count)


@then("the plan metadata should contain apply_validations_run of {count:d}")
def step_metadata_validations(context: Context, count: int) -> None:
    plan = context.lifecycle_service.get_plan(context.plan_id)
    assert plan.error_details is not None
    assert plan.error_details["apply_validations_run"] == str(count)


@then("the plan metadata should contain apply_completed_at timestamp")
def step_metadata_timestamp(context: Context) -> None:
    plan = context.lifecycle_service.get_plan(context.plan_id)
    assert plan.error_details is not None
    assert "apply_completed_at" in plan.error_details


@then("the plan should be in an errored processing state")
def step_plan_errored(context: Context) -> None:
    plan = context.lifecycle_service.get_plan(context.plan_id)
    assert plan.processing_state == ProcessingState.ERRORED


@then("the plan error message should contain merge conflict info")
def step_error_merge_info(context: Context) -> None:
    plan = context.lifecycle_service.get_plan(context.plan_id)
    assert plan.error_message is not None
    assert "Merge failed" in plan.error_message


@then("the plan metadata should contain merge_conflict details")
def step_metadata_merge_conflict(context: Context) -> None:
    plan = context.lifecycle_service.get_plan(context.plan_id)
    assert plan.error_details is not None
    assert "merge_conflict" in plan.error_details


@then("the plan metadata should contain sandbox_rollback pending")
def step_metadata_rollback(context: Context) -> None:
    plan = context.lifecycle_service.get_plan(context.plan_id)
    assert plan.error_details is not None
    assert plan.error_details.get("sandbox_rollback") == "pending"


@then("a validation error should be raised for lifecycle service")
def step_validation_error(context: Context) -> None:
    assert context.error is not None
    assert isinstance(context.error, ValidationError)


@then("the artifacts output should contain key-value pairs")
def step_artifacts_kv(context: Context) -> None:
    assert context.artifacts_output is not None
    assert "plan_id" in context.artifacts_output


@then("the artifacts output should contain YAML structure")
def step_artifacts_yaml(context: Context) -> None:
    assert context.artifacts_output is not None
    assert "plan_id" in context.artifacts_output
    assert "phase" in context.artifacts_output


# -- Additional Given steps for coverage ---


@given("a plan with a changeset and apply summary metadata")
def step_plan_with_apply_metadata(context: Context) -> None:
    """Create a plan with changeset and apply summary stored in error_details."""
    plan_id = _create_test_action_and_plan(context.lifecycle_service)
    _advance_plan_to_execute_complete(context.lifecycle_service, plan_id)
    plan = context.lifecycle_service.get_plan(plan_id)
    changeset = _create_test_changeset(plan_id)
    context.changeset_store._store[changeset.changeset_id] = changeset
    plan.changeset_id = changeset.changeset_id
    plan.sandbox_refs = ["sandbox-ref-001"]
    # Simulate apply summary metadata (as persist_apply_summary would set)
    if plan.error_details is None:
        plan.error_details = {}
    plan.error_details["apply_files_changed"] = "7"
    plan.error_details["apply_validations_run"] = "4"
    context.lifecycle_service._commit_plan(plan)
    context.plan_id = plan_id


@given("a plan with a changeset ID but store has no entry")
def step_plan_with_changeset_id_but_no_store_entry(context: Context) -> None:
    """Create a plan whose changeset_id points to a non-existent store entry."""
    plan_id = _create_test_action_and_plan(context.lifecycle_service)
    _advance_plan_to_execute_complete(context.lifecycle_service, plan_id)
    plan = context.lifecycle_service.get_plan(plan_id)
    # Set changeset_id but do NOT add anything to the store
    plan.changeset_id = "missing-changeset-999"
    context.lifecycle_service._commit_plan(plan)
    context.plan_id = plan_id


# -- Additional Then steps for coverage ---


@then("the artifacts JSON should contain apply summary")
def step_artifacts_json_apply_summary(context: Context) -> None:
    parsed = json.loads(context.artifacts_output)
    assert "apply_summary" in parsed
    assert parsed["apply_summary"]["files_changed"] == "7"
    assert parsed["apply_summary"]["validations_run"] == "4"
