"""Step definitions for correction_service_coverage.feature.

Provides full line-and-branch coverage for
``cleveragents.application.services.correction_service.CorrectionService``.

All step-text uses a ``cov-`` prefix to avoid collisions with existing
correction_flows_steps.py and correction_service_new_coverage_steps.py.
"""

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,
    CorrectionStatus,
)

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


def _parse_adj(spec: str) -> dict[str, list[str]]:
    """Parse ``"A->B,C;B->D"`` into an adjacency dict."""
    tree: dict[str, list[str]] = {}
    for seg in spec.split(";"):
        seg = seg.strip()
        if not seg:
            continue
        parent, children_str = seg.split("->")
        tree[parent.strip()] = [c.strip() for c in children_str.split(",")]
    return tree


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


# -------------------------------------------------------------------
# Background / Given
# -------------------------------------------------------------------


@given("a coverage test correction service")
def step_bg_service(context):
    context.cov_svc = CorrectionService()
    context.cov_cid = None
    context.cov_req = None
    context.cov_impact = None
    context.cov_revert_result = None
    context.cov_append_result = None
    context.cov_dispatch_result = None
    context.cov_dry_run = None
    context.cov_retrieved = None
    context.cov_list = None
    context.cov_attempts = None
    context.cov_subtree = None
    context.cov_error = None


@given('a cov-stored revert correction for plan "{plan}" targeting "{target}"')
def step_stored_revert_cov(context, plan, target):
    req = context.cov_svc.request_correction(
        plan_id=plan,
        target_decision_id=target,
        mode=CorrectionMode.REVERT,
    )
    context.cov_cid = req.correction_id
    context.cov_req = req


@given('a cov-stored append correction for plan "{plan}" targeting "{target}"')
def step_stored_append_cov(context, plan, target):
    req = context.cov_svc.request_correction(
        plan_id=plan,
        target_decision_id=target,
        mode=CorrectionMode.APPEND,
    )
    context.cov_cid = req.correction_id
    context.cov_req = req


@given("the cov-stored correction has been executed via revert with empty tree")
def step_pre_execute_revert(context):
    context.cov_svc.execute_revert(context.cov_cid, {})


@given("the cov-stored correction has been executed via append")
def step_pre_execute_append(context):
    context.cov_svc.execute_append(context.cov_cid)


@given("the cov-stored correction has been analyzed with empty tree")
def step_pre_analyze(context):
    context.cov_svc.analyze_impact(context.cov_cid, {})


@given('the cov-stored correction status is forced to "{status}"')
def step_force_status(context, status):
    req = context.cov_svc.get_correction(context.cov_cid)
    req.status = CorrectionStatus(status)


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


@when(
    'I cov-request a correction for plan "{plan}" targeting "{target}" in revert mode'
)
def step_request_revert(context, plan, target):
    context.cov_req = context.cov_svc.request_correction(
        plan_id=plan,
        target_decision_id=target,
        mode=CorrectionMode.REVERT,
    )
    context.cov_cid = context.cov_req.correction_id


@when(
    'I cov-request a correction for plan "{plan}" targeting "{target}"'
    ' in append mode with guidance "{guidance}" and dry_run'
)
def step_request_append_guidance_dryrun(context, plan, target, guidance):
    context.cov_req = context.cov_svc.request_correction(
        plan_id=plan,
        target_decision_id=target,
        mode=CorrectionMode.APPEND,
        guidance=guidance,
        dry_run=True,
    )
    context.cov_cid = context.cov_req.correction_id


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


@when('I cov-attempt to create a correction with empty plan_id and target "{target}"')
def step_attempt_create_empty_plan(context, target):
    try:
        context.cov_svc.request_correction(
            plan_id="",
            target_decision_id=target,
            mode=CorrectionMode.REVERT,
        )
        context.cov_error = None
    except (ValidationError, Exception) as exc:
        context.cov_error = exc


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


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


@when("I cov-analyze impact with an empty tree")
def step_analyze_empty(context):
    context.cov_impact = context.cov_svc.analyze_impact(context.cov_cid, {})


