"""Step definitions for decision_persistence_serialization.feature.

Tests Decision serialization round-trips (model_dump / model_validate,
JSON), context-snapshot persistence, correction-chain reconstruction,
and decision-tree reconstruction from serialized data.

All step names are prefixed with ``decision persistence`` to avoid
AmbiguousStep conflicts with decision_model_steps.py.
"""

from __future__ import annotations

import json
from typing import Any

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context
from ulid import ULID

from cleveragents.domain.models.core.decision import (
    ArtifactRef,
    ContextSnapshot,
    Decision,
    DecisionType,
    ResourceRef,
)

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

_ALL_DECISION_TYPES: list[DecisionType] = list(DecisionType)


def _make_decision(**overrides: Any) -> Decision:
    """Build a Decision with sensible defaults, merged with *overrides*."""
    defaults: dict[str, Any] = {
        "plan_id": str(ULID()),
        "sequence_number": 0,
        "decision_type": DecisionType.PROMPT_DEFINITION,
        "question": "What approach should we take?",
        "chosen_option": "Build a REST API",
    }
    # prompt_definition must not have a parent
    dt = overrides.get("decision_type", defaults["decision_type"])
    if dt != DecisionType.PROMPT_DEFINITION and "parent_decision_id" not in overrides:
        defaults["parent_decision_id"] = str(ULID())
    defaults.update(overrides)
    return Decision(**defaults)


def _roundtrip_dump(decision: Decision) -> Decision:
    """Serialise via model_dump and deserialise back."""
    data = decision.model_dump()
    return Decision.model_validate(data)


def _roundtrip_json(decision: Decision) -> Decision:
    """Serialise to JSON string and deserialise back."""
    json_str = decision.model_dump_json()
    raw = json.loads(json_str)
    return Decision.model_validate(raw)


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


@given("a decision persistence plan ULID")
def step_dp_plan_ulid(context: Context) -> None:
    context.dp_plan_id = str(ULID())


@given("a decision persistence parent ULID")
def step_dp_parent_ulid(context: Context) -> None:
    context.dp_parent_id = str(ULID())


@given("a decision persistence corrected decision ULID")
def step_dp_corrected_ulid(context: Context) -> None:
    context.dp_corrected_id = str(ULID())


@given("a decision persistence context snapshot with {count:d} resources")
def step_dp_snapshot_with_resources(context: Context, count: int) -> None:
    resources = [
        ResourceRef(resource_id=str(ULID()), path=f"src/file_{i}.py")
        for i in range(count)
    ]
    context.dp_snapshot = ContextSnapshot(
        hot_context_hash="sha256:persist_test",
        hot_context_ref="store://snapshots/persist_test",
        relevant_resources=resources,
        actor_state_ref="checkpoint://actor/persist",
    )


@given('a decision persistence snapshot with actor state ref "{ref}"')
def step_dp_snapshot_actor_ref(context: Context, ref: str) -> None:
    context.dp_snapshot = ContextSnapshot(
        hot_context_hash="sha256:actor_ref_test",
        hot_context_ref="store://snapshots/actor_ref_test",
        relevant_resources=[ResourceRef(resource_id=str(ULID()))],
        actor_state_ref=ref,
    )


# ---------------------------------------------------------------------------
# When steps — creation
# ---------------------------------------------------------------------------


@when("I decision persistence create a root prompt_definition with sequence {seq:d}")
def step_dp_create_root(context: Context, seq: int) -> None:
    context.dp_decision = _make_decision(
        plan_id=context.dp_plan_id,
        sequence_number=seq,
        decision_type=DecisionType.PROMPT_DEFINITION,
    )


@when("I decision persistence create a full strategy_choice child at sequence {seq:d}")
def step_dp_create_full_child(context: Context, seq: int) -> None:
    context.dp_decision = _make_decision(
        plan_id=context.dp_plan_id,
        parent_decision_id=context.dp_parent_id,
        sequence_number=seq,
        decision_type=DecisionType.STRATEGY_CHOICE,
        question="Which framework?",
        chosen_option="FastAPI",
        alternatives_considered=["Flask", "Django", "Starlette"],
        confidence_score=0.88,
        rationale="FastAPI has async support",
    )


@when("I decision persistence create an implementation_choice with the snapshot")
def step_dp_create_impl_with_snapshot(context: Context) -> None:
    context.dp_decision = _make_decision(
        plan_id=context.dp_plan_id,
        parent_decision_id=context.dp_parent_id,
        decision_type=DecisionType.IMPLEMENTATION_CHOICE,
        context_snapshot=context.dp_snapshot,
    )


