"""Helper utilities for v3 plan lifecycle Robot integration tests.

Covers the integration between:
- Plan and Action domain models (plan.py, action.py)
- PlanLifecycleService (plan_lifecycle_service.py)
- Automation levels
- Subplan support
"""

from __future__ import annotations

import sys
from datetime import UTC, datetime

from cleveragents.application.services.plan_lifecycle_service import (
    InvalidPhaseTransitionError,
    PlanLifecycleService,
    PlanNotReadyError,
)
from cleveragents.config.settings import Settings
from cleveragents.domain.models.core.action import (
    Action,
    ActionArgument,
    ActionState,
    ArgumentRequirement,
    ArgumentType,
)
from cleveragents.domain.models.core.plan import (
    AutomationProfileProvenance,
    AutomationProfileRef,
    ExecutionMode,
    NamespacedName,
    PlanPhase,
    ProcessingState,
    ProjectLink,
    SubplanConfig,
    SubplanFailureHandler,
    SubplanMergeStrategy,
    SubplanStatus,
    can_transition,
)


def _create_service() -> PlanLifecycleService:
    settings = Settings()
    return PlanLifecycleService(settings=settings)


def _lifecycle_full_cycle() -> None:
    """Integration test: create action -> use -> strategize -> execute -> apply."""
    service = _create_service()

    # Create action (state is AVAILABLE immediately per new spec)
    action = service.create_action(
        name="local/code-review",
        definition_of_done="All files reviewed and approved",
        strategy_actor="local/planner",
        execution_actor="local/executor",
        description="Code review action",
    )
    assert action.state == ActionState.AVAILABLE, (
        f"Expected AVAILABLE, got {action.state}"
    )

    # Use action to create plan (enters STRATEGIZE, QUEUED)
    plan = service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-001")],
        created_by="integration-test",
    )
    assert plan.phase == PlanPhase.STRATEGIZE
    assert plan.state == ProcessingState.QUEUED
    plan_id = plan.identity.plan_id

    # Strategize lifecycle
    plan = service.start_strategize(plan_id)
    assert plan.state == ProcessingState.PROCESSING

    plan = service.complete_strategize(plan_id)
    assert plan.state == ProcessingState.COMPLETE

    # Execute transition
    plan = service.execute_plan(plan_id)
    assert plan.phase == PlanPhase.EXECUTE
    assert plan.state == ProcessingState.QUEUED

    plan = service.start_execute(plan_id)
    assert plan.state == ProcessingState.PROCESSING

    plan = service.complete_execute(plan_id)
    assert plan.state == ProcessingState.COMPLETE

    # Apply transition
    plan = service.apply_plan(plan_id)
    assert plan.phase == PlanPhase.APPLY
    assert plan.state == ProcessingState.QUEUED

    plan = service.start_apply(plan_id)
    assert plan.state == ProcessingState.PROCESSING

    plan = service.complete_apply(plan_id)
    assert plan.phase == PlanPhase.APPLY
    assert plan.state == ProcessingState.APPLIED
    assert plan.is_terminal

    print("lifecycle-full-cycle-ok")


def _action_crud() -> None:
    """Integration test: action CRUD operations via service."""
    service = _create_service()

    # Create multiple actions (both AVAILABLE immediately)
    a1 = service.create_action(
        name="local/action-one",
        description="Action one",
        definition_of_done="Done 1",
        strategy_actor="local/s1",
        execution_actor="local/e1",
    )
    service.create_action(
        name="local/action-two",
        description="Action two",
        definition_of_done="Done 2",
        strategy_actor="local/s2",
        execution_actor="local/e2",
    )

    # List all
    actions = service.list_actions()
    assert len(actions) == 2, f"Expected 2 actions, got {len(actions)}"

    # List by namespace
    local_actions = service.list_actions(namespace="local")
    assert len(local_actions) == 2

    # Get by name
    found = service.get_action_by_name("local/action-one")
    assert str(found.namespaced_name) == str(a1.namespaced_name)

    # Archive
    archived = service.archive_action(str(a1.namespaced_name))
    assert archived.state == ActionState.ARCHIVED

    # List by state
    available = service.list_actions(state=ActionState.AVAILABLE)
    assert len(available) == 1  # Only action-two remains available

    archived_list = service.list_actions(state=ActionState.ARCHIVED)
    assert len(archived_list) == 1

    print("action-crud-ok")


