"""Step definitions for validation-gated apply pipeline tests.

Uses mock objects for PlanLifecycleService and Plan to test the
validation gate logic in PlanApplyService without needing a database.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock

from behave import given, then, when
from behave.runner import Context

from cleveragents.application.services.plan_apply_service import (
    ApplyOutcome,
    PlanApplyService,
)
from cleveragents.domain.models.core.change import (
    ChangeEntry,
    ChangeOperation,
    SpecChangeSet,
)
from cleveragents.domain.models.core.plan import (
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
)

__all__: list[str] = []


# ---------------------------------------------------------------------------
# Mock helpers
# ---------------------------------------------------------------------------

_PLAN_ID = "01PLANTEST000000000000001"


def _make_mock_plan(
    *,
    plan_id: str = _PLAN_ID,
    phase: PlanPhase = PlanPhase.EXECUTE,
    state: ProcessingState = ProcessingState.COMPLETE,
    changeset_id: str | None = "CS001",
    validation_summary: dict[str, Any] | None = None,
    is_terminal: bool = False,
) -> MagicMock:
    """Create a mock Plan object."""
    plan = MagicMock()
    plan.identity.plan_id = plan_id
    plan.phase = phase
    plan.processing_state = state
    plan.changeset_id = changeset_id
    plan.validation_summary = validation_summary
    plan.is_terminal = is_terminal
    plan.error_details = None
    plan.timestamps = PlanTimestamps()
    return plan


def _make_changeset(
    plan_id: str = _PLAN_ID,
    n_files: int = 1,
) -> SpecChangeSet:
    """Create a SpecChangeSet with n_files entries."""
    cs = SpecChangeSet(plan_id=plan_id)
    for i in range(n_files):
        cs.add_change(
            ChangeEntry(
                plan_id=plan_id,
                resource_id="RES001",
                tool_name="builtin/test-tool",
                operation=ChangeOperation.MODIFY,
                path=f"src/file_{i}.py",
                before_hash=f"before-{i}",
                after_hash=f"after-{i}",
            )
        )
    return cs


def _make_service(
    plan: MagicMock,
    changeset: SpecChangeSet | None = None,
) -> PlanApplyService:
    """Create a PlanApplyService with mocked lifecycle and store."""
    lifecycle = MagicMock()
    lifecycle.get_plan.return_value = plan
    lifecycle.complete_apply.return_value = plan
    lifecycle.constrain_apply.return_value = plan
    lifecycle._commit_plan = MagicMock()

    store = MagicMock()
    if changeset is not None:
        store.get.return_value = changeset
    else:
        store.get.return_value = None

    return PlanApplyService(
        lifecycle_service=lifecycle,
        changeset_store=store,
    )


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


@given("an apply pipeline test environment")
def step_given_apply_env(context: Context) -> None:
    context.ap_plan_id = _PLAN_ID
    context.ap_plan = None
    context.ap_changeset = None
    context.ap_service = None
    context.ap_result = None  # ApplyResult | None
    context.ap_allow_empty = False
    context.ap_external_summary = None  # dict[str, Any] | None


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


@given("a plan with changeset containing {n} files")
def step_given_plan_with_files(context: Context, n: str) -> None:
    cs = _make_changeset(n_files=int(n))
    plan = _make_mock_plan(changeset_id=cs.changeset_id)
    context.ap_plan = plan
    context.ap_changeset = cs
    context.ap_service = _make_service(plan, cs)


@given("the plan has validation summary with {p} required passed and {f} failed")
def step_given_validation_summary(context: Context, p: str, f: str) -> None:
    vs = {
        "total": int(p) + int(f),
        "required_passed": int(p),
        "required_failed": int(f),
        "informational_passed": 0,
        "informational_failed": 0,
    }
    context.ap_plan.validation_summary = vs


@given(
    "the plan has validation summary with {p} required passed "
    "and {f} failed and {i} informational"
)
def step_given_validation_summary_with_info(
    context: Context, p: str, f: str, i: str
) -> None:
    vs = {
        "total": int(p) + int(f) + int(i),
        "required_passed": int(p),
        "required_failed": int(f),
        "informational_passed": int(i),
        "informational_failed": 0,
    }
    context.ap_plan.validation_summary = vs


@given("the plan has no validation summary")
def step_given_no_validation_summary(context: Context) -> None:
    context.ap_plan.validation_summary = None


@given("a plan that is already applied")
def step_given_already_applied(context: Context) -> None:
    plan = _make_mock_plan(
        phase=PlanPhase.APPLY,
        state=ProcessingState.APPLIED,
        is_terminal=True,
    )
    cs = _make_changeset()
    context.ap_plan = plan
    context.ap_changeset = cs
    context.ap_service = _make_service(plan, cs)


@given("a plan that is in errored state")
def step_given_errored_plan(context: Context) -> None:
    plan = _make_mock_plan(
        phase=PlanPhase.APPLY,
        state=ProcessingState.ERRORED,
        is_terminal=True,
    )
    cs = _make_changeset()
    context.ap_plan = plan
    context.ap_changeset = cs
    context.ap_service = _make_service(plan, cs)


@given("a plan that is in cancelled state")
def step_given_cancelled_plan(context: Context) -> None:
    plan = _make_mock_plan(
        phase=PlanPhase.APPLY,
        state=ProcessingState.CANCELLED,
        is_terminal=True,
    )
    cs = _make_changeset()
    context.ap_plan = plan
    context.ap_changeset = cs
    context.ap_service = _make_service(plan, cs)


@given("a plan with empty changeset")
def step_given_empty_changeset(context: Context) -> None:
    cs = _make_changeset(n_files=0)
    plan = _make_mock_plan(changeset_id=cs.changeset_id)
    context.ap_plan = plan
    context.ap_changeset = cs
    context.ap_service = _make_service(plan, cs)


@given("an apply plan with no changeset assigned")
def step_given_no_changeset(context: Context) -> None:
    plan = _make_mock_plan(changeset_id=None)
    context.ap_plan = plan
    context.ap_changeset = None
    context.ap_service = _make_service(plan, None)


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


@when("I apply the plan with validation gate")
def step_when_apply(context: Context) -> None:
    context.ap_result = context.ap_service.apply_with_validation_gate(
        plan_id=context.ap_plan_id,
        allow_empty=context.ap_allow_empty,
    )


@when("I apply the plan with validation gate and allow_empty")
def step_when_apply_allow_empty(context: Context) -> None:
    context.ap_result = context.ap_service.apply_with_validation_gate(
        plan_id=context.ap_plan_id,
        allow_empty=True,
    )


@when(
    "I apply the plan with external validation summary having {p} passed and {f} failed"
)
def step_when_apply_external_summary(context: Context, p: str, f: str) -> None:
    ext_summary = {
        "total": int(p) + int(f),
        "required_passed": int(p),
        "required_failed": int(f),
    }
    context.ap_result = context.ap_service.apply_with_validation_gate(
        plan_id=context.ap_plan_id,
        validation_summary=ext_summary,
    )


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


@then('the apply outcome should be "{outcome}"')
def step_then_outcome(context: Context, outcome: str) -> None:
    assert context.ap_result is not None, "No apply result"
    expected = ApplyOutcome(outcome)
    assert context.ap_result.outcome == expected, (
        f"Expected outcome '{outcome}', got '{context.ap_result.outcome}'"
    )


@then("the apply result files changed should be {n}")
def step_then_files_changed(context: Context, n: str) -> None:
    assert context.ap_result is not None
    assert context.ap_result.files_changed == int(n), (
        f"Expected files_changed={n}, got {context.ap_result.files_changed}"
    )


@then("the apply result validations total should be {n}")
def step_then_validations_total(context: Context, n: str) -> None:
    assert context.ap_result is not None
    assert context.ap_result.validations_total == int(n), (
        f"Expected validations_total={n}, got {context.ap_result.validations_total}"
    )


@then("the apply result validations failed should be {n}")
def step_then_validations_failed(context: Context, n: str) -> None:
    assert context.ap_result is not None
    assert context.ap_result.validations_failed == int(n), (
        f"Expected validations_failed={n}, got {context.ap_result.validations_failed}"
    )


@then('the apply result message should contain "{text}"')
def step_then_message_contains(context: Context, text: str) -> None:
    assert context.ap_result is not None
    assert text in context.ap_result.message, (
        f"Expected message to contain '{text}', got: {context.ap_result.message}"
    )


@then("the apply result plan_id should match the plan ID")
def step_then_plan_id_matches(context: Context) -> None:
    assert context.ap_result is not None
    assert context.ap_result.plan_id == context.ap_plan_id
