"""Step definitions for correction_service_coverage_r3.feature.

Targets remaining uncovered lines in correction_service.py:
  - Line 137:  ``else str(request.mode)`` in ``_emit_correction_applied``
  - Lines 245-247: oversized tree/DAG guard in ``analyze_impact``
  - Line 351:  restoring pre-existing impact in ``generate_dry_run_report``
  - Lines 1060-1063, 1065: sibling isolation violation in
    ``validate_subtree_isolation``

All step patterns use the ``cscov3`` prefix to avoid collisions with
other step files.
"""

from __future__ import annotations

from unittest.mock import patch

from behave import given, then, when

from cleveragents.application.services.correction_service import CorrectionService
from cleveragents.core.exceptions import ValidationError
from cleveragents.domain.models.core.correction import (
    CorrectionImpact,
    CorrectionMode,
)
from cleveragents.infrastructure.events.models import DomainEvent

# ---------------------------------------------------------------------------
# Helper: simple event bus that records events
# ---------------------------------------------------------------------------


class _Cscov3EventBus:
    """Minimal event bus that records emitted events."""

    def __init__(self) -> None:
        self.events: list[DomainEvent] = []

    def emit(self, event: DomainEvent) -> None:
        self.events.append(event)

    def subscribe(self, event_type, handler) -> None:
        pass


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------


@given("a cscov3 fresh correction service")
def step_cscov3_background(context):
    """Initialise a fresh CorrectionService and reset all context slots."""
    context.cscov3_svc = CorrectionService()
    context.cscov3_bus = None
    context.cscov3_cid = None
    context.cscov3_error = None
    context.cscov3_result = None
    context.cscov3_report = None
    context.cscov3_isolation = None
    context.cscov3_original_impact = None


# ---------------------------------------------------------------------------
# Given - service with event bus
# ---------------------------------------------------------------------------


@given("a cscov3 correction service with a working event bus")
def step_cscov3_service_with_bus(context):
    """Create a CorrectionService wired to a recording event bus."""
    context.cscov3_bus = _Cscov3EventBus()
    context.cscov3_svc = CorrectionService(event_bus=context.cscov3_bus)


# ---------------------------------------------------------------------------
# Given - correction requests
# ---------------------------------------------------------------------------


@given(
    'a cscov3 REVERT correction request for plan "{plan_id}" targeting "{decision_id}"'
)
def step_cscov3_revert_request(context, plan_id, decision_id):
    """Create a REVERT correction request and store its ID."""
    svc: CorrectionService = context.cscov3_svc
    req = svc.request_correction(
        plan_id=plan_id,
        target_decision_id=decision_id,
        mode=CorrectionMode.REVERT,
    )
    context.cscov3_cid = req.correction_id


@given('a cscov3 revert correction for plan "{plan_id}" targeting "{decision_id}"')
def step_cscov3_revert_correction(context, plan_id, decision_id):
    """Create a REVERT correction request (alias for plain creation)."""
    svc: CorrectionService = context.cscov3_svc
    req = svc.request_correction(
        plan_id=plan_id,
        target_decision_id=decision_id,
        mode=CorrectionMode.REVERT,
    )
    context.cscov3_cid = req.correction_id


# ---------------------------------------------------------------------------
# Given - mode patching (Line 137)
# ---------------------------------------------------------------------------


@given('the cscov3 correction request mode is patched to a plain string "{mode_str}"')
def step_cscov3_patch_mode_to_plain_string(context, mode_str):
    """Patch the correction request's mode to a plain string (no .value).

    This forces the ``else str(request.mode)`` branch on line 137
    of ``_emit_correction_applied``.
    """
    svc: CorrectionService = context.cscov3_svc
    request = svc._corrections[context.cscov3_cid]
    # Replace mode with a plain string that lacks a .value attribute
    request.mode = mode_str  # type: ignore[assignment]


# ---------------------------------------------------------------------------
# Given - pre-existing cached impact (Line 351)
# ---------------------------------------------------------------------------


@given(
    "the cscov3 correction already has a cached impact with {count:d} affected decisions"
)
def step_cscov3_pre_cache_impact(context, count):
    """Manually inject a CorrectionImpact into the service's cache.

    This ensures ``original_impact is not None`` in
    ``generate_dry_run_report``, hitting line 351.
    """
    svc: CorrectionService = context.cscov3_svc
    affected = [f"pre_existing_{i}" for i in range(count)]
    impact = CorrectionImpact(
        affected_decisions=affected,
        excluded_decisions=[],
        affected_files=[f"{d}.py" for d in affected],
        affected_child_plans=[],
        estimated_cost=float(count) * 1.5,
        risk_level="low",
        rollback_tier="full",
        rollback_tier_depth=0,
        artifacts_to_archive=[f"{d}.artifact" for d in affected],
    )
    svc._impacts[context.cscov3_cid] = impact
    # Keep a reference so we can verify restoration later
    context.cscov3_original_impact = impact


