"""Helper script for m4_correction_subplan_smoke.robot E2E tests.

Each subcommand is a self-contained check that prints a sentinel on success.
"""

from __future__ import annotations

import json
import sys
from collections.abc import Callable
from datetime import UTC, datetime
from pathlib import Path
from unittest.mock import MagicMock, patch

# Ensure local source tree is importable
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
    sys.path.insert(0, _SRC)

from helpers_common import reset_global_state  # noqa: E402
from typer.testing import CliRunner  # noqa: E402
from ulid import ULID  # noqa: E402

from cleveragents.application.container import Container  # noqa: E402
from cleveragents.cli.commands.plan import app as plan_app  # noqa: E402
from cleveragents.domain.models.core.correction import (  # noqa: E402
    CorrectionImpact,
    CorrectionResult,
    CorrectionStatus,
)
from cleveragents.domain.models.core.plan import (  # noqa: E402
    ExecutionMode,
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
    SubplanConfig,
    SubplanFailureHandler,
    SubplanMergeStrategy,
    SubplanStatus,
)

runner = CliRunner()


_PLAN_ULID = str(ULID())
_DECISION_ULID = str(ULID())
_CORRECTION_ID_1 = str(ULID())
_CORRECTION_ID_2 = str(ULID())
_SUBPLAN_ID_1 = str(ULID())
_SUBPLAN_ID_2 = str(ULID())
_SUBPLAN_ID_3 = str(ULID())
_SUBPLAN_ID_4 = str(ULID())
_SUBPLAN_ID_5 = str(ULID())
_NEW_DECISION_ULID = str(ULID())
_FIXTURES_DIR = Path(__file__).resolve().parents[1] / "features" / "fixtures" / "m4"


def _mock_plan(
    *,
    phase: PlanPhase = PlanPhase.EXECUTE,
    state: ProcessingState = ProcessingState.COMPLETE,
    subplan_config: SubplanConfig | None = None,
    subplan_statuses: list[SubplanStatus] | None = None,
) -> Plan:
    now = datetime.now()
    return Plan(
        identity=PlanIdentity(plan_id=_PLAN_ULID),
        namespaced_name=NamespacedName.parse("local/m4-smoke-plan"),
        description="M4 smoke test plan",
        definition_of_done="Corrections applied and subplans merged",
        action_name="local/m4-correction-test",
        phase=phase,
        processing_state=state,
        project_links=[],
        arguments={},
        arguments_order=[],
        strategy_actor="openai/gpt-4",
        execution_actor="openai/gpt-4",
        reusable=True,
        read_only=False,
        created_by=None,
        timestamps=PlanTimestamps(created_at=now, updated_at=now),
        decision_root_id=_DECISION_ULID,
        subplan_config=subplan_config,
        subplan_statuses=subplan_statuses or [],
    )


def _mock_correction_service() -> MagicMock:
    """Create a mock CorrectionService."""
    mock_svc = MagicMock()
    now = datetime.now(UTC)
    mock_svc.apply_correction.return_value = CorrectionResult(
        correction_id=_CORRECTION_ID_1,
        status=CorrectionStatus.APPLIED,
        reverted_decisions=[_DECISION_ULID],
        completed_at=now,
    )
    mock_svc.generate_dry_run_report.return_value = MagicMock(
        impact=CorrectionImpact(
            affected_decisions=[_DECISION_ULID],
            affected_files=["src/auth.py"],
            risk_level="medium",
            rollback_tier="phase",
        ),
    )
    return mock_svc


# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------


def _mock_container() -> MagicMock:
    """Create a mock DI container with a stub DecisionService provider."""
    mock_decision_svc = MagicMock()
    mock_decision_svc.list_decisions.return_value = []
    mock_decision_svc.get_influence_edges.return_value = {}
    container = MagicMock(spec=Container)
    container.decision_service.return_value = mock_decision_svc
    return container


def correction_revert() -> None:
    """Invoke plan correct --mode revert."""
    mock_svc = MagicMock()
    mock_svc.get_plan.return_value = _mock_plan()
    mock_correction = _mock_correction_service()

    with (
        patch(
            "cleveragents.cli.commands.plan._get_lifecycle_service",
            return_value=mock_svc,
        ),
        patch(
            "cleveragents.application.services.correction_service.CorrectionService",
            return_value=mock_correction,
        ),
        patch(
            "cleveragents.application.container.get_container",
            return_value=_mock_container(),
        ),
    ):
        result = runner.invoke(
            plan_app,
            [
                "correct",
                _DECISION_ULID,
                "--mode",
                "revert",
                "--guidance",
                "Use FastAPI instead",
                "--plan",
                _PLAN_ULID,
                "--yes",
            ],
        )
    if result.exit_code == 0:
        print("m4-correction-revert-ok")
    else:
        print(f"FAIL: exit={result.exit_code} output={result.output}")
        sys.exit(1)