def _plan_cancellation() -> None:
    """Integration test: plan cancellation at various phases."""
    service = _create_service()

    action = service.create_action(
        name="local/cancel-test",
        description="Cancel test action",
        definition_of_done="Test cancellation",
        strategy_actor="local/s",
        execution_actor="local/e",
    )

    plan = service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-001")],
    )
    plan_id = plan.identity.plan_id

    # Cancel while in STRATEGIZE
    cancelled = service.cancel_plan(plan_id, reason="User requested")
    assert cancelled.state == ProcessingState.CANCELLED
    assert cancelled.error_message == "User requested"

    print("plan-cancellation-ok")


def _plan_failure_recovery() -> None:
    """Integration test: plan failure and error states."""
    service = _create_service()

    action = service.create_action(
        name="local/fail-test",
        description="Fail test action",
        definition_of_done="Test failure",
        strategy_actor="local/s",
        execution_actor="local/e",
    )

    plan = service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-001")],
    )
    plan_id = plan.identity.plan_id

    # Start strategize then fail it
    service.start_strategize(plan_id)
    plan = service.fail_strategize(plan_id, "Resource unavailable")
    assert plan.state == ProcessingState.ERRORED
    assert plan.is_errored
    assert plan.error_message == "Resource unavailable"

    print("plan-failure-recovery-ok")


def _invalid_transitions() -> None:
    """Integration test: invalid phase transitions raise proper errors."""
    service = _create_service()

    action = service.create_action(
        name="local/invalid-trans",
        description="Invalid transitions test action",
        definition_of_done="Test invalid transitions",
        strategy_actor="local/s",
        execution_actor="local/e",
    )

    plan = service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-001")],
    )
    plan_id = plan.identity.plan_id

    # Try to execute before completing strategize (should fail)
    try:
        service.execute_plan(plan_id)
        print("FAIL: Expected PlanNotReadyError")
        return
    except PlanNotReadyError:
        pass

    # Try to apply before execute (should fail)
    try:
        service.apply_plan(plan_id)
        print("FAIL: Expected InvalidPhaseTransitionError or PlanNotReadyError")
        return
    except (InvalidPhaseTransitionError, PlanNotReadyError):
        pass

    print("invalid-transitions-ok")


def _automation_full_auto() -> None:
    """Integration test: full automation auto-progresses through all phases."""
    service = _create_service()

    action = service.create_action(
        name="local/full-auto",
        description="Full automation test action",
        definition_of_done="Test full automation",
        strategy_actor="local/s",
        execution_actor="local/e",
    )

    plan = service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-001")],
    )
    plan_id = plan.identity.plan_id

    # Set full-auto profile so auto_progress will advance the plan
    plan.automation_profile = AutomationProfileRef(
        profile_name="full-auto",
        provenance=AutomationProfileProvenance.PLAN,
    )

    # Start and complete strategize - should auto-progress to EXECUTE
    service.start_strategize(plan_id)
    plan = service.complete_strategize(plan_id)

    # After complete_strategize with full-auto profile, auto_progress kicks in
    plan = service.get_plan(plan_id)
    assert plan.phase == PlanPhase.EXECUTE, f"Expected EXECUTE, got {plan.phase}"

    # Start and complete execute - should auto-progress to APPLY
    service.start_execute(plan_id)
    plan = service.complete_execute(plan_id)

    plan = service.get_plan(plan_id)
    assert plan.phase == PlanPhase.APPLY, f"Expected APPLY, got {plan.phase}"

    print("automation-full-auto-ok")