@when("I decision persistence create a strategy_choice with the snapshot")
def step_dp_create_strategy_with_snapshot(context: Context) -> None:
    context.dp_decision = _make_decision(
        plan_id=context.dp_plan_id,
        parent_decision_id=context.dp_parent_id,
        decision_type=DecisionType.STRATEGY_CHOICE,
        context_snapshot=context.dp_snapshot,
    )


@when("I decision persistence create a decision with {count:d} artifacts")
def step_dp_create_with_artifacts(context: Context, count: int) -> None:
    artifacts = [
        ArtifactRef(artifact_path=f"src/artifact_{i}.py", artifact_type="file")
        for i in range(count)
    ]
    context.dp_decision = _make_decision(
        plan_id=context.dp_plan_id,
        parent_decision_id=context.dp_parent_id,
        decision_type=DecisionType.TOOL_INVOCATION,
        artifacts_produced=artifacts,
    )


@when("I decision persistence create a decision with no alternatives")
def step_dp_create_no_alternatives(context: Context) -> None:
    context.dp_decision = _make_decision(
        plan_id=context.dp_plan_id,
        decision_type=DecisionType.PROMPT_DEFINITION,
        alternatives_considered=[],
    )


@when("I decision persistence create a decision with confidence {value}")
def step_dp_create_with_confidence(context: Context, value: str) -> None:
    conf: float | None = None if value == "None" else float(value)
    kwargs: dict[str, Any] = {
        "plan_id": context.dp_plan_id,
        "decision_type": DecisionType.PROMPT_DEFINITION,
    }
    if conf is not None:
        kwargs["confidence_score"] = conf
    context.dp_decision = _make_decision(**kwargs)


@when("I decision persistence create a decision with {count:d} downstream IDs")
def step_dp_create_downstream_ids(context: Context, count: int) -> None:
    context.dp_decision = _make_decision(
        plan_id=context.dp_plan_id,
        decision_type=DecisionType.PROMPT_DEFINITION,
        downstream_decision_ids=[str(ULID()) for _ in range(count)],
    )


@when("I decision persistence create a decision with {count:d} downstream plan IDs")
def step_dp_create_downstream_plan_ids(context: Context, count: int) -> None:
    context.dp_decision = _make_decision(
        plan_id=context.dp_plan_id,
        decision_type=DecisionType.PROMPT_DEFINITION,
        downstream_plan_ids=[str(ULID()) for _ in range(count)],
    )


@when("I decision persistence create a correction decision")
def step_dp_create_correction(context: Context) -> None:
    context.dp_decision = _make_decision(
        plan_id=context.dp_plan_id,
        decision_type=DecisionType.STRATEGY_CHOICE,
        is_correction=True,
        corrects_decision_id=context.dp_corrected_id,
        correction_reason="Original approach was suboptimal",
    )


@when("I decision persistence create a decision superseded by another")
def step_dp_create_superseded(context: Context) -> None:
    context.dp_decision = _make_decision(
        plan_id=context.dp_plan_id,
        decision_type=DecisionType.PROMPT_DEFINITION,
        superseded_by=str(ULID()),
    )


@when("I decision persistence create a decision with a {length:d} character rationale")
def step_dp_create_long_rationale(context: Context, length: int) -> None:
    context.dp_decision = _make_decision(
        plan_id=context.dp_plan_id,
        decision_type=DecisionType.PROMPT_DEFINITION,
        rationale="R" * length,
    )


# ---------------------------------------------------------------------------
# When steps — round-trip operations
# ---------------------------------------------------------------------------


@when("I decision persistence round-trip the decision through model_dump")
def step_dp_roundtrip_dump(context: Context) -> None:
    context.dp_restored = _roundtrip_dump(context.dp_decision)


@when("I decision persistence round-trip the decision through JSON")
def step_dp_roundtrip_json(context: Context) -> None:
    context.dp_restored = _roundtrip_json(context.dp_decision)


# ---------------------------------------------------------------------------
# When steps — correction chain
# ---------------------------------------------------------------------------


