"""Helper utilities for plan diff and artifacts Robot integration tests.

Covers:
- Diff output in multiple formats (rich, plain, JSON, YAML)
- Artifacts summary rendering
- Empty changeset guard
- Apply summary persistence
- Merge failure handling
"""

from __future__ import annotations

import json
import sys

from cleveragents.application.services.plan_apply_service import PlanApplyService
from cleveragents.application.services.plan_lifecycle_service import (
    PlanLifecycleService,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import PlanError
from cleveragents.domain.models.core.change import (
    ChangeEntry,
    ChangeOperation,
    InMemoryChangeSetStore,
    SpecChangeSet,
)
from cleveragents.domain.models.core.plan import (
    PlanPhase,
    ProcessingState,
)


def _create_services() -> tuple[
    PlanLifecycleService, InMemoryChangeSetStore, PlanApplyService
]:
    """Create service instances for testing."""
    settings = Settings()
    lifecycle = PlanLifecycleService(settings=settings)
    store = InMemoryChangeSetStore()
    apply_svc = PlanApplyService(lifecycle_service=lifecycle, changeset_store=store)
    return lifecycle, store, apply_svc


def _create_plan_with_changeset(
    lifecycle: PlanLifecycleService,
    store: InMemoryChangeSetStore,
) -> str:
    """Create a test action, plan, and changeset. Return plan_id."""
    lifecycle.create_action(
        name="local/robot-diff-test",
        description="Robot diff test",
        definition_of_done="Tests pass",
        strategy_actor="local/planner",
        execution_actor="local/executor",
    )
    plan = lifecycle.use_action(action_name="local/robot-diff-test")
    plan_id = plan.identity.plan_id

    # Advance through strategize and execute
    lifecycle.start_strategize(plan_id)
    lifecycle.complete_strategize(plan_id)
    plan = lifecycle.get_plan(plan_id)
    if plan.phase == PlanPhase.STRATEGIZE:
        lifecycle.execute_plan(plan_id)
    plan = lifecycle.get_plan(plan_id)
    if plan.phase == PlanPhase.EXECUTE:
        lifecycle.start_execute(plan_id)
        lifecycle.complete_execute(plan_id)

    # Create changeset
    cs = SpecChangeSet(
        changeset_id="robot-cs-001",
        plan_id=plan_id,
        entries=[
            ChangeEntry(
                plan_id=plan_id,
                resource_id="res-001",
                tool_name="file-write",
                operation=ChangeOperation.CREATE,
                path="src/robot_new.py",
                after_hash="abc123",
            ),
            ChangeEntry(
                plan_id=plan_id,
                resource_id="res-002",
                tool_name="file-edit",
                operation=ChangeOperation.MODIFY,
                path="src/robot_existing.py",
                before_hash="old111",
                after_hash="new222",
            ),
        ],
    )
    store._store[cs.changeset_id] = cs

    plan = lifecycle.get_plan(plan_id)
    plan.changeset_id = cs.changeset_id
    plan.sandbox_refs = ["sandbox-robot-001"]
    lifecycle._commit_plan(plan)

    return plan_id


def _create_empty_changeset_plan(
    lifecycle: PlanLifecycleService,
    store: InMemoryChangeSetStore,
) -> str:
    """Create a plan with empty changeset. Return plan_id."""
    lifecycle.create_action(
        name="local/robot-empty-test",
        description="Robot empty test",
        definition_of_done="Tests pass",
        strategy_actor="local/planner",
        execution_actor="local/executor",
    )
    plan = lifecycle.use_action(action_name="local/robot-empty-test")
    plan_id = plan.identity.plan_id

    lifecycle.start_strategize(plan_id)
    lifecycle.complete_strategize(plan_id)
    plan = lifecycle.get_plan(plan_id)
    if plan.phase == PlanPhase.STRATEGIZE:
        lifecycle.execute_plan(plan_id)
    plan = lifecycle.get_plan(plan_id)
    if plan.phase == PlanPhase.EXECUTE:
        lifecycle.start_execute(plan_id)
        lifecycle.complete_execute(plan_id)

    cs = SpecChangeSet(changeset_id="robot-cs-empty", plan_id=plan_id, entries=[])
    store._store[cs.changeset_id] = cs
    plan = lifecycle.get_plan(plan_id)
    plan.changeset_id = cs.changeset_id
    lifecycle._commit_plan(plan)

    return plan_id


def _advance_to_apply_processing(lifecycle: PlanLifecycleService, plan_id: str) -> None:
    """Advance plan to apply/processing."""
    plan = lifecycle.get_plan(plan_id)
    if plan.phase == PlanPhase.EXECUTE and plan.state == ProcessingState.COMPLETE:
        lifecycle.apply_plan(plan_id)
    plan = lifecycle.get_plan(plan_id)
    if plan.phase == PlanPhase.APPLY and plan.state == ProcessingState.QUEUED:
        lifecycle.start_apply(plan_id)


def _diff_output() -> None:
    """Test diff output in rich format."""
    lifecycle, store, apply_svc = _create_services()
    plan_id = _create_plan_with_changeset(lifecycle, store)

    output = apply_svc.diff(plan_id, fmt="rich")
    assert "robot-cs-001" in output, f"Changeset ID missing: {output[:200]}"
    assert "src/robot_new.py" in output, f"Path missing: {output[:200]}"
    assert "src/robot_existing.py" in output, f"Path missing: {output[:200]}"
    print("diff-output-ok")


def _artifacts_output() -> None:
    """Test artifacts output.

    ``format_output`` for machine-readable formats writes directly to
    ``sys.stdout`` and returns ``""``.  We redirect stdout so the JSON
    payload is captured for parsing.
    """
    from io import StringIO

    lifecycle, store, apply_svc = _create_services()
    plan_id = _create_plan_with_changeset(lifecycle, store)

    buf = StringIO()
    old_stdout = sys.stdout
    sys.stdout = buf
    try:
        output = apply_svc.artifacts(plan_id, fmt="json")
    finally:
        sys.stdout = old_stdout
    # Combine: return value or captured stdout
    effective = output or buf.getvalue().strip()
    parsed = json.loads(effective)
    assert parsed["changeset_id"] == "robot-cs-001"
    assert "sandbox-robot-001" in parsed["sandbox_refs"]
    assert len(parsed["files_changed"]) == 2
    print("artifacts-output-ok")


def _empty_guard() -> None:
    """Test empty changeset guard blocks apply."""
    lifecycle, store, apply_svc = _create_services()
    plan_id = _create_empty_changeset_plan(lifecycle, store)

    try:
        apply_svc.guard_empty_changeset(plan_id)
        raise AssertionError("Should have raised PlanError")
    except PlanError as e:
        assert "empty" in e.message.lower()
    print("empty-guard-ok")


def _empty_guard_allow() -> None:
    """Test empty changeset guard allows with flag."""
    lifecycle, store, apply_svc = _create_services()
    plan_id = _create_empty_changeset_plan(lifecycle, store)

    result = apply_svc.guard_empty_changeset(plan_id, allow_empty=True)
    assert result is True
    print("empty-guard-allow-ok")


def _apply_summary() -> None:
    """Test apply summary persistence."""
    lifecycle, store, apply_svc = _create_services()
    plan_id = _create_plan_with_changeset(lifecycle, store)
    _advance_to_apply_processing(lifecycle, plan_id)

    apply_svc.persist_apply_summary(plan_id, files_changed=7, validations_run=2)

    plan = lifecycle.get_plan(plan_id)
    assert plan.error_details is not None
    assert plan.error_details["apply_files_changed"] == "7"
    assert plan.error_details["apply_validations_run"] == "2"
    assert "apply_completed_at" in plan.error_details
    print("apply-summary-ok")


def _merge_failure() -> None:
    """Test merge failure handling."""
    lifecycle, store, apply_svc = _create_services()
    plan_id = _create_plan_with_changeset(lifecycle, store)
    _advance_to_apply_processing(lifecycle, plan_id)

    apply_svc.handle_merge_failure(plan_id, conflict_details="Conflict in src/main.py")

    plan = lifecycle.get_plan(plan_id)
    assert plan.processing_state == ProcessingState.ERRORED
    assert "Merge failed" in (plan.error_message or "")
    assert plan.error_details is not None
    assert "merge_conflict" in plan.error_details
    print("merge-failure-ok")


def _diff_json() -> None:
    """Test diff JSON format."""
    lifecycle, store, apply_svc = _create_services()
    plan_id = _create_plan_with_changeset(lifecycle, store)

    output = apply_svc.diff(plan_id, fmt="json")
    parsed = json.loads(output)
    assert "entries" in parsed
    assert "summary" in parsed
    assert len(parsed["entries"]) == 2
    print("diff-json-ok")


def _diff_yaml() -> None:
    """Test diff YAML format."""
    lifecycle, store, apply_svc = _create_services()
    plan_id = _create_plan_with_changeset(lifecycle, store)

    output = apply_svc.diff(plan_id, fmt="yaml")
    assert "changeset_id" in output
    assert "robot-cs-001" in output
    print("diff-yaml-ok")


COMMANDS: dict[str, object] = {
    "diff-output": _diff_output,
    "artifacts-output": _artifacts_output,
    "empty-guard": _empty_guard,
    "empty-guard-allow": _empty_guard_allow,
    "apply-summary": _apply_summary,
    "merge-failure": _merge_failure,
    "diff-json": _diff_json,
    "diff-yaml": _diff_yaml,
}


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <command>", file=sys.stderr)
        print(f"Available: {', '.join(COMMANDS)}", file=sys.stderr)
        sys.exit(1)
    cmd = sys.argv[1]
    func = COMMANDS.get(cmd)
    if func is None:
        print(f"Unknown command: {cmd}", file=sys.stderr)
        sys.exit(1)
    func()  # type: ignore[operator]