def _automation_review_before_apply() -> None:
    """Integration test: review-before-apply pauses before apply."""
    service = _create_service()

    action = service.create_action(
        name="local/review-mode",
        description="Review mode test action",
        definition_of_done="Test review mode",
        strategy_actor="local/s",
        execution_actor="local/e",
    )

    plan = service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-001")],
    )
    plan_id = plan.identity.plan_id

    # Set "auto" profile (review-before-apply equivalent):
    # create_tool=0.0, select_tool=1.0 → advances after strategize but pauses at apply
    plan.automation_profile = AutomationProfileRef(
        profile_name="auto",
        provenance=AutomationProfileProvenance.PLAN,
    )

    # Complete strategize - should auto-progress to EXECUTE
    service.start_strategize(plan_id)
    plan = service.complete_strategize(plan_id)

    plan = service.get_plan(plan_id)
    assert plan.phase == PlanPhase.EXECUTE, f"Expected EXECUTE, got {plan.phase}"

    # Complete execute - should NOT auto-progress to APPLY (requires review)
    service.start_execute(plan_id)
    plan = service.complete_execute(plan_id)

    plan = service.get_plan(plan_id)
    # In review/auto mode, execute completes but does not auto-transition to APPLY
    assert plan.phase == PlanPhase.EXECUTE, (
        f"Expected EXECUTE (paused), got {plan.phase}"
    )
    assert plan.state == ProcessingState.COMPLETE

    print("automation-review-before-apply-ok")


def _automation_pause_resume() -> None:
    """Integration test: pause and resume automation."""
    service = _create_service()

    action = service.create_action(
        name="local/pause-resume",
        description="Pause/resume test action",
        definition_of_done="Test pause/resume",
        strategy_actor="local/s",
        execution_actor="local/e",
    )

    plan = service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-001")],
    )
    plan_id = plan.identity.plan_id

    # Pause - should set profile to manual
    plan = service.pause_plan(plan_id)
    assert plan.automation_profile is not None
    assert plan.automation_profile.profile_name == "manual"

    # Resume with auto profile
    plan = service.resume_plan(plan_id, automation_profile="auto")
    assert plan.automation_profile is not None
    assert plan.automation_profile.profile_name == "auto"

    print("automation-pause-resume-ok")


def _subplan_model() -> None:
    """Integration test: subplan configuration and failure handling."""
    config = SubplanConfig(
        execution_mode=ExecutionMode.PARALLEL,
        merge_strategy=SubplanMergeStrategy.GIT_THREE_WAY,
        max_parallel=3,
        fail_fast=True,
        timeout_per_subplan_seconds=600,
        retry_failed=True,
        max_retries=2,
    )
    assert config.execution_mode == ExecutionMode.PARALLEL
    assert config.max_parallel == 3

    # Create a subplan status (using valid ULID-format IDs)
    # Error strings must match RETRIABLE_FAILURES (PascalCase: TimeoutError, etc.)
    status = SubplanStatus(
        subplan_id="01KH72MDGQMPRW3R5WPZVB8KC1",
        action_name="local/sub-task",
        target_resources=["src/main.py"],
        status=ProcessingState.ERRORED,
        error="TimeoutError: Operation timed out",
    )

    handler = SubplanFailureHandler()
    should_stop = handler.should_stop_others(config, status)
    assert should_stop is True, "fail_fast=True should stop others"

    should_retry = handler.should_retry(config, status)
    assert should_retry is True, "TimeoutError is retriable and retries enabled"

    # Non-retriable error (must match NON_RETRIABLE_ERRORS: ConfigurationError, etc.)
    status_perm = SubplanStatus(
        subplan_id="01KH72MDGQMPRW3R5WPZVB8KC2",
        action_name="local/sub-task",
        target_resources=["src/other.py"],
        status=ProcessingState.ERRORED,
        error="ConfigurationError: Invalid config",
    )
    should_retry_perm = handler.should_retry(config, status_perm)
    assert should_retry_perm is False, "ConfigurationError is not retriable"

    print("subplan-model-ok")


