"""Step definitions for PlanApplyService rendering coverage tests."""

from __future__ import annotations

import datetime
from typing import TYPE_CHECKING, Any

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

if TYPE_CHECKING:
    pass


# ---------------------------------------------------------------------------
# Helpers — build plain data dicts for the render functions
# ---------------------------------------------------------------------------


def _make_diff_data(
    mode: str = "revert",
    original_decision_id: str = "dec-0",
    new_decision_id: str | None = "dec-1",
    guidance: str = "",
    completed_at: bool = False,
    patched_hint: str | None = None,
    artifacts_path: str | None = None,
) -> dict[str, Any]:
    """Build a minimal correction-diff data dict for render function tests."""
    now_iso = datetime.datetime.now(datetime.UTC).isoformat()

    summary: dict[str, Any] = {
        "correction": "corr-test-001",
        "original_decision": original_decision_id,
        "mode": mode,
        "state": "complete" if completed_at else "executing",
        "guidance": guidance or "",
        "created_at": now_iso,
        "completed_at": now_iso if completed_at else None,
    }

    reverted_decisions: list[dict[str, Any]] = []
    added_decisions: list[dict[str, Any]] = []
    if original_decision_id:
        reverted_decisions.append(
            {"decision_id": original_decision_id, "status": "reverted"}
        )
    if new_decision_id:
        added_decisions.append({"decision_id": new_decision_id, "status": "added"})

    comparison = {
        "reverted_decisions": reverted_decisions,
        "added_decisions": added_decisions,
        "decisions_changed": len(reverted_decisions) + len(added_decisions),
        "decisions_added": len(added_decisions),
        "decisions_reverted": len(reverted_decisions),
    }

    if patched_hint is None and artifacts_path is None:
        patch_preview = [
            {
                "note": (
                    "Patch preview is available after correction execution completes. "
                    "Current state: executing"
                ),
            }
        ]
    elif artifacts_path:
        patch_preview = [
            {
                "note": "Corrected execution artifacts available",
                "artifacts_path": artifacts_path,
            }
        ]
    else:
        patch_preview = [{"note": patched_hint}]

    return {
        "correction_diff": summary,
        "comparison": comparison,
        "patch_preview": patch_preview,
    }


def _import_render_functions() -> tuple:
    """Lazy-import the render functions."""
    from cleveragents.application.services import plan_apply_service as mod

    return (
        mod._render_correction_diff_plain,
        mod._render_correction_diff_rich,
        mod._build_correction_diff_dict,
    )


# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------


@given(
    'I have a CorrectionAttemptRecord with mode="{mode}", '
    'original_decision_id="{orig}", new_decision_id="{new}", '
    'guidance="{guidance}", and archived_artifacts_path="{artifacts}"'
)
def step_corr_record_with_artifact(
    context: Context,
    mode: str,
    orig: str,
    new: str,
    guidance: str,
    artifacts: str,
) -> None:
    """Construct a minimal CorrectionAttemptRecord for _build_correction_diff_dict."""
    from cleveragents.domain.models.core.correction import (
        CorrectionAttemptRecord,
        CorrectionAttemptState,
        CorrectionMode,
    )

    record = CorrectionAttemptRecord(
        correction_attempt_id="test-corr-001",
        plan_id="plan-001",
        original_decision_id=orig,
        new_decision_id=new if new != "none" else None,
        mode=CorrectionMode.REVERT if mode == "revert" else CorrectionMode.APPEND,
        guidance=guidance or "",
        archived_artifacts_path=artifacts if artifacts not in ("", "none") else None,
        state=CorrectionAttemptState.COMPLETE,
    )
    context.corr_record: CORRECTIONATTEMPTRECORD = record  # noqa: F821


@given(
    'I have a CorrectionAttemptRecord with mode="{mode}", '
    'original_decision_id="{orig}", no new_decision, '
    'guidance="{guidance}", and no archived artifacts'
)
def step_corr_record_no_artifact(
    context: Context, mode: str, orig: str, guidance: str
) -> None:
    """Construct a CorrectionAttemptRecord without new_decision or archived artifacts."""
    from cleveragents.domain.models.core.correction import (
        CorrectionAttemptRecord,
        CorrectionAttemptState,
        CorrectionMode,
    )

    record = CorrectionAttemptRecord(
        correction_attempt_id="test-corr-002",
        plan_id="plan-002",
        original_decision_id=orig,
        new_decision_id=None,
        mode=CorrectionMode.REVERT if mode == "revert" else CorrectionMode.APPEND,
        guidance=guidance or "",
        archived_artifacts_path=None,
        state=CorrectionAttemptState.PENDING,
    )
    context.corr_record: CORRECTIONATTEMPTRECORD = record  # noqa: F821


