"""Step definitions for correction_service_new_coverage.feature.

Provides comprehensive coverage for every method in CorrectionService,
including boundary conditions, BFS ordering, risk classification, and
error paths.  Step text is intentionally distinct from the existing
correction_flows_steps.py and correction_model_steps.py to avoid clashes.
"""

from __future__ import annotations

from behave import given, then, when

from cleveragents.application.services.correction_service import CorrectionService
from cleveragents.core.exceptions import ResourceNotFoundError, ValidationError
from cleveragents.domain.models.core.correction import CorrectionMode

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


def _parse_adjacency(spec: str) -> dict[str, list[str]]:
    """Parse a compact adjacency-list notation.

    Format: ``"A->B,C;B->D,E;C->F"``
    Returns: ``{"A": ["B","C"], "B": ["D","E"], "C": ["F"]}``
    """
    tree: dict[str, list[str]] = {}
    for segment in spec.split(";"):
        segment = segment.strip()
        if not segment:
            continue
        parent, children_str = segment.split("->")
        tree[parent.strip()] = [c.strip() for c in children_str.split(",")]
    return tree


def _build_chain(root: str, count: int) -> dict[str, list[str]]:
    """Build a linear chain of *count* nodes starting at *root*."""
    tree: dict[str, list[str]] = {}
    current = root
    for i in range(1, count):
        child = f"{root}_n{i}"
        tree[current] = [child]
        current = child
    return tree


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


@given("a fresh correction service instance")
def step_fresh_service(context):
    context.svc = CorrectionService()
    context.cid = None  # current correction_id
    context.new_req = None
    context.impact = None
    context.revert_result = None
    context.append_result = None
    context.dispatch_result = None
    context.dry_run_report = None
    context.retrieved = None
    context.corrections_list = None
    context.attempts_list = None
    context.subtree = None
    context.svc_error = None


@given('a stored revert correction for plan "{plan}" targeting "{target}"')
def step_stored_revert(context, plan, target):
    req = context.svc.request_correction(
        plan_id=plan,
        target_decision_id=target,
        mode=CorrectionMode.REVERT,
    )
    context.cid = req.correction_id
    context.new_req = req


@given('a stored append correction for plan "{plan}" targeting "{target}"')
def step_stored_append(context, plan, target):
    req = context.svc.request_correction(
        plan_id=plan,
        target_decision_id=target,
        mode=CorrectionMode.APPEND,
    )
    context.cid = req.correction_id
    context.new_req = req


@given("the stored correction has been reverted with an empty tree")
def step_execute_revert_given(context):
    context.svc.execute_revert(context.cid, {})


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


@when('I create a correction for plan "{plan}" targeting "{target}" in revert mode')
def step_create_revert(context, plan, target):
    context.new_req = context.svc.request_correction(
        plan_id=plan,
        target_decision_id=target,
        mode=CorrectionMode.REVERT,
    )
    context.cid = context.new_req.correction_id


@when(
    'I create a correction for plan "{plan}" targeting "{target}" in append mode'
    ' with guidance "{guidance}" and dry_run true'
)
def step_create_append_guidance_dryrun(context, plan, target, guidance):
    context.new_req = context.svc.request_correction(
        plan_id=plan,
        target_decision_id=target,
        mode=CorrectionMode.APPEND,
        guidance=guidance,
        dry_run=True,
    )
    context.cid = context.new_req.correction_id


@when('I attempt to create a correction with plan_id "{plan}" and target "{target}"')
def step_attempt_create_bad(context, plan, target):
    try:
        context.svc.request_correction(
            plan_id=plan,
            target_decision_id=target,
            mode=CorrectionMode.REVERT,
        )
        context.svc_error = None
    except (ValidationError, Exception) as exc:
        context.svc_error = exc


@when("I analyze impact with an empty decision tree")
def step_analyze_empty(context):
    context.impact = context.svc.analyze_impact(context.cid, {})


@when('I analyze impact with tree "{spec}"')
def step_analyze_with_tree(context, spec):
    tree = _parse_adjacency(spec)
    context.impact = context.svc.analyze_impact(context.cid, tree)