def _plan_with_subplans() -> None:
    """Integration test: plan with subplan config via service."""
    service = _create_service()

    action = service.create_action(
        name="local/parent-action",
        description="Parent action with subplans",
        definition_of_done="Parent task with subplans",
        strategy_actor="local/s",
        execution_actor="local/e",
    )

    plan = service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-001")],
    )

    # Add subplan config to verify Plan model accepts it
    plan_copy = plan.model_copy(
        update={
            "subplan_config": SubplanConfig(
                execution_mode=ExecutionMode.SEQUENTIAL,
                merge_strategy=SubplanMergeStrategy.SEQUENTIAL_APPLY,
            ),
            "subplan_statuses": [
                SubplanStatus(
                    subplan_id="01KH72MDGQMPRW3R5WPZVB8KC3",
                    action_name="local/sub-task",
                    target_resources=["src/main.py"],
                    status=ProcessingState.QUEUED,
                ),
            ],
        }
    )

    assert plan_copy.has_subplans
    assert plan_copy.subplan_config is not None
    assert plan_copy.subplan_config.execution_mode == ExecutionMode.SEQUENTIAL
    assert len(plan_copy.subplan_statuses) == 1

    # The original plan has no parent => is_root_plan
    assert not plan_copy.is_subplan  # no parent_plan_id set

    print("plan-with-subplans-ok")


def _phase_transitions_model() -> None:
    """Integration test: verify can_transition function for all valid/invalid paths."""
    # Valid transitions (four-phase: ACTION -> STRATEGIZE -> EXECUTE -> APPLY)
    assert can_transition(PlanPhase.ACTION, PlanPhase.STRATEGIZE)
    assert can_transition(PlanPhase.STRATEGIZE, PlanPhase.EXECUTE)
    assert can_transition(PlanPhase.EXECUTE, PlanPhase.APPLY)
    assert can_transition(PlanPhase.EXECUTE, PlanPhase.STRATEGIZE)
    assert can_transition(PlanPhase.APPLY, PlanPhase.STRATEGIZE)

    # Invalid transitions
    assert not can_transition(PlanPhase.STRATEGIZE, PlanPhase.APPLY)
    assert not can_transition(PlanPhase.ACTION, PlanPhase.EXECUTE)
    assert not can_transition(PlanPhase.ACTION, PlanPhase.APPLY)
    assert not can_transition(PlanPhase.APPLY, PlanPhase.EXECUTE)

    print("phase-transitions-ok")


def _namespaced_name_parsing() -> None:
    """Integration test: NamespacedName parse and string round-trip."""
    # Simple local name
    n1 = NamespacedName.parse("local/my-action")
    assert n1.namespace == "local"
    assert n1.name == "my-action"
    assert str(n1) == "local/my-action"

    # Server-prefixed name
    n2 = NamespacedName.parse("server:org/my-action")
    assert n2.server == "server"
    assert n2.namespace == "org"
    assert n2.name == "my-action"
    assert str(n2) == "server:org/my-action"

    # Default namespace
    n3 = NamespacedName.parse("standalone-action")
    assert n3.namespace == "local"
    assert n3.name == "standalone-action"

    print("namespaced-name-ok")


def _action_argument_parsing() -> None:
    """Integration test: ActionArgument parse and validation."""
    arg = ActionArgument.parse("target:str:required:The target directory")
    assert arg.name == "target"
    assert arg.arg_type.value == "string"
    assert arg.requirement.value == "required"
    assert arg.description == "The target directory"

    # Test Action argument validation
    action = Action(
        namespaced_name=NamespacedName.parse("local/test-action"),
        description="Test",
        definition_of_done="Done",
        strategy_actor="local/s",
        execution_actor="local/e",
        arguments=[arg],
        created_at=datetime.now(UTC),
        updated_at=datetime.now(UTC),
    )

    # Valid arguments
    errors = action.validate_arguments({"target": "/tmp/dir"})
    assert len(errors) == 0, f"Expected no errors, got {errors}"

    # Missing required argument
    errors = action.validate_arguments({})
    assert len(errors) > 0, "Expected validation error for missing required arg"

    print("action-argument-ok")