@given(
    'I have a correction diff dict with mode="{mode}", '
    'original_decision_id="{orig}", guidance="{guidance}", '
    'completed_at present, and one added decision "{added}"'
)
def step_diff_dict_with_artifacts(
    context: Context, mode: str, orig: str, guidance: str, added: str
) -> None:
    """Build a plain data dict for render function tests."""
    # Use the added value as new_decision_id if provided
    nid = added if added != "none" and added else None
    data = _make_diff_data(
        mode=mode,
        original_decision_id=orig,
        new_decision_id=nid,
        guidance=guidance,
        completed_at=True,
    )
    context.diff_data = data


@given(
    'I have a correction diff dict with mode="{mode}", '
    'original_decision_id="{orig}", no guidance text, '
    'and patch preview note about "{hint}"'
)
def step_diff_dict_no_guidance(
    context: Context, mode: str, orig: str, hint: str
) -> None:
    """Build a plain data dict with missing fields."""
    data = _make_diff_data(
        mode=mode,
        original_decision_id=orig,
        new_decision_id=None,
        guidance="",
        completed_at=False,
        patched_hint=f"Patch preview is available after correction execution completes. "
        f"Current state: {hint}",
    )
    context.diff_data = data


# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------


@when("I call _build_correction_diff_dict on the record")
def step_build_corr_dict(context: Context) -> None:
    """Call the production _build_correction_diff_dict function & save dict output."""
    _, _, build_func = _import_render_functions()
    record = context.corr_record  # type: ignore[attr-defined]
    data = build_func(record)
    context.diff_data = data


@when("I call _render_correction_diff_plain on the dict")
def step_render_plain(context: Context) -> None:
    """Call plain-text renderer and save output."""
    render_plain, _, _ = _import_render_functions()
    context.render_output = render_plain(context.diff_data)  # type: ignore[attr-defined]


@when("I call _render_correction_diff_rich on the dict")
def step_render_rich(context: Context) -> None:
    """Call rich-text renderer and save output."""
    _, render_rich, _ = _import_render_functions()
    context.render_output = render_rich(context.diff_data)  # type: ignore[attr-defined]


# ---------------------------------------------------------------------------
# Then steps — verify structure & content
# ---------------------------------------------------------------------------


@then('the result should contain "{key1}", "{key2}", and "{key3}" keys')
def step_result_contains_keys(
    context: Context, key1: str, key2: str, key3: str
) -> None:
    """Verify the data dict has the expected top-level keys."""
    for k in (key1, key2, key3):
        assert k in context.diff_data, f"Missing key '{k}' in result"


@then(
    "the comparison should have {n_reverted:d} reverted decision and "
    "{n_added:d} added decisions"
)
def step_comparison_count(context: Context, n_reverted: int, n_added: int) -> None:
    """Verify the number of reverted/added decisions in comparison."""
    cmp = context.diff_data["comparison"]
    actual_reverted = len(cmp["reverted_decisions"])
    actual_added = len(cmp["added_decisions"])
    assert actual_reverted == n_reverted, (
        f"Expected {n_reverted} reverted, got {actual_reverted}"
    )
    assert actual_added == n_added, f"Expected {n_added} added, got {actual_added}"


@then('the patch preview should include the artifacts path "{path}"')
def step_patch_has_artifacts(context: Context, path: str) -> None:
    """Verify patch-preview contains the specified artifacts path."""
    patch = context.diff_data["patch_preview"][0]
    assert patch.get("artifacts_path") == path, (
        f"Expected artifacts_path='{path}', got {patch}"
    )