def correction_append() -> None:
    """Invoke plan correct --mode append."""
    mock_svc = MagicMock()
    mock_svc.get_plan.return_value = _mock_plan()
    mock_correction = _mock_correction_service()
    mock_correction.apply_correction.return_value = CorrectionResult(
        correction_id=_CORRECTION_ID_2,
        status=CorrectionStatus.APPLIED,
        new_decisions=[_NEW_DECISION_ULID],
        completed_at=datetime.now(UTC),
    )

    with (
        patch(
            "cleveragents.cli.commands.plan._get_lifecycle_service",
            return_value=mock_svc,
        ),
        patch(
            "cleveragents.application.services.correction_service.CorrectionService",
            return_value=mock_correction,
        ),
        patch(
            "cleveragents.application.container.get_container",
            return_value=_mock_container(),
        ),
    ):
        result = runner.invoke(
            plan_app,
            [
                "correct",
                _DECISION_ULID,
                "--mode",
                "append",
                "--guidance",
                "Add caching layer",
                "--plan",
                _PLAN_ULID,
                "--yes",
            ],
        )
    if result.exit_code == 0:
        print("m4-correction-append-ok")
    else:
        print(f"FAIL: exit={result.exit_code} output={result.output}")
        sys.exit(1)


def correction_dry_run() -> None:
    """Invoke plan correct --dry-run."""
    mock_svc = MagicMock()
    mock_svc.get_plan.return_value = _mock_plan()
    mock_correction = _mock_correction_service()

    with (
        patch(
            "cleveragents.cli.commands.plan._get_lifecycle_service",
            return_value=mock_svc,
        ),
        patch(
            "cleveragents.application.services.correction_service.CorrectionService",
            return_value=mock_correction,
        ),
        patch(
            "cleveragents.application.container.get_container",
            return_value=_mock_container(),
        ),
    ):
        result = runner.invoke(
            plan_app,
            [
                "correct",
                _DECISION_ULID,
                "--mode",
                "revert",
                "--dry-run",
                "--guidance",
                "Preview correction impact",
                "--plan",
                _PLAN_ULID,
            ],
        )
    if result.exit_code == 0:
        print("m4-correction-dry-run-ok")
    else:
        print(f"FAIL: exit={result.exit_code} output={result.output}")
        sys.exit(1)


def subplan_status_sequential() -> None:
    """Verify plan status shows subplan count for sequential config."""
    config = SubplanConfig(
        execution_mode=ExecutionMode.SEQUENTIAL,
        merge_strategy=SubplanMergeStrategy.GIT_THREE_WAY,
        max_parallel=1,
    )
    statuses = [
        SubplanStatus(
            subplan_id=_SUBPLAN_ID_1,
            action_name="local/refactor-auth",
            target_resources=["src/auth.py"],
            status=ProcessingState.COMPLETE,
        ),
    ]
    plan = _mock_plan(subplan_config=config, subplan_statuses=statuses)

    mock_svc = MagicMock()
    mock_svc.get_plan.return_value = plan
    mock_svc.list_plans.return_value = [plan]

    with patch(
        "cleveragents.cli.commands.plan._get_lifecycle_service",
        return_value=mock_svc,
    ):
        result = runner.invoke(plan_app, ["status", _PLAN_ULID, "--format", "json"])
    if result.exit_code == 0 and "plan_id" in result.output:
        print("m4-subplan-status-sequential-ok")
    else:
        print(f"FAIL: exit={result.exit_code} output={result.output}")
        sys.exit(1)


def subplan_status_parallel() -> None:
    """Verify plan status shows subplan info for parallel config."""
    config = SubplanConfig(
        execution_mode=ExecutionMode.PARALLEL,
        merge_strategy=SubplanMergeStrategy.GIT_THREE_WAY,
        max_parallel=5,
    )
    statuses = [
        SubplanStatus(
            subplan_id=_SUBPLAN_ID_2,
            action_name="local/update-api",
            target_resources=["src/api.py"],
            status=ProcessingState.COMPLETE,
        ),
        SubplanStatus(
            subplan_id=_SUBPLAN_ID_3,
            action_name="local/update-tests",
            target_resources=["tests/test_api.py"],
            status=ProcessingState.COMPLETE,
        ),
    ]
    plan = _mock_plan(subplan_config=config, subplan_statuses=statuses)

    mock_svc = MagicMock()
    mock_svc.get_plan.return_value = plan
    mock_svc.list_plans.return_value = [plan]

    with patch(
        "cleveragents.cli.commands.plan._get_lifecycle_service",
        return_value=mock_svc,
    ):
        result = runner.invoke(plan_app, ["status", _PLAN_ULID, "--format", "json"])
    if result.exit_code == 0:
        print("m4-subplan-status-parallel-ok")
    else:
        print(f"FAIL: exit={result.exit_code} output={result.output}")
        sys.exit(1)