@when("I decision persistence build a correction chain of {count:d} decisions")
def step_dp_correction_chain(context: Context, count: int) -> None:
    plan_id = context.dp_plan_id
    decisions: list[Decision] = []

    # First decision: the original
    original = _make_decision(
        plan_id=plan_id,
        sequence_number=0,
        decision_type=DecisionType.PROMPT_DEFINITION,
        question="Initial approach?",
        chosen_option="Monolith",
    )
    decisions.append(original)

    prev_id = original.decision_id
    for i in range(1, count):
        correction = _make_decision(
            plan_id=plan_id,
            sequence_number=i,
            decision_type=DecisionType.STRATEGY_CHOICE,
            question=f"Correction {i}?",
            chosen_option=f"Approach v{i + 1}",
            is_correction=True,
            corrects_decision_id=prev_id,
            correction_reason=f"Reason {i}",
        )
        # Mark previous as superseded
        decisions[-1] = decisions[-1].with_superseded_by(correction.decision_id)
        decisions.append(correction)
        prev_id = correction.decision_id

    context.dp_chain = decisions


@when("I decision persistence round-trip all decisions through model_dump")
def step_dp_roundtrip_all(context: Context) -> None:
    context.dp_chain_restored = [_roundtrip_dump(d) for d in context.dp_chain]


# ---------------------------------------------------------------------------
# When steps — decision tree
# ---------------------------------------------------------------------------


@when("I decision persistence build a 3-level decision tree")
def step_dp_build_tree(context: Context) -> None:
    """Build: root -> 2 children -> 2 grandchildren each = 7 nodes."""
    plan_id = context.dp_plan_id
    nodes: list[Decision] = []
    seq = 0

    root = _make_decision(
        plan_id=plan_id,
        sequence_number=seq,
        decision_type=DecisionType.PROMPT_DEFINITION,
        question="Root?",
        chosen_option="Start",
    )
    nodes.append(root)
    seq += 1

    for ci in range(2):
        child = _make_decision(
            plan_id=plan_id,
            parent_decision_id=root.decision_id,
            sequence_number=seq,
            decision_type=DecisionType.STRATEGY_CHOICE,
            question=f"Child {ci}?",
            chosen_option=f"Option C{ci}",
        )
        nodes.append(child)
        seq += 1

        for gi in range(2):
            grandchild = _make_decision(
                plan_id=plan_id,
                parent_decision_id=child.decision_id,
                sequence_number=seq,
                decision_type=DecisionType.IMPLEMENTATION_CHOICE,
                question=f"Grandchild {ci}.{gi}?",
                chosen_option=f"Impl {ci}.{gi}",
            )
            nodes.append(grandchild)
            seq += 1

    context.dp_tree = nodes


@when("I decision persistence serialize and deserialize all tree nodes")
def step_dp_roundtrip_tree(context: Context) -> None:
    context.dp_tree_restored = [_roundtrip_dump(n) for n in context.dp_tree]


# ---------------------------------------------------------------------------
# When steps — all types
# ---------------------------------------------------------------------------


@when("I decision persistence create and round-trip all decision types")
def step_dp_all_types(context: Context) -> None:
    plan_id = context.dp_plan_id
    results: list[tuple[Decision, Decision]] = []
    for i, dt in enumerate(_ALL_DECISION_TYPES):
        kwargs: dict[str, Any] = {
            "plan_id": plan_id,
            "sequence_number": i,
            "decision_type": dt,
            "question": f"Q for {dt.value}?",
            "chosen_option": f"A for {dt.value}",
        }
        if dt != DecisionType.PROMPT_DEFINITION:
            kwargs["parent_decision_id"] = str(ULID())
        original = Decision(**kwargs)
        restored = _roundtrip_dump(original)
        results.append((original, restored))
    context.dp_all_types_results = results


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


@then("the decision persistence restored decision should match the original")
def step_dp_match_original(context: Context) -> None:
    orig = context.dp_decision
    rest = context.dp_restored
    assert rest.decision_id == orig.decision_id
    assert rest.plan_id == orig.plan_id
    assert rest.parent_decision_id == orig.parent_decision_id
    assert rest.sequence_number == orig.sequence_number
    assert rest.decision_type == orig.decision_type
    assert rest.question == orig.question
    assert rest.chosen_option == orig.chosen_option
    assert rest.confidence_score == orig.confidence_score
    assert rest.is_correction == orig.is_correction
    assert rest.corrects_decision_id == orig.corrects_decision_id
    assert rest.superseded_by == orig.superseded_by
    assert rest.rationale == orig.rationale