@when('I cov-analyze impact with adjacency "{spec}"')
def step_analyze_adj(context, spec):
    tree = _parse_adj(spec)
    context.cov_impact = context.cov_svc.analyze_impact(context.cov_cid, tree)


@when('I cov-attempt to analyze impact for correction id "{cid}"')
def step_attempt_analyze(context, cid):
    try:
        context.cov_svc.analyze_impact(cid, {})
        context.cov_error = None
    except ResourceNotFoundError as exc:
        context.cov_error = exc


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


@when("I cov-generate dry-run report with an empty tree")
def step_dryrun_empty(context):
    context.cov_dry_run = context.cov_svc.generate_dry_run_report(context.cov_cid, {})


@when('I cov-generate dry-run report with adjacency "{spec}"')
def step_dryrun_adj(context, spec):
    tree = _parse_adj(spec)
    context.cov_dry_run = context.cov_svc.generate_dry_run_report(context.cov_cid, tree)


@when('I cov-generate dry-run report with a chain of {n:d} nodes rooted at "{root}"')
def step_dryrun_chain(context, n, root):
    tree = _build_chain_cov(root, n)
    context.cov_dry_run = context.cov_svc.generate_dry_run_report(context.cov_cid, tree)


@when('I cov-attempt to generate dry-run report for correction id "{cid}"')
def step_attempt_dryrun(context, cid):
    try:
        context.cov_svc.generate_dry_run_report(cid, {})
        context.cov_error = None
    except ResourceNotFoundError as exc:
        context.cov_error = exc


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


@when('I cov-execute revert with adjacency "{spec}"')
def step_exec_revert_adj(context, spec):
    tree = _parse_adj(spec)
    context.cov_revert_result = context.cov_svc.execute_revert(context.cov_cid, tree)


@when("I cov-execute revert with empty tree")
def step_exec_revert_empty(context):
    context.cov_revert_result = context.cov_svc.execute_revert(context.cov_cid, {})


@when("I cov-attempt to execute revert on the stored correction")
def step_attempt_exec_revert(context):
    try:
        context.cov_svc.execute_revert(context.cov_cid, {})
        context.cov_error = None
    except (ValidationError, ResourceNotFoundError) as exc:
        context.cov_error = exc


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


@when("I cov-execute append for the stored correction")
def step_exec_append(context):
    context.cov_append_result = context.cov_svc.execute_append(context.cov_cid)


@when("I cov-attempt to execute append on the stored correction")
def step_attempt_exec_append(context):
    try:
        context.cov_svc.execute_append(context.cov_cid)
        context.cov_error = None
    except (ValidationError, ResourceNotFoundError) as exc:
        context.cov_error = exc


# -------------------------------------------------------------------
# When steps - execute_correction (dispatch)
# -------------------------------------------------------------------


@when('I cov-dispatch execute correction with adjacency "{spec}"')
def step_dispatch_adj(context, spec):
    tree = _parse_adj(spec)
    context.cov_dispatch_result = context.cov_svc.execute_correction(
        context.cov_cid, tree
    )


@when("I cov-dispatch execute correction with no tree")
def step_dispatch_no_tree(context):
    context.cov_dispatch_result = context.cov_svc.execute_correction(context.cov_cid)


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


@when("I cov-get the stored correction by id")
def step_get_by_id(context):
    context.cov_retrieved = context.cov_svc.get_correction(context.cov_cid)


@when('I cov-attempt to get correction with id "{cid}"')
def step_attempt_get(context, cid):
    try:
        context.cov_svc.get_correction(cid)
        context.cov_error = None
    except ResourceNotFoundError as exc:
        context.cov_error = exc


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


@when("I cov-list all corrections without filter")
def step_list_all(context):
    context.cov_list = context.cov_svc.list_corrections()


@when('I cov-list corrections for plan "{plan}"')
def step_list_by_plan(context, plan):
    context.cov_list = context.cov_svc.list_corrections(plan_id=plan)


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


@when("I cov-list attempts for the stored correction")
def step_list_attempts(context):
    context.cov_attempts = context.cov_svc.list_attempts(context.cov_cid)