def failure_handler() -> None:
    """Evaluate SubplanFailureHandler decisions."""
    handler = SubplanFailureHandler()

    # fail-fast should stop others
    config_ff = SubplanConfig(execution_mode=ExecutionMode.PARALLEL, fail_fast=True)
    failed = SubplanStatus(
        subplan_id=_SUBPLAN_ID_4,
        action_name="local/task-b",
        status=ProcessingState.ERRORED,
        error="ConfigurationError: missing config",
    )
    assert handler.should_stop_others(config_ff, failed) is True

    # retry retriable
    config_retry = SubplanConfig(
        execution_mode=ExecutionMode.SEQUENTIAL,
        retry_failed=True,
        max_retries=2,
    )
    retriable = SubplanStatus(
        subplan_id=_SUBPLAN_ID_5,
        action_name="local/flaky-task",
        status=ProcessingState.ERRORED,
        error="ValidationError: schema mismatch",
        attempt_number=1,
    )
    assert handler.should_retry(config_retry, retriable) is True

    # skip non-retriable
    non_retriable = SubplanStatus(
        subplan_id=str(ULID()),
        action_name="local/broken-task",
        status=ProcessingState.ERRORED,
        error="ConfigurationError: permanent failure",
        attempt_number=1,
    )
    assert handler.should_retry(config_retry, non_retriable) is False

    print("m4-failure-handler-ok")


def fixture_loading() -> None:
    """Load all M4 fixture files and verify structure."""
    for fname in (
        "correction_flows.json",
        "subplan_execution.json",
        "conflict_simulations.json",
    ):
        fpath = _FIXTURES_DIR / fname
        with open(fpath) as f:
            data = json.load(f)
        assert "fixtures" in data, f"Missing 'fixtures' key in {fname}"
        assert len(data["fixtures"]) > 0, f"Empty fixtures in {fname}"
    print("m4-fixture-loading-ok")


def full_flow() -> None:
    """End-to-end: correction + subplan status + failure handler.

    This subcommand chains three logical steps inside a single process.
    ``reset_global_state()`` is called between steps to ensure the
    Settings singleton, DI container, and engine cache do not carry
    stale references from one invocation into the next.  The initial
    reset is handled by the ``__main__`` dispatcher before calling
    this function.
    """
    # Step 1: correction revert (no reset needed — __main__ already reset)
    mock_svc = MagicMock()
    mock_svc.get_plan.return_value = _mock_plan()
    mock_correction = _mock_correction_service()

    with (
        patch(
            "cleveragents.cli.commands.plan._get_lifecycle_service",
            return_value=mock_svc,
        ),
        patch(
            "cleveragents.application.services.correction_service.CorrectionService",
            return_value=mock_correction,
        ),
        patch(
            "cleveragents.application.container.get_container",
            return_value=_mock_container(),
        ),
    ):
        r1 = runner.invoke(
            plan_app,
            [
                "correct",
                _DECISION_ULID,
                "--mode",
                "revert",
                "--guidance",
                "Full flow test",
                "--plan",
                _PLAN_ULID,
                "--yes",
            ],
        )
    if r1.exit_code != 0:
        print(f"FAIL step 1: exit={r1.exit_code} output={r1.output}")
        sys.exit(1)

    # Step 2: subplan status
    reset_global_state()
    config = SubplanConfig(
        execution_mode=ExecutionMode.SEQUENTIAL,
        merge_strategy=SubplanMergeStrategy.GIT_THREE_WAY,
    )
    statuses = [
        SubplanStatus(
            subplan_id=_SUBPLAN_ID_1,
            action_name="local/refactor-auth",
            status=ProcessingState.COMPLETE,
        ),
    ]
    plan = _mock_plan(subplan_config=config, subplan_statuses=statuses)
    mock_svc2 = MagicMock()
    mock_svc2.get_plan.return_value = plan

    with patch(
        "cleveragents.cli.commands.plan._get_lifecycle_service",
        return_value=mock_svc2,
    ):
        r2 = runner.invoke(plan_app, ["status", _PLAN_ULID, "--format", "json"])
    if r2.exit_code != 0:
        print(f"FAIL step 2: exit={r2.exit_code} output={r2.output}")
        sys.exit(1)

    # Step 3: failure handler (pure domain logic — no CLI / DB)
    handler = SubplanFailureHandler()
    config_ff = SubplanConfig(fail_fast=True)
    failed = SubplanStatus(
        subplan_id=_SUBPLAN_ID_4,
        action_name="local/task-b",
        status=ProcessingState.ERRORED,
        error="ConfigurationError: fail",
    )
    assert handler.should_stop_others(config_ff, failed) is True

    print("m4-full-flow-ok")


# ---------------------------------------------------------------------------
# Dispatcher
# ---------------------------------------------------------------------------

_COMMANDS: dict[str, Callable[[], None]] = {
    "correction-revert": correction_revert,
    "correction-append": correction_append,
    "correction-dry-run": correction_dry_run,
    "subplan-status-sequential": subplan_status_sequential,
    "subplan-status-parallel": subplan_status_parallel,
    "failure-handler": failure_handler,
    "fixture-loading": fixture_loading,
    "full-flow": full_flow,
}

if __name__ == "__main__":
    if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
        print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>")
        sys.exit(1)
    # Reset singletons once at process entry; full_flow() resets again
    # between each chained CLI invocation.
    reset_global_state()
    fn = _COMMANDS[sys.argv[1]]
    fn()