def _action_yaml_config_loading() -> None:
    """Integration test: Action.from_config() loads YAML fields
    and as_cli_dict() renders them."""
    # Simulate a full YAML config dict with all supported fields
    config = {
        "name": "org/code-coverage",
        "description": "Increase code coverage to target level",
        "long_description": "Comprehensive code coverage improvement action",
        "definition_of_done": "Coverage meets ${target_coverage}% threshold",
        "strategy_actor": "openai/gpt-4",
        "execution_actor": "anthropic/claude-3",
        "review_actor": "local/reviewer",
        "apply_actor": "local/applier",
        "estimation_actor": "local/estimator",
        "invariant_actor": "local/invariant-checker",
        "automation_profile": "org/high-trust",
        "invariants": [
            "No test deletions allowed",
            "Coverage must not decrease",
            "All new code must have tests",
        ],
        "arguments": [
            {
                "name": "target_coverage",
                "type": "integer",
                "required": True,
                "description": "Target coverage percentage",
                "default": 80,
                "min_value": 0,
                "max_value": 100,
            },
            {
                "name": "test_framework",
                "type": "string",
                "required": False,
                "description": "Test framework to use",
            },
            {
                "name": "include_branches",
                "type": "boolean",
                "required": False,
                "description": "Include branch coverage",
            },
        ],
        "reusable": True,
        "read_only": False,
        "tags": ["coverage", "testing"],
        "created_by": "yaml-test",
    }

    # --- Test from_config() loads all fields correctly ---
    action = Action.from_config(config)

    assert str(action.namespaced_name) == "org/code-coverage", (
        f"Expected 'org/code-coverage', got '{action.namespaced_name}'"
    )
    assert action.description == "Increase code coverage to target level"
    assert action.long_description == "Comprehensive code coverage improvement action"
    assert action.definition_of_done == "Coverage meets ${target_coverage}% threshold"
    assert action.strategy_actor == "openai/gpt-4"
    assert action.execution_actor == "anthropic/claude-3"
    assert action.review_actor == "local/reviewer"
    assert action.apply_actor == "local/applier"
    assert action.estimation_actor == "local/estimator"
    assert action.invariant_actor == "local/invariant-checker"
    assert action.automation_profile == "org/high-trust"
    assert action.reusable is True
    assert action.read_only is False
    assert action.state == ActionState.AVAILABLE
    assert action.created_by == "yaml-test"
    assert action.tags == ["coverage", "testing"]

    # Invariants
    assert len(action.invariants) == 3
    assert action.invariants[0] == "No test deletions allowed"
    assert action.invariants[1] == "Coverage must not decrease"
    assert action.invariants[2] == "All new code must have tests"

    # Arguments
    assert len(action.arguments) == 3
    arg_coverage = action.arguments[0]
    assert arg_coverage.name == "target_coverage"
    assert arg_coverage.arg_type == ArgumentType.INTEGER
    assert arg_coverage.requirement == ArgumentRequirement.REQUIRED
    assert arg_coverage.description == "Target coverage percentage"
    assert arg_coverage.default_value == 80
    assert arg_coverage.min_value == 0
    assert arg_coverage.max_value == 100

    arg_framework = action.arguments[1]
    assert arg_framework.name == "test_framework"
    assert arg_framework.arg_type == ArgumentType.STRING
    assert arg_framework.requirement == ArgumentRequirement.OPTIONAL

    arg_branches = action.arguments[2]
    assert arg_branches.name == "include_branches"
    assert arg_branches.arg_type == ArgumentType.BOOLEAN
    assert arg_branches.requirement == ArgumentRequirement.OPTIONAL

    # --- Test as_cli_dict() renders expected keys ---
    cli = action.as_cli_dict()

    # Core identity
    assert cli["name"] == "org/code-coverage"
    assert cli["description"] == "Increase code coverage to target level"
    assert cli["state"] == "available"

    # Actors
    assert cli["strategy_actor"] == "openai/gpt-4"
    assert cli["execution_actor"] == "anthropic/claude-3"
    assert cli["review_actor"] == "local/reviewer"
    assert cli["apply_actor"] == "local/applier"
    assert cli["estimation_actor"] == "local/estimator"
    assert cli["invariant_actor"] == "local/invariant-checker"

    # Automation profile
    assert "automation_profile" in cli, "automation_profile missing from cli dict"
    assert cli["automation_profile"] == "org/high-trust"

    # Invariants
    assert "invariants" in cli, "invariants missing from cli dict"
    assert len(cli["invariants"]) == 3
    assert cli["invariants"][0] == "No test deletions allowed"
    assert cli["invariants"][2] == "All new code must have tests"

    # Arguments rendered
    assert "arguments" in cli, "arguments missing from cli dict"
    assert len(cli["arguments"]) == 3
    assert cli["arguments"][0]["name"] == "target_coverage"
    assert cli["arguments"][0]["type"] == "integer"
    assert cli["arguments"][0]["required"] is True
    assert cli["arguments"][1]["name"] == "test_framework"
    assert cli["arguments"][1]["required"] is False
    assert cli["arguments"][2]["name"] == "include_branches"
    assert cli["arguments"][2]["type"] == "boolean"

    # Behavior flags
    assert cli["reusable"] is True
    assert cli["read_only"] is False

    # Tags
    assert cli["tags"] == ["coverage", "testing"]

    # Definition of done
    assert cli["definition_of_done"] == "Coverage meets ${target_coverage}% threshold"

    print("action-yaml-config-ok")