# ---------------------------------------------------------------------------
# Given - trees for isolation tests (Lines 1060-1065)
# ---------------------------------------------------------------------------


@given("a cscov3 tree where sibling is reachable from target")
def step_cscov3_tree_sibling_reachable(context):
    """Build a tree where a sibling of 'target' is also a child of 'target'.

    Tree: parent -> [target, sibling], target -> [sibling]
    BFS from 'target' yields {target, sibling}.
    'sibling' is a sibling of 'target' (both children of 'parent')
    AND in the structural affected set → isolation violation.
    """
    context.cscov3_isolation_tree = {
        "parent": ["target", "sibling"],
        "target": ["sibling"],
    }


@given("a cscov3 tree where siblings are not reachable from target")
def step_cscov3_tree_sibling_not_reachable(context):
    """Build a tree where sibling 'C' is NOT reachable from target 'B'.

    Tree: A -> [B, C], B -> [D]
    BFS from 'B' yields {B, D}. 'C' is a sibling but not in the set → valid.
    """
    context.cscov3_isolation_tree = {
        "A": ["B", "C"],
        "B": ["D"],
    }


# ---------------------------------------------------------------------------
# When - execute revert (Line 137 path)
# ---------------------------------------------------------------------------


@when("I cscov3 execute the revert correction")
def step_cscov3_execute_revert(context):
    """Execute revert for the stored correction ID."""
    svc: CorrectionService = context.cscov3_svc
    try:
        context.cscov3_result = svc.execute_revert(context.cscov3_cid)
    except Exception as exc:
        context.cscov3_error = exc


# ---------------------------------------------------------------------------
# When - analyze_impact with oversized tree (Lines 245-247)
# ---------------------------------------------------------------------------


@when("I cscov3 analyze impact with a tree exceeding the max node limit")
def step_cscov3_analyze_oversized_tree(context):
    """Call analyze_impact with _MAX_TREE_NODES patched to a small value.

    Patching _MAX_TREE_NODES to 2 means even a tree with 3 keys triggers
    the guard on lines 244-248.
    """
    svc: CorrectionService = context.cscov3_svc
    oversized_tree = {f"node_{i}": [f"child_{i}"] for i in range(3)}
    patcher = patch(
        "cleveragents.application.services.correction_service._MAX_TREE_NODES",
        2,
    )
    patcher.start()
    context.add_cleanup(patcher.stop)
    try:
        svc.analyze_impact(context.cscov3_cid, decision_tree=oversized_tree)
        context.cscov3_error = None
    except (ValidationError, Exception) as exc:
        context.cscov3_error = exc


@when("I cscov3 analyze impact with a DAG exceeding the max node limit")
def step_cscov3_analyze_oversized_dag(context):
    """Call analyze_impact with influence_edges exceeding the limit.

    Patching _MAX_TREE_NODES to 2 and passing a DAG with 3 keys.
    """
    svc: CorrectionService = context.cscov3_svc
    oversized_dag = {f"src_{i}": [f"tgt_{i}"] for i in range(3)}
    patcher = patch(
        "cleveragents.application.services.correction_service._MAX_TREE_NODES",
        2,
    )
    patcher.start()
    context.add_cleanup(patcher.stop)
    try:
        svc.analyze_impact(
            context.cscov3_cid, decision_tree={}, influence_edges=oversized_dag
        )
        context.cscov3_error = None
    except (ValidationError, Exception) as exc:
        context.cscov3_error = exc


# ---------------------------------------------------------------------------
# When - dry-run report with pre-existing impact (Line 351)
# ---------------------------------------------------------------------------


@when('I cscov3 generate a dry-run report with adjacency "{adj}"')
def step_cscov3_dry_run_with_adjacency(context, adj):
    """Generate a dry-run report with the given adjacency list.

    Because there is a pre-existing impact in the cache, the finally
    block in generate_dry_run_report will execute line 351 to restore it.
    """
    svc: CorrectionService = context.cscov3_svc
    tree = _parse_adjacency(adj)
    try:
        context.cscov3_report = svc.generate_dry_run_report(
            context.cscov3_cid, decision_tree=tree
        )
    except Exception as exc:
        context.cscov3_error = exc