@when('I analyze impact with a chain tree of {n:d} nodes rooted at "{root}"')
def step_analyze_chain(context, n, root):
    tree = _build_chain(root, n)
    context.impact = context.svc.analyze_impact(context.cid, tree)


@when('I attempt to analyze impact for correction "{cid}"')
def step_attempt_analyze_bad(context, cid):
    try:
        context.svc.analyze_impact(cid, {})
        context.svc_error = None
    except ResourceNotFoundError as exc:
        context.svc_error = exc


@when('I generate a dry-run report with a chain tree of {n:d} nodes rooted at "{root}"')
def step_dryrun_chain(context, n, root):
    tree = _build_chain(root, n)
    context.dry_run_report = context.svc.generate_dry_run_report(context.cid, tree)


@when("I generate a dry-run report with an empty decision tree")
def step_dryrun_empty(context):
    context.dry_run_report = context.svc.generate_dry_run_report(context.cid, {})


@when('I execute revert with tree "{spec}"')
def step_execute_revert_tree(context, spec):
    tree = _parse_adjacency(spec)
    context.revert_result = context.svc.execute_revert(context.cid, tree)


@when("I execute revert with an empty decision tree")
def step_execute_revert_empty(context):
    context.revert_result = context.svc.execute_revert(context.cid, {})


@when("I execute append for the stored correction")
def step_execute_append(context):
    context.append_result = context.svc.execute_append(context.cid)


@when('I execute correction via the dispatch method with tree "{spec}"')
def step_dispatch_with_tree(context, spec):
    tree = _parse_adjacency(spec)
    context.dispatch_result = context.svc.execute_correction(context.cid, tree)


@when("I execute correction via the dispatch method with no tree")
def step_dispatch_no_tree(context):
    context.dispatch_result = context.svc.execute_correction(context.cid)


@when("I get the stored correction by its id")
def step_get_by_id(context):
    context.retrieved = context.svc.get_correction(context.cid)


@when('I attempt to get correction with id "{cid}"')
def step_attempt_get_bad(context, cid):
    try:
        context.svc.get_correction(cid)
        context.svc_error = None
    except ResourceNotFoundError as exc:
        context.svc_error = exc


@when("I list all service corrections without filter")
def step_list_all(context):
    context.corrections_list = context.svc.list_corrections()


@when('I list service corrections for plan "{plan}"')
def step_list_by_plan(context, plan):
    context.corrections_list = context.svc.list_corrections(plan_id=plan)


@when("I list attempts for the stored correction")
def step_list_attempts(context):
    context.attempts_list = context.svc.list_attempts(context.cid)


@when("I cancel the stored correction")
def step_cancel(context):
    context.svc.cancel_correction(context.cid)


@when("I attempt to cancel the stored correction")
def step_attempt_cancel(context):
    try:
        context.svc.cancel_correction(context.cid)
        context.svc_error = None
    except ValidationError as exc:
        context.svc_error = exc


@when('I compute affected subtree for "{root}" with tree "{spec}"')
def step_compute_subtree(context, root, spec):
    tree = _parse_adjacency(spec)
    context.subtree = CorrectionService._compute_affected_subtree(root, tree)


@when('I compute affected subtree for "{root}" with an empty tree')
def step_compute_subtree_empty(context, root):
    context.subtree = CorrectionService._compute_affected_subtree(root, {})


# -------------------------------------------------------------------
# Then steps
# -------------------------------------------------------------------


@then("the new correction should be stored in the service")
def step_stored(context):
    assert context.new_req is not None, "No correction was created"
    fetched = context.svc.get_correction(context.new_req.correction_id)
    assert fetched.correction_id == context.new_req.correction_id


@then('the new correction plan_id should equal "{val}"')
def step_new_plan(context, val):
    assert context.new_req.plan_id == val, (
        f"Expected plan_id '{val}', got '{context.new_req.plan_id}'"
    )


@then('the new correction target_decision_id should equal "{val}"')
def step_new_target(context, val):
    assert context.new_req.target_decision_id == val


@then('the new correction mode should equal "{val}"')
def step_new_mode(context, val):
    assert context.new_req.mode.value == val, (
        f"Expected mode '{val}', got '{context.new_req.mode.value}'"
    )


@then('the new correction status should equal "{val}"')
def step_new_status(context, val):
    assert context.new_req.status.value == val