def _plan_status_rendering() -> None:
    """Plan status renders action_name + automation profile.

    Verifies that Plan.as_cli_dict() includes the correct action_name and
    automation_profile fields with provenance tags.
    """
    service = _create_service()

    action = service.create_action(
        name="local/status-test",
        description="Status rendering test action",
        definition_of_done="Test plan status rendering",
        strategy_actor="local/s",
        execution_actor="local/e",
        automation_profile="trusted",
    )

    plan = service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[
            ProjectLink(project_name="local/api-service", alias="api"),
        ],
    )

    # Verify action_name is set on the plan
    assert plan.action_name, "Plan must have action_name set"
    assert "status-test" in plan.action_name, (
        f"Expected action_name to contain 'status-test', got '{plan.action_name}'"
    )

    # Verify as_cli_dict renders action_name
    cli_dict = plan.as_cli_dict()
    assert "action" in cli_dict, "CLI dict must include 'action' key"
    assert cli_dict["action"] == plan.action_name

    # Verify phase and state are rendered
    assert cli_dict["phase"] == "strategize"
    assert cli_dict["state"] == "queued"

    # Verify projects are rendered
    assert "projects" in cli_dict, "CLI dict must include 'projects' key"
    assert len(cli_dict["projects"]) == 1
    assert cli_dict["projects"][0]["name"] == "local/api-service"
    assert cli_dict["projects"][0]["alias"] == "api"

    # If automation profile was resolved, verify provenance
    if plan.automation_profile:
        assert "automation_profile" in cli_dict
        assert "automation_profile_source" in cli_dict
        assert cli_dict["automation_profile_source"] in (
            "plan",
            "action",
            "project",
            "global",
        )

    # Verify timestamps are present
    assert "created_at" in cli_dict
    assert "updated_at" in cli_dict

    print("plan-status-rendering-ok")


def main() -> None:
    if len(sys.argv) < 2:
        raise SystemExit("Expected command argument")
    command = sys.argv[1]
    commands: dict[str, object] = {
        "lifecycle-full-cycle": _lifecycle_full_cycle,
        "action-crud": _action_crud,
        "plan-cancellation": _plan_cancellation,
        "plan-failure-recovery": _plan_failure_recovery,
        "invalid-transitions": _invalid_transitions,
        "automation-full-auto": _automation_full_auto,
        "automation-review-before-apply": _automation_review_before_apply,
        "automation-pause-resume": _automation_pause_resume,
        "subplan-model": _subplan_model,
        "plan-with-subplans": _plan_with_subplans,
        "phase-transitions": _phase_transitions_model,
        "namespaced-name": _namespaced_name_parsing,
        "action-argument": _action_argument_parsing,
        "action-yaml-config": _action_yaml_config_loading,
        "plan-status-rendering": _plan_status_rendering,
    }
    if command not in commands:
        raise SystemExit(f"Unknown command: {command}")
    func = commands[command]
    if callable(func):
        func()


if __name__ == "__main__":
    main()