# ---------------------------------------------------------------------------
# When - validate_subtree_isolation (Lines 1060-1065)
# ---------------------------------------------------------------------------


@when('I cscov3 validate subtree isolation for target "{target}" on that tree')
def step_cscov3_validate_isolation(context, target):
    """Call validate_subtree_isolation with the stored tree."""
    svc: CorrectionService = context.cscov3_svc
    context.cscov3_isolation = svc.validate_subtree_isolation(
        target_decision_id=target,
        decision_tree=context.cscov3_isolation_tree,
    )


# ---------------------------------------------------------------------------
# Then - error assertions
# ---------------------------------------------------------------------------


@then("a cscov3 ValidationError should have been raised")
def step_cscov3_assert_validation_error(context):
    """Assert that a ValidationError was caught."""
    assert context.cscov3_error is not None, "Expected an error but none was raised"
    assert isinstance(context.cscov3_error, ValidationError), (
        f"Expected ValidationError, got {type(context.cscov3_error).__name__}: "
        f"{context.cscov3_error}"
    )


@then('the cscov3 error message should contain "{text}"')
def step_cscov3_error_contains(context, text):
    """Assert the error message contains the given text."""
    assert context.cscov3_error is not None, "No error was raised"
    assert text in str(context.cscov3_error), (
        f"Expected '{text}' in error message, got: {context.cscov3_error}"
    )


# ---------------------------------------------------------------------------
# Then - event bus assertions (Line 137)
# ---------------------------------------------------------------------------


@then("the cscov3 event bus should have received {count:d} event")
def step_cscov3_event_count(context, count):
    """Assert the event bus received exactly N events."""
    bus: _Cscov3EventBus = context.cscov3_bus
    assert len(bus.events) == count, f"Expected {count} event(s), got {len(bus.events)}"


@then('the cscov3 emitted event details mode should be "{expected_mode}"')
def step_cscov3_event_mode(context, expected_mode):
    """Assert the emitted event's mode detail matches."""
    bus: _Cscov3EventBus = context.cscov3_bus
    assert len(bus.events) > 0, "No events emitted"
    event = bus.events[-1]
    actual_mode = event.details.get("mode")
    assert actual_mode == expected_mode, (
        f"Expected mode '{expected_mode}', got '{actual_mode}'"
    )


# ---------------------------------------------------------------------------
# Then - dry-run report assertions (Line 351)
# ---------------------------------------------------------------------------


@then("the cscov3 dry-run report should have been generated")
def step_cscov3_report_generated(context):
    """Assert that a dry-run report was successfully produced."""
    assert context.cscov3_error is None, (
        f"Dry-run report generation failed: {context.cscov3_error}"
    )
    assert context.cscov3_report is not None, "No report was generated"


@then(
    "the cscov3 original cached impact should be restored with {count:d} affected decisions"
)
def step_cscov3_cached_impact_restored(context, count):
    """Assert the pre-existing impact was restored after dry run.

    Line 351 executes: ``self._impacts[correction_id] = original_impact``
    """
    svc: CorrectionService = context.cscov3_svc
    restored_impact = svc._impacts.get(context.cscov3_cid)
    assert restored_impact is not None, "No impact in cache after dry run"
    assert len(restored_impact.affected_decisions) == count, (
        f"Expected {count} affected decisions in restored impact, "
        f"got {len(restored_impact.affected_decisions)}"
    )
    # Verify it is the same object we injected
    assert restored_impact is context.cscov3_original_impact, (
        "Restored impact is not the same object as the original"
    )


# ---------------------------------------------------------------------------
# Then - isolation assertions (Lines 1060-1065)
# ---------------------------------------------------------------------------


@then("the cscov3 isolation result should be false")
def step_cscov3_isolation_false(context):
    """Assert that validate_subtree_isolation returned False."""
    assert context.cscov3_isolation is False, (
        f"Expected False, got {context.cscov3_isolation}"
    )


@then("the cscov3 isolation result should be true")
def step_cscov3_isolation_true(context):
    """Assert that validate_subtree_isolation returned True."""
    assert context.cscov3_isolation is True, (
        f"Expected True, got {context.cscov3_isolation}"
    )


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


def _parse_adjacency(spec: str) -> dict[str, list[str]]:
    """Parse compact adjacency notation: ``"A->B,C;B->D"``."""
    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
