"""Step definitions for plan_cli_spec_print_r2.feature.

Targets remaining partial branches in
``cleveragents.cli.commands.plan`` (plan.py) - round 2, split 1 of 3.

Covers:
- ``_plan_spec_dict``: project link alias/read_only, automation_profile,
  invariants, validation_summary/dod, last_completed_step, last_checkpoint_id
- ``_print_lifecycle_plan``: definition_of_done, dod evaluation pass/fail,
  arguments with/without order, automation profile, invariants, resume
  metadata, project link alias/read_only, long description

All step text uses the ``r2plan-`` prefix to avoid collisions.
"""

from __future__ import annotations

from datetime import datetime
from io import StringIO
from typing import Any
from unittest.mock import patch

from behave import given, then, when
from typer.testing import CliRunner

from cleveragents.cli.commands import plan as plan_module
from cleveragents.cli.commands.plan import (
    _plan_spec_dict,
    _print_lifecycle_plan,
)
from cleveragents.domain.models.core.plan import (
    AutomationProfileProvenance,
    AutomationProfileRef,
    InvariantSource,
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanInvariant,
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
    ProjectLink,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_ULID_BASE = "01ARZ3NDEKTSV4RRFFQ69G5F"
_runner = CliRunner()


def _ulid(suffix: str = "A1") -> str:
    """Return a valid 26-char ULID for tests.

    ULIDs use Crockford's Base32 (0-9, A-H, J-K, M-N, P-T, V-Z; no I/L/O/U).
    """
    # Map potentially invalid chars to valid Crockford Base32
    cleaned = (
        suffix.replace("I", "J").replace("L", "K").replace("O", "P").replace("U", "V")
    )
    base = _ULID_BASE + cleaned
    return base[:26]


def _make_plan(
    *,
    plan_id: str | None = None,
    name: str = "local/r2-plan",
    description: str = "Test plan for r2 coverage",
    phase: PlanPhase = PlanPhase.STRATEGIZE,
    processing_state: ProcessingState = ProcessingState.QUEUED,
    project_links: list[ProjectLink] | None = None,
    automation_profile: AutomationProfileRef | None = None,
    invariants: list[PlanInvariant] | None = None,
    validation_summary: dict[str, Any] | None = None,
    error_message: str | None = None,
    last_completed_step: int = -1,
    last_checkpoint_id: str | None = None,
    definition_of_done: str | None = None,
    arguments: dict[str, Any] | None = None,
    arguments_order: list[str] | None = None,
    estimation_actor: str | None = None,
    invariant_actor: str | None = None,
    timestamps: PlanTimestamps | None = None,
    action_name: str = "local/test-action",
) -> Plan:
    if timestamps is None:
        timestamps = PlanTimestamps(
            created_at=datetime.now(),
            updated_at=datetime.now(),
        )
    return Plan(
        identity=PlanIdentity(plan_id=plan_id or _ulid("A1")),
        namespaced_name=NamespacedName.parse(name),
        action_name=action_name,
        description=description,
        definition_of_done=definition_of_done,
        phase=phase,
        processing_state=processing_state,
        strategy_actor=None,
        execution_actor=None,
        project_links=project_links or [],
        automation_profile=automation_profile,
        invariants=invariants or [],
        validation_summary=validation_summary,
        error_message=error_message,
        last_completed_step=last_completed_step,
        last_checkpoint_id=last_checkpoint_id,
        arguments=arguments or {},
        arguments_order=arguments_order or [],
        estimation_actor=estimation_actor,
        invariant_actor=invariant_actor,
        timestamps=timestamps,
        created_by=None,
        reusable=True,
        read_only=False,
    )


def _capture_print(plan: Any) -> str:
    """Call _print_lifecycle_plan and capture the console output."""
    buf = StringIO()
    from rich.console import Console as RichConsole

    test_console = RichConsole(file=buf, width=200, no_color=True)
    with patch.object(plan_module, "console", test_console):
        _print_lifecycle_plan(plan, title="R2 Test")
    return buf.getvalue()


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


@given('r2plan-a v3 Plan with a project link that has alias "{alias}"')
def step_plan_link_alias(context: Any, alias: str) -> None:
    link = ProjectLink(project_name="local/api", alias=alias)
    context.r2_plan = _make_plan(project_links=[link])


@given("r2plan-a v3 Plan with a project link that is read_only")
def step_plan_link_readonly(context: Any) -> None:
    link = ProjectLink(project_name="local/docs", read_only=True)
    context.r2_plan = _make_plan(project_links=[link])


@given("r2plan-a v3 Plan with a plain project link")
def step_plan_link_plain(context: Any) -> None:
    link = ProjectLink(project_name="local/plain")
    context.r2_plan = _make_plan(project_links=[link])


@given('r2plan-a v3 Plan with automation_profile "{profile}"')
def step_plan_with_automation_profile(context: Any, profile: str) -> None:
    ap = AutomationProfileRef(
        profile_name=profile,
        provenance=AutomationProfileProvenance.PLAN,
    )
    context.r2_plan = _make_plan(automation_profile=ap)


@given("r2plan-a v3 Plan without automation_profile")
def step_plan_without_automation_profile(context: Any) -> None:
    context.r2_plan = _make_plan(automation_profile=None)


@given("r2plan-a v3 Plan with invariants")
def step_plan_with_invariants(context: Any) -> None:
    invs = [
        PlanInvariant(text="No new warnings", source=InvariantSource.PLAN),
        PlanInvariant(text="Coverage >= 80%", source=InvariantSource.ACTION),
    ]
    context.r2_plan = _make_plan(invariants=invs)


@given("r2plan-a v3 Plan without invariants")
def step_plan_without_invariants(context: Any) -> None:
    context.r2_plan = _make_plan(invariants=[])


@given("r2plan-a v3 Plan with dod validation summary")
def step_plan_with_dod_validation(context: Any) -> None:
    vs = {
        "dod_evaluated": True,
        "dod_all_passed": True,
        "required_passed": 3,
        "required_failed": 0,
    }
    context.r2_plan = _make_plan(validation_summary=vs)


@given("r2plan-a v3 Plan without validation_summary")
def step_plan_without_validation_summary(context: Any) -> None:
    context.r2_plan = _make_plan(validation_summary=None)


@given("r2plan-a v3 Plan with last_completed_step {n:d}")
def step_plan_with_step(context: Any, n: int) -> None:
    context.r2_plan = _make_plan(last_completed_step=n)


@given("r2plan-a v3 Plan with last_completed_step default")
def step_plan_with_step_default(context: Any) -> None:
    context.r2_plan = _make_plan(last_completed_step=-1)


@given('r2plan-a v3 Plan with last_checkpoint_id "{chk}"')
def step_plan_with_checkpoint(context: Any, chk: str) -> None:
    context.r2_plan = _make_plan(last_checkpoint_id=chk)


@given("r2plan-a v3 Plan without last_checkpoint_id")
def step_plan_without_checkpoint(context: Any) -> None:
    context.r2_plan = _make_plan(last_checkpoint_id=None)


@given('r2plan-a v3 Plan with definition_of_done "{dod}"')
def step_plan_with_dod(context: Any, dod: str) -> None:
    context.r2_plan = _make_plan(definition_of_done=dod)


@given("r2plan-a v3 Plan with definition_of_done longer than 200 chars")
def step_plan_with_long_dod(context: Any) -> None:
    dod = "x" * 250
    context.r2_plan = _make_plan(definition_of_done=dod)


@given("r2plan-a v3 Plan without definition_of_done")
def step_plan_without_dod(context: Any) -> None:
    context.r2_plan = _make_plan(definition_of_done=None)


@given("r2plan-a v3 Plan with dod evaluated as passed")
def step_plan_dod_passed(context: Any) -> None:
    vs = {
        "dod_evaluated": True,
        "dod_all_passed": True,
        "required_passed": 5,
        "required_failed": 0,
    }
    context.r2_plan = _make_plan(validation_summary=vs)


@given("r2plan-a v3 Plan with dod evaluated as failed")
def step_plan_dod_failed(context: Any) -> None:
    vs = {
        "dod_evaluated": True,
        "dod_all_passed": False,
        "required_passed": 2,
        "required_failed": 3,
    }
    context.r2_plan = _make_plan(validation_summary=vs)


@given("r2plan-a v3 Plan with arguments and arguments_order")
def step_plan_with_args_order(context: Any) -> None:
    context.r2_plan = _make_plan(
        arguments={"target_coverage": 80, "format": "json"},
        arguments_order=["target_coverage", "format"],
    )


@given("r2plan-a v3 Plan with arguments but no arguments_order")
def step_plan_with_args_no_order(context: Any) -> None:
    context.r2_plan = _make_plan(
        arguments={"beta_key": "val", "alpha_key": "val2"},
        arguments_order=[],
    )


@given("r2plan-a v3 Plan with description longer than 200 chars")
def step_plan_with_long_description(context: Any) -> None:
    context.r2_plan = _make_plan(description="D" * 250)


@given("r2plan-a v3 Plan with resume metadata")
def step_plan_with_resume_metadata(context: Any) -> None:
    context.r2_plan = _make_plan(
        last_completed_step=5,
        last_checkpoint_id="01CHKPTRESUME000000000000",
    )


@given("r2plan-a v3 Plan with project link alias and read_only")
def step_plan_with_link_alias_readonly(context: Any) -> None:
    link = ProjectLink(project_name="local/ref-data", alias="data", read_only=True)
    context.r2_plan = _make_plan(project_links=[link])


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


@when("r2plan-I call _plan_spec_dict")
def step_call_spec_dict(context: Any) -> None:
    context.r2_spec = _plan_spec_dict(context.r2_plan)


@when("r2plan-I call _print_lifecycle_plan")
def step_call_print_plan(context: Any) -> None:
    context.r2_output = _capture_print(context.r2_plan)


# ---------------------------------------------------------------------------
# Then steps - spec dict assertions
# ---------------------------------------------------------------------------


@then('r2plan-the spec dict project_links should include alias "{alias}"')
def step_spec_alias(context: Any, alias: str) -> None:
    links = context.r2_spec["project_links"]
    assert any(link.get("alias") == alias for link in links), (
        f"No link with alias '{alias}' in {links}"
    )


@then("r2plan-the spec dict project_links should include read_only true")
def step_spec_readonly(context: Any) -> None:
    links = context.r2_spec["project_links"]
    assert any(link.get("read_only") is True for link in links), (
        f"No link with read_only=True in {links}"
    )


@then("r2plan-the spec dict project_links should not include alias")
def step_spec_no_alias(context: Any) -> None:
    links = context.r2_spec["project_links"]
    assert all("alias" not in link for link in links), f"Unexpected alias in {links}"


@then("r2plan-the spec dict project_links should not include read_only")
def step_spec_no_readonly(context: Any) -> None:
    links = context.r2_spec["project_links"]
    assert all("read_only" not in link for link in links), (
        f"Unexpected read_only in {links}"
    )


@then('r2plan-the spec dict automation_profile should be "{profile}"')
def step_spec_profile(context: Any, profile: str) -> None:
    assert context.r2_spec["automation_profile"] == profile, (
        f"Expected '{profile}', got '{context.r2_spec['automation_profile']}'"
    )


@then("r2plan-the spec dict automation_profile should be null")
def step_spec_profile_null(context: Any) -> None:
    assert context.r2_spec["automation_profile"] is None


@then('r2plan-the spec dict should contain key "{key}"')
def step_spec_has_key(context: Any, key: str) -> None:
    assert key in context.r2_spec, (
        f"Key '{key}' not in spec dict: {list(context.r2_spec.keys())}"
    )


@then('r2plan-the spec dict should not contain key "{key}"')
def step_spec_no_key(context: Any, key: str) -> None:
    assert key not in context.r2_spec, f"Key '{key}' should not be in spec dict"


@then("r2plan-the spec dict invariants count should be {n:d}")
def step_spec_inv_count(context: Any, n: int) -> None:
    actual = len(context.r2_spec["invariants"])
    assert actual == n, f"Expected {n} invariants, got {actual}"


@then("r2plan-the spec dict dod_evaluation all_passed should be true")
def step_spec_dod_passed(context: Any) -> None:
    assert context.r2_spec["dod_evaluation"]["all_passed"] is True


@then("r2plan-the spec dict last_completed_step should be {n:d}")
def step_spec_step(context: Any, n: int) -> None:
    assert context.r2_spec["last_completed_step"] == n


# -- print output assertions --


@then('r2plan-the printed output should contain "{text}"')
def step_output_contains(context: Any, text: str) -> None:
    assert text in context.r2_output, (
        f"Expected '{text}' in output:\n{context.r2_output}"
    )


@then('r2plan-the printed output should not contain "{text}"')
def step_output_not_contains(context: Any, text: str) -> None:
    assert text not in context.r2_output, (
        f"Did not expect '{text}' in output:\n{context.r2_output}"
    )