@when('I cov-attempt to list attempts for correction id "{cid}"')
def step_attempt_list_attempts(context, cid):
    try:
        context.cov_svc.list_attempts(cid)
        context.cov_error = None
    except ResourceNotFoundError as exc:
        context.cov_error = exc


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


@when("I cov-cancel the stored correction")
def step_cancel(context):
    context.cov_svc.cancel_correction(context.cov_cid)


@when("I cov-attempt to cancel the stored correction")
def step_attempt_cancel(context):
    try:
        context.cov_svc.cancel_correction(context.cov_cid)
        context.cov_error = None
    except ValidationError as exc:
        context.cov_error = exc


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


@when('I cov-compute subtree for "{root}" with empty tree')
def step_subtree_empty(context, root):
    context.cov_subtree = CorrectionService._compute_affected_subtree(root, {})


@when('I cov-compute subtree for "{root}" with adjacency "{spec}"')
def step_subtree_adj(context, root, spec):
    tree = _parse_adj(spec)
    context.cov_subtree = CorrectionService._compute_affected_subtree(root, tree)


# ===================================================================
# Then steps
# ===================================================================

# -------------------------------------------------------------------
# __init__ assertions
# -------------------------------------------------------------------


@then("the coverage service corrections dict should be empty")
def step_init_corrections(context):
    assert len(context.cov_svc._corrections) == 0


@then("the coverage service impacts dict should be empty")
def step_init_impacts(context):
    assert len(context.cov_svc._impacts) == 0


@then("the coverage service attempts dict should be empty")
def step_init_attempts(context):
    assert len(context.cov_svc._attempts) == 0


@then("the coverage service results dict should be empty")
def step_init_results(context):
    assert len(context.cov_svc._results) == 0


# -------------------------------------------------------------------
# request_correction assertions
# -------------------------------------------------------------------


@then("the cov-request should be stored in the service")
def step_req_stored(context):
    assert context.cov_req is not None
    fetched = context.cov_svc.get_correction(context.cov_req.correction_id)
    assert fetched.correction_id == context.cov_req.correction_id


@then('the cov-request plan_id should be "{val}"')
def step_req_plan(context, val):
    assert context.cov_req.plan_id == val


@then('the cov-request target_decision_id should be "{val}"')
def step_req_target(context, val):
    assert context.cov_req.target_decision_id == val


@then('the cov-request mode should be "{val}"')
def step_req_mode(context, val):
    assert context.cov_req.mode.value == val


@then('the cov-request status should be "{val}"')
def step_req_status(context, val):
    assert context.cov_req.status.value == val


@then("the cov-request dry_run should be false")
def step_req_dryrun_false(context):
    assert context.cov_req.dry_run is False


@then("the cov-request dry_run should be true")
def step_req_dryrun_true(context):
    assert context.cov_req.dry_run is True


@then('the cov-request guidance should be "{val}"')
def step_req_guidance(context, val):
    assert context.cov_req.guidance == val


@then("the cov-request guidance should be empty")
def step_req_guidance_empty(context):
    assert context.cov_req.guidance == ""


# -------------------------------------------------------------------
# Error assertions
# -------------------------------------------------------------------


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


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


# -------------------------------------------------------------------
# Impact assertions
# -------------------------------------------------------------------


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


@then('the cov-impact risk level should be "{val}"')
def step_impact_risk(context, val):
    assert context.cov_impact.risk_level == val


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


@then("the cov-impact estimated cost should be {cost:g}")
def step_impact_cost(context, cost):
    assert context.cov_impact.estimated_cost == cost


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


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


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


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


# -------------------------------------------------------------------
# Dry-run report assertions
# -------------------------------------------------------------------


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


@then("the cov dry-run warnings should be empty")
def step_dr_warnings_empty(context):
    assert len(context.cov_dry_run.warnings) == 0, (
        f"Expected no warnings, got {context.cov_dry_run.warnings}"
    )


@then('the cov dry-run warnings should contain "{fragment}"')
def step_dr_warning_contains(context, fragment):
    joined = " | ".join(context.cov_dry_run.warnings)
    assert fragment in joined, (
        f"'{fragment}' not found in warnings: {context.cov_dry_run.warnings}"
    )


