"""Shared mock fixtures for TDD plan-correct auto-resolve tests.

Provides constants, mock builders, and CLI argument helpers used by both
the Behave step definitions
(``features/steps/tdd_plan_correct_auto_resolve_steps.py``) and the Robot
Framework integration test helper
(``robot/helper_tdd_plan_correct_auto_resolve.py``).

Centralising the mock builders eliminates duplication and ensures both
test suites exercise the ``plan correct`` CLI auto-resolve path with
identically-shaped mock objects.

Bug: https://git.cleverthis.com/cleveragents/cleveragents-core/issues/1025
TDD: https://git.cleverthis.com/cleveragents/cleveragents-core/issues/1035
"""

from __future__ import annotations

from datetime import datetime
from types import SimpleNamespace
from unittest.mock import MagicMock

from cleveragents.core.exceptions import ResourceNotFoundError
from cleveragents.domain.models.core.plan import (
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
    ProjectLink,
)

# ---------------------------------------------------------------------------
# Patch targets
# ---------------------------------------------------------------------------

PATCH_CONTAINER: str = "cleveragents.application.container.get_container"
PATCH_CORRECTION_SVC: str = (
    "cleveragents.application.services.correction_service.CorrectionService"
)
# We intentionally do NOT patch ``_resolve_active_plan_id`` because
# this test exercises the auto-resolve path itself.

# ---------------------------------------------------------------------------
# Fixed identifiers for deterministic assertions
# ---------------------------------------------------------------------------

# The decision_id passed as the positional argument (not a plan_id).
DECISION_ID: str = "DEC-1025-TARGET"
# Root decision in the plan's tree.
ROOT_DECISION_ID: str = "DEC-1025-ROOT"


# ---------------------------------------------------------------------------
# Mock builders
# ---------------------------------------------------------------------------


def make_decision_ns(
    decision_id: str,
    parent_decision_id: str | None,
) -> SimpleNamespace:
    """Create a minimal decision-like namespace for list_decisions."""
    return SimpleNamespace(
        decision_id=decision_id,
        parent_decision_id=parent_decision_id,
    )


def _make_plan() -> Plan:
    """Build a real ``Plan`` in Execute/COMPLETE state.

    This simulates a plan that has completed the Execute phase — the
    exact state described in bug #1025.  A fresh ULID is used for the
    identity to satisfy the 26-char ULID validation constraint.
    """
    from ulid import ULID

    return Plan(
        identity=PlanIdentity(plan_id=str(ULID())),
        namespaced_name=NamespacedName(
            server=None, namespace="local", name="tdd-1025-plan"
        ),
        action_name="local/tdd-1025-action",
        description="TDD plan for bug #1025 — Execute/COMPLETE state",
        definition_of_done=None,
        phase=PlanPhase.EXECUTE,
        processing_state=ProcessingState.COMPLETE,
        project_links=[ProjectLink(project_name="proj-1")],
        strategy_actor="openai/gpt-4",
        execution_actor="openai/gpt-4",
        created_by=None,
        reusable=False,
        read_only=False,
        timestamps=PlanTimestamps(created_at=datetime.now(), updated_at=datetime.now()),
    )


def make_full_container() -> MagicMock:
    """Build a mock DI container that has the plan and decision tree.

    This container simulates the "main" context where the plan exists
    in the database.  Both ``plan_lifecycle_service`` and
    ``decision_service`` return valid data.
    """
    plan = _make_plan()

    mock_plan_svc = MagicMock()
    # get_plan raises RNF for the decision_id (it's not a plan_id),
    # ensuring the code falls through to the decision_id path.
    mock_plan_svc.get_plan.side_effect = ResourceNotFoundError(
        resource_type="Plan",
        resource_id=DECISION_ID,
    )
    # list_plans returns the plan in Execute/COMPLETE — auto-resolve
    # should find this.
    mock_plan_svc.list_plans.return_value = [plan]

    decisions = [
        make_decision_ns(ROOT_DECISION_ID, None),
        make_decision_ns(DECISION_ID, ROOT_DECISION_ID),
    ]

    mock_decision_svc = MagicMock()
    mock_decision_svc.list_decisions.return_value = decisions
    mock_decision_svc.get_influence_edges.return_value = {}

    mock_container = MagicMock()
    mock_container.plan_lifecycle_service.return_value = mock_plan_svc
    mock_container.decision_service.return_value = mock_decision_svc
    return mock_container


def make_isolated_container() -> MagicMock:
    """Build a mock DI container simulating an isolated E2E environment.

    In an isolated E2E subprocess environment (where
    ``CLEVERAGENTS_HOME`` points to a fresh temp directory), the
    ``plan_lifecycle_service`` resolves to a fresh database that has
    **no plans**.  This container simulates that condition.
    """
    mock_plan_svc = MagicMock()
    mock_plan_svc.list_plans.return_value = []  # No plans in isolated DB

    mock_container = MagicMock()
    mock_container.plan_lifecycle_service.return_value = mock_plan_svc
    return mock_container


def make_container_side_effects() -> list[MagicMock]:
    """Build the side_effect list for ``get_container`` calls.

    The ``correct_decision`` function calls ``get_container()`` once
    for its own use, then ``_resolve_active_plan_id`` calls
    ``_get_lifecycle_service`` → ``get_container()`` a second time.

    In isolated E2E environments, the second call resolves to a fresh
    container with an empty database.  This side-effect list simulates
    that divergence:

    - Call 1: full container (has plans, decisions, services)
    - Call 2: isolated container (empty plan list)

    Returns:
        A two-element list suitable for ``MagicMock.side_effect``.
    """
    return [make_full_container(), make_isolated_container()]


def make_correction_svc(
    target_decision_id: str,
    mode: str = "revert",
) -> MagicMock:
    """Build a mock CorrectionService that succeeds for a given target.

    Args:
        target_decision_id: The decision ID expected in the correction.
        mode: The correction mode (``"revert"`` or ``"append"``).
    """
    svc = MagicMock()
    svc.request_correction.return_value = SimpleNamespace(
        correction_id="CORR-TDD-1025",
        mode=SimpleNamespace(value=mode),
        target_decision_id=target_decision_id,
        guidance="Recompute decision subtree",
    )
    svc.execute_correction.return_value = SimpleNamespace(
        correction_id="CORR-TDD-1025",
        status=SimpleNamespace(value="applied"),
        reverted_decisions=[target_decision_id],
        new_decisions=[],
    )
    return svc


def build_cli_args(decision_id: str, mode: str = "revert") -> list[str]:
    """Build the CLI argument list for ``plan correct <decision_id>``.

    The ``--plan`` flag is intentionally **omitted** so that the code
    must auto-resolve the active plan via ``_resolve_active_plan_id()``.

    Args:
        decision_id: The decision ID to pass as the first positional arg.
        mode: The correction mode (``"revert"`` or ``"append"``).
    """
    return [
        "correct",
        decision_id,
        "--mode",
        mode,
        "--guidance",
        "Recompute decision subtree",
        "--yes",
        "--format",
        "plain",
    ]


__all__: list[str] = [
    "DECISION_ID",
    "PATCH_CONTAINER",
    "PATCH_CORRECTION_SVC",
    "ROOT_DECISION_ID",
    "build_cli_args",
    "make_container_side_effects",
    "make_correction_svc",
    "make_decision_ns",
    "make_full_container",
    "make_isolated_container",
]