@then("the new correction dry_run should be false")
def step_dryrun_false(context):
    assert context.new_req.dry_run is False


@then("the new correction dry_run should be true")
def step_dryrun_true(context):
    assert context.new_req.dry_run is True


@then('the new correction guidance should equal "{val}"')
def step_new_guidance(context, val):
    assert context.new_req.guidance == val


@then("a service ValidationError should have been raised")
def step_svc_validation_error(context):
    assert context.svc_error is not None, "Expected ValidationError but none was raised"
    assert isinstance(context.svc_error, ValidationError), (
        f"Expected ValidationError, got {type(context.svc_error).__name__}"
    )


@then("a service ResourceNotFoundError should have been raised")
def step_svc_not_found_error(context):
    assert context.svc_error is not None, (
        "Expected ResourceNotFoundError but none was raised"
    )
    assert isinstance(context.svc_error, ResourceNotFoundError), (
        f"Expected ResourceNotFoundError, got {type(context.svc_error).__name__}"
    )


# ── impact assertions ──────────────────────────────────────────


@then('the impact affected decisions should be exactly "{expected}"')
def step_impact_decisions(context, expected):
    expected_list = [d.strip() for d in expected.split(",")]
    assert context.impact.affected_decisions == expected_list, (
        f"Expected {expected_list}, got {context.impact.affected_decisions}"
    )


@then('the impact risk level should equal "{val}"')
def step_impact_risk(context, val):
    assert context.impact.risk_level == val, (
        f"Expected risk '{val}', got '{context.impact.risk_level}'"
    )


@then('the impact rollback tier should equal "{val}"')
def step_impact_rollback(context, val):
    assert context.impact.rollback_tier == val


@then("the impact estimated cost should equal {cost:g}")
def step_impact_cost(context, cost):
    assert context.impact.estimated_cost == cost, (
        f"Expected cost {cost}, got {context.impact.estimated_cost}"
    )


@then('the impact artifacts to archive should be exactly "{expected}"')
def step_impact_artifacts(context, expected):
    expected_list = [a.strip() for a in expected.split(",")]
    assert context.impact.artifacts_to_archive == expected_list


@then('the impact affected files should be exactly "{expected}"')
def step_impact_files(context, expected):
    expected_list = [f.strip() for f in expected.split(",")]
    assert context.impact.affected_files == expected_list


@then("the impact affected decisions count should be {n:d}")
def step_impact_count(context, n):
    actual = len(context.impact.affected_decisions)
    assert actual == n, f"Expected {n} affected decisions, got {actual}"


@then('the stored correction status should be "{val}"')
def step_stored_status(context, val):
    req = context.svc.get_correction(context.cid)
    assert req.status.value == val, f"Expected status '{val}', got '{req.status.value}'"


# ── dry-run report assertions ─────────────────────────────────


@then('the dry-run report mode should equal "{val}"')
def step_dr_mode(context, val):
    assert context.dry_run_report.mode.value == val


@then('the dry-run report warnings should include "{fragment}"')
def step_dr_warning(context, fragment):
    joined = " | ".join(context.dry_run_report.warnings)
    assert fragment in joined, (
        f"'{fragment}' not in warnings: {context.dry_run_report.warnings}"
    )


@then("the dry-run report decisions to invalidate should not be empty")
def step_dr_invalidate_not_empty(context):
    assert len(context.dry_run_report.decisions_to_invalidate) > 0


@then("the dry-run report decisions to invalidate should be empty")
def step_dr_invalidate_empty(context):
    assert len(context.dry_run_report.decisions_to_invalidate) == 0


@then("the dry-run report estimated recompute time should be {t:g}")
def step_dr_recompute(context, t):
    assert context.dry_run_report.estimated_recompute_time_seconds == t, (
        f"Expected {t}, got {context.dry_run_report.estimated_recompute_time_seconds}"
    )


# ── revert result assertions ──────────────────────────────────


@then('the revert result status should equal "{val}"')
def step_revert_status(context, val):
    assert context.revert_result.status.value == val


@then('the revert result reverted decisions should include "{d}"')
def step_revert_decisions(context, d):
    assert d in context.revert_result.reverted_decisions, (
        f"'{d}' not in {context.revert_result.reverted_decisions}"
    )