@then('the cov dry-run decisions to invalidate should be "{expected}"')
def step_dr_invalidate(context, expected):
    expected_list = [d.strip() for d in expected.split(",")]
    assert context.cov_dry_run.decisions_to_invalidate == expected_list


@then("the cov dry-run decisions to invalidate should be empty list")
def step_dr_invalidate_empty(context):
    assert context.cov_dry_run.decisions_to_invalidate == []


@then("the cov dry-run decisions to invalidate count should be {n:d}")
def step_dr_invalidate_count(context, n):
    actual = len(context.cov_dry_run.decisions_to_invalidate)
    assert actual == n, f"Expected {n}, got {actual}"


@then("the cov dry-run estimated recompute time should be {t:g}")
def step_dr_recompute(context, t):
    assert context.cov_dry_run.estimated_recompute_time_seconds == t


# -------------------------------------------------------------------
# Revert result assertions
# -------------------------------------------------------------------


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


@then('the cov revert result reverted decisions should include "{d}"')
def step_revert_incl(context, d):
    assert d in context.cov_revert_result.reverted_decisions


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


# -------------------------------------------------------------------
# Append result assertions
# -------------------------------------------------------------------


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


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


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


@then("the cov first attempt details should contain spawned_child_plan_id")
def step_append_attempt_details(context):
    attempts = context.cov_svc.list_attempts(context.cov_cid)
    assert "spawned_child_plan_id" in attempts[0].details


# -------------------------------------------------------------------
# Dispatch result assertions
# -------------------------------------------------------------------


@then('the cov dispatch result status should be "{val}"')
def step_dispatch_status(context, val):
    assert context.cov_dispatch_result.status.value == val


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


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


# -------------------------------------------------------------------
# get_correction assertions
# -------------------------------------------------------------------


@then('the cov-retrieved correction plan_id should be "{val}"')
def step_get_plan(context, val):
    assert context.cov_retrieved.plan_id == val


@then('the cov-retrieved correction target_decision_id should be "{val}"')
def step_get_target(context, val):
    assert context.cov_retrieved.target_decision_id == val


# -------------------------------------------------------------------
# list_corrections assertions
# -------------------------------------------------------------------


@then("the cov corrections list should have at least {n:d} items")
def step_list_min(context, n):
    actual = len(context.cov_list)
    assert actual >= n, f"Expected at least {n}, got {actual}"


@then("the cov corrections list should have {n:d} item")
def step_list_exact_one(context, n):
    actual = len(context.cov_list)
    assert actual == n, f"Expected {n}, got {actual}"


@then("the cov corrections list should have {n:d} items")
def step_list_exact(context, n):
    actual = len(context.cov_list)
    assert actual == n, f"Expected {n}, got {actual}"


# -------------------------------------------------------------------
# list_attempts assertions
# -------------------------------------------------------------------


@then("the cov attempts list should have {n:d} items")
def step_attempts_count_plural(context, n):
    actual = len(context.cov_attempts)
    assert actual == n, f"Expected {n}, got {actual}"


@then("the cov attempts list should have {n:d} item")
def step_attempts_count_single(context, n):
    actual = len(context.cov_attempts)
    assert actual == n, f"Expected {n}, got {actual}"


# -------------------------------------------------------------------
# Attempt detail assertions
# -------------------------------------------------------------------


@then("the cov attempts for the stored correction should have {n:d} entry")
def step_attempts_for_stored(context, n):
    attempts = context.cov_svc.list_attempts(context.cov_cid)
    assert len(attempts) == n, f"Expected {n}, got {len(attempts)}"


@then("the cov first attempt should be successful")
def step_first_attempt_ok(context):
    attempts = context.cov_svc.list_attempts(context.cov_cid)
    assert attempts[0].success is True


@then("the cov first attempt completed_at should be set")
def step_first_attempt_completed(context):
    attempts = context.cov_svc.list_attempts(context.cov_cid)
    assert attempts[0].completed_at is not None


# -------------------------------------------------------------------
# _classify_risk assertions
# -------------------------------------------------------------------


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


# -------------------------------------------------------------------
# _compute_affected_subtree assertions
# -------------------------------------------------------------------


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


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