@then("the patch preview should indicate correction execution is pending")
def step_patch_pending(context: Context) -> None:
    """Verify that the patch-preview note signals a pending/complete state."""
    patch = context.diff_data["patch_preview"][0]
    note_lower = patch.get("note", "").lower()
    assert "pending" in note_lower or "complete" in note_lower, (
        f"Expected pending/complete hint in: {patch['note']}"
    )


@then('the output should contain "{text}" heading')
def step_output_contains_heading(context: Context, text: str) -> None:
    """Verify the rendered output contains a given heading string."""
    assert text in context.render_output, (  # type: ignore[attr-defined]
        f"Expected '{text}' in render output:\n{context.render_output}"
    )


@then('the output should contain "{value}" mode value')
def step_output_contains_mode(context: Context, value: str) -> None:
    """Verify the rendered output contains a given mode string."""
    assert value in context.render_output, (  # type: ignore[attr-defined]
        f"Expected '{value}' in render output:\n{context.render_output}"
    )


@then('the output should contain "{text}" trailer')
def step_output_contains_trailer(context: Context, text: str) -> None:
    """Verify the rendered output contains an end marker string."""
    assert text in context.render_output, (  # type: ignore[attr-defined]
        f"Expected '{text}' in render output:\n{context.render_output}"
    )


@then('the output should contain the added decision line starting with "{marker} {id}"')
def step_output_contains_added_decision(context: Context, marker: str, id: str) -> None:
    """Verify rendered output contains an added-decision line."""
    for line in context.render_output.splitlines():  # type: ignore[attr-defined]
        stripped = line.strip()
        if f"+ {id}" in stripped or f"{marker} {id}" in stripped:
            break
    else:
        raise AssertionError(
            f"Expected added decision '{marker} {id}' in output:\n{context.render_output}"
        )


@then('the output should NOT contain "{text}" line')
def step_output_not_contains(context: Context, text: str) -> None:
    """Verify the rendered output does NOT contain a given text string."""
    for line in context.render_output.splitlines():  # type: ignore[attr-defined]
        assert text not in line.rstrip(), f"Unexpected '{text}' found in line: {line}"


@then("the output should contain the pending-execution note text")
def step_output_contains_pending_note(context: Context) -> None:
    """Verify render output includes the patch-preview hint."""
    assert "Patch preview is available after" in context.render_output, (  # type: ignore[attr-defined]
        f"Expected pending-note: {context.render_output}"
    )


@then("the output should contain a red-colored reverted decision ID")
def step_output_contains_red_revert(context: Context) -> None:
    """Verify rich-rendered output has the [red] markup for reverted decisions."""
    assert "[red]" in context.render_output, (  # type: ignore[attr-defined]
        f"Expected '[red]' markup: {context.render_output}"
    )


@then("the output should contain a green-colored added decision ID")
def step_output_contains_green_added(context: Context) -> None:
    """Verify rich-rendered output has the [green] markup for added decisions."""
    assert "[green]" in context.render_output, (  # type: ignore[attr-defined]
        f"Expected '[green]' markup: {context.render_output}"
    )


@then('the output should contain "{marker} OK {marker}" checkmark marker at end')
def step_output_contains_marker(context: Context, marker: str) -> None:
    """Verify the rich-rendered output includes the [green]✔ OK footer."""
    assert "OK" in context.render_output, (  # type: ignore[attr-defined]
        f"Expected 'OK' footer: {context.render_output}"
    )


@then("the output should contain the pending-execution note in dim markup")
def step_pending_in_dim(context: Context) -> None:
    """Verify rendered text includes '[dim]' color wrapper."""
    assert "[dim]" in context.render_output, (  # type: ignore[attr-defined]
        f"Expected '[dim]' markup: {context.render_output}"
    )


# ---------------------------------------------------------------------------
# Comparison-count shortcut — avoids ambiguity with the generic matcher below.
# ---------------------------------------------------------------------------


@then('the comparison counts are "{n_rev}" reverted decisions and "{n_add}" new ones')
def step_comparison_counts_shorthand(context: Context, n_rev: str, n_add: str) -> None:
    """Verify the comparison dict has the expected revert/add counts.

    This step uses literal string values from the Gherkin to disambiguate
    from the parameterised "should have {n:d} ..." matcher above.
    """
    cmp = context.diff_data["comparison"]
    actual_rev = len(cmp["reverted_decisions"])
    actual_add = len(cmp["added_decisions"])
    assert str(actual_rev) == n_rev, f"Expected {n_rev} reverted, got {actual_rev}"
    assert str(actual_add) == n_add, f"Expected {n_add} added, got {actual_add}"