@then('the revert result archived artifacts should include "{a}"')
def step_revert_artifacts(context, a):
    assert a in context.revert_result.archived_artifacts, (
        f"'{a}' not in {context.revert_result.archived_artifacts}"
    )


# ── attempt assertions ────────────────────────────────────────


@then("the service attempts for the correction should have {n:d} entry")
def step_attempts_count(context, n):
    attempts = context.svc.list_attempts(context.cid)
    assert len(attempts) == n, f"Expected {n} attempts, got {len(attempts)}"


@then("the first attempt should have success true")
def step_first_attempt_ok(context):
    attempts = context.svc.list_attempts(context.cid)
    assert attempts[0].success is True


@then("the first attempt completed_at should not be none")
def step_first_attempt_completed(context):
    attempts = context.svc.list_attempts(context.cid)
    assert attempts[0].completed_at is not None


@then("the attempts list should have {n:d} entry")
def step_attempts_list_count(context, n):
    assert len(context.attempts_list) == n, (
        f"Expected {n}, got {len(context.attempts_list)}"
    )


# ── append result assertions ──────────────────────────────────


@then('the append result status should equal "{val}"')
def step_append_status(context, val):
    assert context.append_result.status.value == val


@then("the append result spawned_child_plan_id should not be none")
def step_append_child(context):
    assert context.append_result.spawned_child_plan_id is not None


@then("the append result new decisions should not be empty")
def step_append_new_decisions(context):
    assert len(context.append_result.new_decisions) > 0


# ── dispatch result assertions ────────────────────────────────


@then('the dispatched result status should equal "{val}"')
def step_dispatch_status(context, val):
    assert context.dispatch_result.status.value == val


@then('the dispatched result reverted decisions should include "{d}"')
def step_dispatch_reverted(context, d):
    assert d in context.dispatch_result.reverted_decisions


@then("the dispatched result spawned_child_plan_id should not be none")
def step_dispatch_child(context):
    assert context.dispatch_result.spawned_child_plan_id is not None


# ── get_correction assertions ─────────────────────────────────


@then('the retrieved correction plan_id should equal "{val}"')
def step_retrieved_plan(context, val):
    assert context.retrieved.plan_id == val


@then('the retrieved correction target_decision_id should equal "{val}"')
def step_retrieved_target(context, val):
    assert context.retrieved.target_decision_id == val


# ── list_corrections assertions ───────────────────────────────


@then("the service corrections list should have {n:d} items")
def step_corrections_count(context, n):
    assert len(context.corrections_list) == n, (
        f"Expected {n}, got {len(context.corrections_list)}"
    )


# ── classify_risk assertions ──────────────────────────────────


@then('classifying risk for {n:d} affected nodes should return "{expected}"')
def step_classify_risk(context, n, expected):
    result = CorrectionService._classify_risk(n)
    assert result == expected, (
        f"_classify_risk({n}) = '{result}', expected '{expected}'"
    )


# ── subtree assertions ────────────────────────────────────────


@then('the affected subtree should be exactly "{expected}" in order')
def step_subtree_order(context, expected):
    expected_list = [n.strip() for n in expected.split(",")]
    assert context.subtree == expected_list, (
        f"Expected {expected_list}, got {context.subtree}"
    )


@then("the affected subtree count should be {n:d}")
def step_subtree_count(context, n):
    assert len(context.subtree) == n, f"Expected {n} nodes, got {len(context.subtree)}"


@when("I attempt to create a correction with an empty plan_id")
def step_attempt_create_empty_plan(context):
    try:
        context.svc.request_correction(
            plan_id="",
            target_decision_id="DX1",
            mode=CorrectionMode.REVERT,
        )
        context.svc_error = None
    except (ValidationError, Exception) as exc:
        context.svc_error = exc


@when("I attempt to create a correction with an empty target_decision_id")
def step_attempt_create_empty_target(context):
    try:
        context.svc.request_correction(
            plan_id="PX1",
            target_decision_id="",
            mode=CorrectionMode.REVERT,
        )
        context.svc_error = None
    except (ValidationError, Exception) as exc:
        context.svc_error = exc