@then("the decision persistence restored alternatives should have {count:d} entries")
def step_dp_alternatives_count(context: Context, count: int) -> None:
    assert len(context.dp_restored.alternatives_considered) == count


@then("the decision persistence restored snapshot hash should match")
def step_dp_snapshot_hash_match(context: Context) -> None:
    assert (
        context.dp_restored.context_snapshot.hot_context_hash
        == context.dp_decision.context_snapshot.hot_context_hash
    )


@then("the decision persistence restored snapshot should have {count:d} resources")
def step_dp_snapshot_resource_count(context: Context, count: int) -> None:
    assert len(context.dp_restored.context_snapshot.relevant_resources) == count


@then("the decision persistence restored snapshot hash should be empty")
def step_dp_snapshot_hash_empty(context: Context) -> None:
    assert context.dp_restored.context_snapshot.hot_context_hash == ""


@then('the decision persistence restored actor state ref should be "{ref}"')
def step_dp_actor_state_ref(context: Context, ref: str) -> None:
    assert context.dp_restored.context_snapshot.actor_state_ref == ref


@then("the decision persistence restored decision should have {count:d} artifacts")
def step_dp_artifact_count(context: Context, count: int) -> None:
    assert len(context.dp_restored.artifacts_produced) == count


@then("the decision persistence restored is_correction should be true")
def step_dp_is_correction_true(context: Context) -> None:
    assert context.dp_restored.is_correction is True


@then("the decision persistence restored corrects_decision_id should match")
def step_dp_corrects_id_match(context: Context) -> None:
    assert (
        context.dp_restored.corrects_decision_id
        == context.dp_decision.corrects_decision_id
    )


@then("the decision persistence restored is_superseded should be true")
def step_dp_is_superseded_true(context: Context) -> None:
    assert context.dp_restored.is_superseded is True


@then("the decision persistence correction chain should be reconstructable")
def step_dp_chain_reconstructable(context: Context) -> None:
    originals = context.dp_chain
    restored = context.dp_chain_restored
    assert len(restored) == len(originals)

    for orig, rest in zip(originals, restored, strict=True):
        assert rest.decision_id == orig.decision_id
        assert rest.is_correction == orig.is_correction
        assert rest.corrects_decision_id == orig.corrects_decision_id
        assert rest.superseded_by == orig.superseded_by

    # Verify the chain linkage
    for i in range(1, len(restored)):
        assert restored[i].corrects_decision_id == restored[i - 1].decision_id
        assert restored[i - 1].superseded_by == restored[i].decision_id


@then("the decision persistence tree parent links should be intact")
def step_dp_tree_parent_links(context: Context) -> None:
    nodes = context.dp_tree_restored
    id_set = {n.decision_id for n in nodes}
    root_count = 0
    for n in nodes:
        if n.parent_decision_id is None:
            root_count += 1
        else:
            assert n.parent_decision_id in id_set, (
                f"Parent {n.parent_decision_id} not found in tree"
            )
    assert root_count == 1, f"Expected 1 root, found {root_count}"


@then("the decision persistence tree should have {count:d} nodes")
def step_dp_tree_node_count(context: Context, count: int) -> None:
    assert len(context.dp_tree_restored) == count


@then("the decision persistence restored should have {count:d} downstream IDs")
def step_dp_downstream_ids_count(context: Context, count: int) -> None:
    assert len(context.dp_restored.downstream_decision_ids) == count


@then("the decision persistence restored should have {count:d} downstream plan IDs")
def step_dp_downstream_plan_ids_count(context: Context, count: int) -> None:
    assert len(context.dp_restored.downstream_plan_ids) == count


@then("the decision persistence restored confidence should be {value}")
def step_dp_confidence_value(context: Context, value: str) -> None:
    if value == "None":
        assert context.dp_restored.confidence_score is None
    else:
        assert context.dp_restored.confidence_score == float(value)


@then("all decision persistence round-trips should succeed")
def step_dp_all_types_ok(context: Context) -> None:
    results = context.dp_all_types_results
    assert len(results) == len(DecisionType)
    for original, restored in results:
        assert restored.decision_id == original.decision_id
        assert restored.decision_type == original.decision_type
        assert restored.question == original.question
        assert restored.chosen_option == original.chosen_option


@then("the decision persistence restored rationale length should be {length:d}")
def step_dp_rationale_length(context: Context, length: int) -> None:
    assert len(context.dp_restored.rationale) == length