# ---------------------------------------------------------------------------
# Singular / variant steps that the feature file uses
# ---------------------------------------------------------------------------


@then(
    "the comparison should have {n_reverted:d} reverted decision"
    " and {n_added:d} added decision"
)
def step_comparison_count_singular(
    context: Context, n_reverted: int, n_added: int
) -> None:
    """Singular variant of the comparison count step (feature uses singular)."""
    cmp = context.diff_data["comparison"]
    actual_reverted = len(cmp["reverted_decisions"])
    actual_added = len(cmp["added_decisions"])
    assert actual_reverted == n_reverted, (
        f"Expected {n_reverted} reverted, got {actual_reverted}"
    )
    assert actual_added == n_added, f"Expected {n_added} added, got {actual_added}"


@then("the patch preview should include the artifacts path")
def step_patch_has_any_artifacts(context: Context) -> None:
    """Verify patch-preview contains an artifacts_path (any value)."""
    patch = context.diff_data["patch_preview"][0]
    assert patch.get("artifacts_path"), (
        f"Expected artifacts_path to be set, got: {patch}"
    )


@then('the output should contain the mode value "{value}"')
def step_output_contains_mode_value(context: Context, value: str) -> None:
    """Verify rendered output contains the given mode value string."""
    assert value in context.render_output, (  # type: ignore[attr-defined]
        f"Expected mode value '{value}' in render output:\n{context.render_output}"
    )


@then('the output should contain "{text}" timestamp')
def step_output_contains_timestamp(context: Context, text: str) -> None:
    """Verify rendered output contains a given timestamp label string."""
    assert text in context.render_output, (  # type: ignore[attr-defined]
        f"Expected '{text}' in render output:\n{context.render_output}"
    )


@then('the output should NOT contain a "{text}" line')
def step_output_not_contains_a_line(context: Context, text: str) -> None:
    """Verify rendered output does NOT contain a given text (with 'a' article)."""
    for line in context.render_output.splitlines():  # type: ignore[attr-defined]
        assert text not in line.rstrip(), f"Unexpected '{text}' found in line: {line}"


@then('the output should contain "{text}" mode color markup')
def step_output_contains_mode_markup(context: Context, text: str) -> None:
    """Verify rendered output contains the given rich-markup mode string."""
    assert text in context.render_output, (  # type: ignore[attr-defined]
        f"Expected mode markup '{text}' in render output:\n{context.render_output}"
    )


@then("the output should contain the rich green checkmark marker at end")
def step_output_contains_checkmark(context: Context) -> None:
    """Verify rich-rendered output includes the green checkmark footer."""
    output = context.render_output  # type: ignore[attr-defined]
    assert "[green]" in output or "✔" in output or "OK" in output, (
        f"Expected green checkmark / OK footer: {output}"
    )


@given(
    'I have a correction diff dict with mode="{mode}", '
    'original_decision_id="{orig}", no guidance text, '
    "and patch preview note about pending execution"
)
def step_diff_dict_no_guidance_pending(context: Context, mode: str, orig: str) -> None:
    """Build a plain data dict with no guidance and a pending-execution hint."""
    data = _make_diff_data(
        mode=mode,
        original_decision_id=orig,
        new_decision_id=None,
        guidance="",
        completed_at=False,
        patched_hint=(
            "Patch preview is available after correction execution completes. "
            "Current state: executing"
        ),
    )
    context.diff_data = data


@given(
    'I have a correction diff dict with mode="{mode}", '
    'original_decision_id="{orig}", no guidance, '
    "and patch preview note about pending execution"
)
def step_diff_dict_no_guidance_short_pending(
    context: Context, mode: str, orig: str
) -> None:
    """Same as the 'no guidance text' variant - 'no guidance' short form."""
    data = _make_diff_data(
        mode=mode,
        original_decision_id=orig,
        new_decision_id=None,
        guidance="",
        completed_at=False,
        patched_hint=(
            "Patch preview is available after correction execution completes. "
            "Current state: executing"
        ),
    )
    context.diff_data = data
