from __future__ import annotations

from typing import Any
import pytest
from bootstrap.constants import MAX_TERMINAL_OUTPUT_CHARS
from bootstrap.artifacts import ManifestResult, ManifestStatus
from bootstrap.config_loader import UserConfig
from bootstrap.orchestrator import BootstrapConfig, BootstrapOrchestrator
from bootstrap.git_ops import GitConfig
from bootstrap.payload import RepoContext
from bootstrap.payload import BootstrapAlreadyClaimed
from bootstrap.payload import BootstrapPayload
from bootstrap.runner import CodexInvocation, CodexEvent


def test_orchestrator_happy_path(monkeypatch, tmp_path):
    """End-to-end orchestration with fakes: no real network or codex."""

    workspace = tmp_path / "work"
    artifacts = [{"artifact_type": "text", "payload": {"content": "hi"}}]
    captured_env: dict[str, str] = {}
    captured_extra_flags: tuple[str, ...] = ()

    # Prepare a dummy config file
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")

    # Telemetry collectors
    heartbeats: list[dict[str, Any]] = []
    logs: list[dict[str, Any]] = []
    posted_artifacts: list[dict[str, Any]] = []
    completions: list[dict[str, Any]] = []
    errors: list[dict[str, Any]] = []

    monkeypatch.setattr(
        "bootstrap.orchestrator.load_codex_config",
        lambda path: UserConfig(
            raw={},
            working_dir=workspace,
            sandbox_mode=None,
            approval_policy="unless-allow-listed",
            oss_provider=None,
            writable_roots=(workspace,),
            workspace_instructions="use workspace",
            instructions_source="inline",
            warnings=(),
        ),
    )
    monkeypatch.setattr("bootstrap.orchestrator.codex_on_path", lambda: True)
    monkeypatch.setattr(
        "bootstrap.orchestrator.codex_login_status_ok", lambda _path: True
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.fetch_bootstrap_payload",
        lambda server_url, run_id, token: BootstrapPayload(prompt="PROMPT"),
    )

    def fake_build_invocation(codex_executable, prompt, workdir, env, extra_flags=()):
        captured_env.update(env)
        nonlocal captured_extra_flags
        captured_extra_flags = tuple(extra_flags)
        return CodexInvocation(args=["codex", "exec"], env=env, workdir=workdir)

    def fake_run_and_stream(invocation):
        invocation.exit_code = 0
        yield CodexEvent(raw={"run_id": "codex-run-1"})
        yield CodexEvent(raw={"message": "ok"})

    monkeypatch.setattr(
        "bootstrap.orchestrator.build_invocation", fake_build_invocation
    )
    monkeypatch.setattr("bootstrap.orchestrator.run_and_stream", fake_run_and_stream)
    monkeypatch.setattr(
        "bootstrap.orchestrator.read_manifest",
        lambda path: ManifestResult(status=ManifestStatus.VALID, artifacts=artifacts),
    )

    monkeypatch.setattr(
        "bootstrap.orchestrator.post_heartbeat",
        lambda server_url, run_id, token, summary: heartbeats.append(
            {"summary": summary}
        ),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_log",
        lambda server_url, run_id, token, level, message, extra: logs.append(
            {"level": level, "message": message}
        ),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_artifacts",
        lambda server_url, run_id, token, entries: posted_artifacts.extend(entries),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_completion",
        lambda server_url, run_id, token, summary: completions.append(
            {"summary": summary}
        ),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_error",
        lambda server_url, run_id, token, reason, summary=None: errors.append(
            {"reason": reason, "summary": summary}
        ),
    )

    cfg = BootstrapConfig(
        run_id="run-123",
        capability_token="secret",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)
    code = orchestrator.run()

    assert code == 0
    assert heartbeats, "expected at least one heartbeat"
    assert logs, "expected log forwarding"
    assert posted_artifacts == artifacts
    assert completions and not errors
    assert "PATH" in captured_env
    assert captured_env.get("FLYWHEEL_RUN_ID") == "run-123"
    assert captured_env.get("FLYWHEEL_RUN_TOKEN") == "secret"
    assert captured_env.get("FLYWHEEL_SERVER") == "http://server"
    assert "CODEX_HOME" in captured_env
    gitignore_path = workspace / ".gitignore"
    assert gitignore_path.exists()
    assert ".codex_home/auth.json" in gitignore_path.read_text(encoding="utf-8")
    # With sandbox_mode=None, no sandbox flags should be added
    assert len(captured_extra_flags) == 0


def test_orchestrator_bootstrap_conflict_exits_cleanly(monkeypatch, tmp_path) -> None:
    """Bootstrap 409 should exit without posting completion/error."""

    workspace = tmp_path / "work"
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")

    logs: list[dict[str, Any]] = []
    posted_artifacts: list[dict[str, Any]] = []
    completions: list[dict[str, Any]] = []
    errors: list[dict[str, Any]] = []

    monkeypatch.setattr(
        "bootstrap.orchestrator.load_codex_config",
        lambda path: UserConfig(
            raw={},
            working_dir=workspace,
            sandbox_mode=None,
            approval_policy="unless-allow-listed",
            oss_provider=None,
            writable_roots=(workspace,),
            workspace_instructions="use workspace",
            instructions_source="inline",
            warnings=(),
        ),
    )
    monkeypatch.setattr("bootstrap.orchestrator.codex_on_path", lambda: True)
    monkeypatch.setattr(
        "bootstrap.orchestrator.codex_login_status_ok", lambda _path: True
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.fetch_bootstrap_payload",
        lambda server_url, run_id, token: (_ for _ in ()).throw(
            BootstrapAlreadyClaimed("bootstrap already claimed")
        ),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.BootstrapOrchestrator._launch_codex_and_stream",
        lambda _self: pytest.fail("codex should not launch when bootstrap is claimed"),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_log",
        lambda server_url, run_id, token, level, message, extra: logs.append(
            {"level": level, "message": message}
        ),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_artifacts",
        lambda server_url, run_id, token, entries: posted_artifacts.extend(entries),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_completion",
        lambda server_url, run_id, token, summary: completions.append(
            {"summary": summary}
        ),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_error",
        lambda server_url, run_id, token, reason, summary=None: errors.append(
            {"reason": reason, "summary": summary}
        ),
    )

    cfg = BootstrapConfig(
        run_id="run-123",
        capability_token="secret",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)
    code = orchestrator.run()

    assert code == 0
    assert not posted_artifacts
    assert not completions
    assert not errors
    assert any("bootstrap already claimed" in entry["message"] for entry in logs)


def test_orchestrator_bootstrap_payload_error_posts_failure(
    monkeypatch, tmp_path
) -> None:
    """Unexpected bootstrap failures should still be reported as errors."""

    workspace = tmp_path / "work"
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")

    errors: list[dict[str, Any]] = []

    monkeypatch.setattr(
        "bootstrap.orchestrator.load_codex_config",
        lambda path: UserConfig(
            raw={},
            working_dir=workspace,
            sandbox_mode=None,
            approval_policy="unless-allow-listed",
            oss_provider=None,
            writable_roots=(workspace,),
            workspace_instructions="use workspace",
            instructions_source="inline",
            warnings=(),
        ),
    )
    monkeypatch.setattr("bootstrap.orchestrator.codex_on_path", lambda: True)
    monkeypatch.setattr(
        "bootstrap.orchestrator.codex_login_status_ok", lambda _path: True
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.fetch_bootstrap_payload",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("boom")),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_error",
        lambda server_url, run_id, token, reason, summary=None: errors.append(
            {"reason": reason, "summary": summary}
        ),
    )

    cfg = BootstrapConfig(
        run_id="run-123",
        capability_token="secret",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)
    code = orchestrator.run()

    assert code == 1
    assert errors
    assert any("boom" in entry["reason"] for entry in errors)


def test_ensure_workspace_gitignore_entry_is_idempotent(tmp_path) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    workspace = tmp_path / "work"
    workspace.mkdir(parents=True, exist_ok=True)

    gitignore_path = workspace / ".gitignore"
    gitignore_path.write_text(".codex_home/auth.json\n", encoding="utf-8")

    orchestrator = BootstrapOrchestrator(
        BootstrapConfig(
            run_id="run-123",
            capability_token="token",
            config_path=cfg_file,
            server_url="http://server",
            run_root=tmp_path,
        )
    )
    orchestrator.workspace = workspace

    orchestrator._ensure_workspace_gitignore_entry(".codex_home/auth.json")
    orchestrator._ensure_workspace_gitignore_entry(".codex_home/auth.json")

    lines = gitignore_path.read_text(encoding="utf-8").splitlines()
    assert lines.count(".codex_home/auth.json") == 1


def test_resolve_workspace_rejects_outside_writable(monkeypatch, tmp_path):
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")

    orchestrator = BootstrapOrchestrator(
        BootstrapConfig(
            run_id="run-1",
            capability_token="token",
            config_path=cfg_file,
            server_url="http://server",
            run_root=tmp_path,
        )
    )
    orchestrator.user_config = UserConfig(
        raw={},
        working_dir=tmp_path / "work",
        sandbox_mode="workspace-write",
        approval_policy="unless-allow-listed",
        oss_provider=None,
        writable_roots=(tmp_path / "other",),
        workspace_instructions="instr",
        instructions_source="inline",
        warnings=(),
    )

    with pytest.raises(SystemExit):
        orchestrator._resolve_workspace()


def test_resolve_workspace_expands_run_id_placeholder(tmp_path) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")

    orchestrator = BootstrapOrchestrator(
        BootstrapConfig(
            run_id="run-xyz",
            capability_token="token",
            config_path=cfg_file,
            server_url="http://server",
            run_root=tmp_path,
        )
    )

    templated = tmp_path / "runs" / "<run_id>"
    orchestrator.user_config = UserConfig(
        raw={},
        working_dir=templated,
        sandbox_mode="workspace-write",
        approval_policy="unless-allow-listed",
        oss_provider=None,
        writable_roots=(tmp_path,),
        workspace_instructions="instr",
        instructions_source="inline",
        warnings=(),
    )

    orchestrator._resolve_workspace()
    assert orchestrator.workspace == (tmp_path / "runs" / "run-xyz").resolve()


def test_finalize_git_repo_pushes_on_failure(monkeypatch, tmp_path) -> None:
    """Non-zero exit should still commit and push changes."""
    cfg = BootstrapConfig(
        run_id="run-123",
        capability_token="token",
        config_path=tmp_path / "config.toml",
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)
    orchestrator.git_config = GitConfig(
        workspace=tmp_path,
        repo_context=RepoContext(
            repo_url="https://github.com/testuser/repo",
            repo_owner="testuser",
            repo_name="repo",
            branch_name="flywheel/test",
            base_branch="main",
        ),
        github_token="old-token",
        log_fn=lambda *_: None,
    )

    commit_calls = []
    push_calls = []
    update_calls = []

    artifact_output = tmp_path / "artifacts" / "out.txt"
    artifact_output.parent.mkdir(parents=True, exist_ok=True)
    artifact_output.write_text("artifact\n", encoding="utf-8")
    manifest = tmp_path / "flywheel_artifacts.json"
    manifest.write_text(
        '[{"artifact_type":"text","payload":{"path":"artifacts/out.txt"}}]',
        encoding="utf-8",
    )
    orchestrator.workspace = tmp_path

    def _record_commit(
        config: GitConfig, message: str, *, exclude_paths: tuple[str, ...] = ()
    ) -> bool:
        commit_calls.append((message, tuple(sorted(exclude_paths))))
        return True

    def _record_push(config: GitConfig) -> bool:
        push_calls.append(True)
        return True

    def _record_update_auth(config: GitConfig, token: str) -> bool:
        update_calls.append(token)
        return True

    monkeypatch.setattr("bootstrap.orchestrator.commit_changes", _record_commit)
    monkeypatch.setattr("bootstrap.orchestrator.push_changes", _record_push)
    monkeypatch.setattr(
        "bootstrap.orchestrator.fetch_github_token",
        lambda *args, **kwargs: "new-token",
    )
    monkeypatch.setattr("bootstrap.orchestrator.update_git_auth", _record_update_auth)

    orchestrator._finalize_git_repo(exit_code=2)

    assert commit_calls
    assert "[WIP]" in commit_calls[0][0]
    assert "artifacts/out.txt" in commit_calls[0][1]
    assert "flywheel_artifacts.json" in commit_calls[0][1]
    assert update_calls == ["new-token"]
    assert push_calls


def test_artifact_paths_for_git_exclusion_handles_missing_manifest(tmp_path) -> None:
    """Missing manifest should produce no exclusions."""
    cfg = BootstrapConfig(
        run_id="run-123",
        capability_token="token",
        config_path=tmp_path / "config.toml",
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)
    orchestrator.workspace = tmp_path

    assert orchestrator._artifact_paths_for_git_exclusion() == ()


def test_load_artifacts_with_content_inlines_json_and_checkpoint(
    monkeypatch, tmp_path
) -> None:
    """Files below threshold should be embedded into artifact payloads."""
    cfg = BootstrapConfig(
        run_id="run-blob",
        capability_token="token",
        config_path=tmp_path / "config.toml",
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)
    orchestrator.workspace = tmp_path
    orchestrator._artifact_blob_threshold_bytes = 1024

    artifacts_dir = tmp_path / "artifacts"
    artifacts_dir.mkdir(parents=True, exist_ok=True)

    json_file = artifacts_dir / "metrics.json"
    json_file.write_text('{"acc": 0.91, "loss": 0.12}', encoding="utf-8")

    checkpoint_file = artifacts_dir / "model.bin"
    checkpoint_file.write_bytes(b"\x00\x01\x02\x03checkpoint")

    manifest_entries = [
        {"artifact_type": "json", "payload": {"path": "artifacts/metrics.json"}},
        {"artifact_type": "text", "payload": {"path": "artifacts/model.bin"}},
    ]
    monkeypatch.setattr(
        "bootstrap.orchestrator.read_manifest",
        lambda _path: ManifestResult(
            status=ManifestStatus.VALID,
            artifacts=manifest_entries,
        ),
    )

    result, enriched = orchestrator._load_artifacts_with_content(
        tmp_path / "flywheel_artifacts.json"
    )
    assert result.status == ManifestStatus.VALID
    assert len(enriched) == 2

    json_payload = enriched[0]["payload"]
    assert isinstance(json_payload, dict)
    assert json_payload["content"] == {"acc": 0.91, "loss": 0.12}
    assert isinstance(json_payload.get("size_bytes"), int)

    checkpoint_artifact = enriched[1]
    assert checkpoint_artifact["artifact_type"] == "checkpoint"
    checkpoint_payload = checkpoint_artifact["payload"]
    assert isinstance(checkpoint_payload, dict)
    assert isinstance(checkpoint_payload.get("size_bytes"), int)
    assert str(checkpoint_payload.get("data_url", "")).startswith(
        "data:application/octet-stream;base64,"
    )


def test_load_artifacts_with_content_marks_large_file(monkeypatch, tmp_path) -> None:
    """Files above threshold should not be inlined and should carry a size error."""
    cfg = BootstrapConfig(
        run_id="run-blob-large",
        capability_token="token",
        config_path=tmp_path / "config.toml",
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)
    orchestrator.workspace = tmp_path
    orchestrator._artifact_blob_threshold_bytes = 16

    artifacts_dir = tmp_path / "artifacts"
    artifacts_dir.mkdir(parents=True, exist_ok=True)
    large_text = artifacts_dir / "large.txt"
    large_text.write_text("x" * 128, encoding="utf-8")

    monkeypatch.setattr(
        "bootstrap.orchestrator.read_manifest",
        lambda _path: ManifestResult(
            status=ManifestStatus.VALID,
            artifacts=[
                {"artifact_type": "text", "payload": {"path": "artifacts/large.txt"}}
            ],
        ),
    )

    _, enriched = orchestrator._load_artifacts_with_content(
        tmp_path / "flywheel_artifacts.json"
    )
    payload = enriched[0]["payload"]
    assert isinstance(payload, dict)
    assert "rendering_error" in payload
    assert "max 16" in str(payload["rendering_error"])
    assert "content" not in payload


def test_malformed_manifest_triggers_two_retry_attempts(monkeypatch, tmp_path):
    """When manifest is malformed, the orchestrator retries up to 2 times with feedback."""

    workspace = tmp_path / "work"
    good_artifacts = [{"artifact_type": "text", "payload": {"content": "hi"}}]

    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")

    heartbeats: list[dict[str, Any]] = []
    logs: list[dict[str, Any]] = []
    posted_artifacts: list[dict[str, Any]] = []
    completions: list[dict[str, Any]] = []
    errors: list[dict[str, Any]] = []

    monkeypatch.setattr(
        "bootstrap.orchestrator.load_codex_config",
        lambda path: UserConfig(
            raw={},
            working_dir=workspace,
            sandbox_mode=None,
            approval_policy="unless-allow-listed",
            oss_provider=None,
            writable_roots=(workspace,),
            workspace_instructions="use workspace",
            instructions_source="inline",
            warnings=(),
        ),
    )
    monkeypatch.setattr("bootstrap.orchestrator.codex_on_path", lambda: True)
    monkeypatch.setattr(
        "bootstrap.orchestrator.codex_login_status_ok", lambda _path: True
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.fetch_bootstrap_payload",
        lambda server_url, run_id, token: BootstrapPayload(prompt="PROMPT"),
    )

    def fake_build_invocation(codex_executable, prompt, workdir, env, extra_flags=()):
        return CodexInvocation(args=["codex", "exec"], env=env, workdir=workdir)

    def fake_run_and_stream(invocation):
        invocation.exit_code = 0
        yield CodexEvent(raw={"run_id": "codex-run-1"})
        yield CodexEvent(raw={"message": "ok"})

    monkeypatch.setattr(
        "bootstrap.orchestrator.build_invocation", fake_build_invocation
    )
    monkeypatch.setattr("bootstrap.orchestrator.run_and_stream", fake_run_and_stream)

    calls = {"manifest": 0, "retry": 0}

    def fake_read_manifest(path):
        calls["manifest"] += 1
        # First two reads return malformed, third returns good data
        if calls["manifest"] <= 2:
            return ManifestResult(
                status=ManifestStatus.MALFORMED,
                artifacts=[],
                error="artifact manifest wrapped in dict",
            )
        return ManifestResult(status=ManifestStatus.VALID, artifacts=good_artifacts)

    monkeypatch.setattr("bootstrap.orchestrator.read_manifest", fake_read_manifest)

    def fake_attempt_artifact_retry(self, manifest_path, manifest_result):
        calls["retry"] += 1

    monkeypatch.setattr(
        "bootstrap.orchestrator.BootstrapOrchestrator._attempt_artifact_retry",
        fake_attempt_artifact_retry,
    )

    monkeypatch.setattr("bootstrap.orchestrator.HEARTBEAT_INTERVAL_SECONDS", 0.01)
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_heartbeat",
        lambda server_url, run_id, token, summary: heartbeats.append(
            {"summary": summary}
        ),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_log",
        lambda server_url, run_id, token, level, message, extra: logs.append(
            {"level": level, "message": message}
        ),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_artifacts",
        lambda server_url, run_id, token, entries: posted_artifacts.extend(entries),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_completion",
        lambda server_url, run_id, token, summary: completions.append(
            {"summary": summary}
        ),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_error",
        lambda server_url, run_id, token, reason, summary=None: errors.append(
            {"reason": reason, "summary": summary}
        ),
    )

    cfg = BootstrapConfig(
        run_id="run-456",
        capability_token="secret",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)
    code = orchestrator.run()

    assert code == 0
    # Should have attempted retry twice (max retries = 2)
    assert calls["retry"] == 2
    # manifest read: initial + after 1st retry + after 2nd retry = 3
    assert calls["manifest"] == 3
    assert posted_artifacts == good_artifacts
    assert completions and not errors


def test_malformed_manifest_exhausts_retries_still_completes(monkeypatch, tmp_path):
    """When all retry attempts fail to produce artifacts, run still completes (no crash)."""

    workspace = tmp_path / "work"

    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")

    completions: list[dict[str, Any]] = []
    errors: list[dict[str, Any]] = []

    monkeypatch.setattr(
        "bootstrap.orchestrator.load_codex_config",
        lambda path: UserConfig(
            raw={},
            working_dir=workspace,
            sandbox_mode=None,
            approval_policy="unless-allow-listed",
            oss_provider=None,
            writable_roots=(workspace,),
            workspace_instructions="use workspace",
            instructions_source="inline",
            warnings=(),
        ),
    )
    monkeypatch.setattr("bootstrap.orchestrator.codex_on_path", lambda: True)
    monkeypatch.setattr(
        "bootstrap.orchestrator.codex_login_status_ok", lambda _path: True
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.fetch_bootstrap_payload",
        lambda server_url, run_id, token: BootstrapPayload(prompt="PROMPT"),
    )

    def fake_build_invocation(codex_executable, prompt, workdir, env, extra_flags=()):
        return CodexInvocation(args=["codex", "exec"], env=env, workdir=workdir)

    def fake_run_and_stream(invocation):
        invocation.exit_code = 0
        yield CodexEvent(raw={"run_id": "codex-run-1"})

    monkeypatch.setattr(
        "bootstrap.orchestrator.build_invocation", fake_build_invocation
    )
    monkeypatch.setattr("bootstrap.orchestrator.run_and_stream", fake_run_and_stream)

    calls = {"retry": 0}

    # Always return malformed (manifest never gets fixed)
    monkeypatch.setattr(
        "bootstrap.orchestrator.read_manifest",
        lambda path: ManifestResult(
            status=ManifestStatus.MALFORMED,
            artifacts=[],
            error="artifact manifest is empty",
        ),
    )

    def fake_attempt_artifact_retry(self, manifest_path, manifest_result):
        calls["retry"] += 1

    monkeypatch.setattr(
        "bootstrap.orchestrator.BootstrapOrchestrator._attempt_artifact_retry",
        fake_attempt_artifact_retry,
    )

    monkeypatch.setattr("bootstrap.orchestrator.HEARTBEAT_INTERVAL_SECONDS", 0.01)
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_heartbeat",
        lambda server_url, run_id, token, summary: None,
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_log",
        lambda server_url, run_id, token, level, message, extra: None,
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_artifacts",
        lambda server_url, run_id, token, entries: None,
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_completion",
        lambda server_url, run_id, token, summary: completions.append(
            {"summary": summary}
        ),
    )
    monkeypatch.setattr(
        "bootstrap.orchestrator.post_error",
        lambda server_url, run_id, token, reason, summary=None: errors.append(
            {"reason": reason, "summary": summary}
        ),
    )

    cfg = BootstrapConfig(
        run_id="run-789",
        capability_token="secret",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)
    code = orchestrator.run()

    assert code == 0
    # Exhausted all 2 retry attempts
    assert calls["retry"] == 2
    # Still completes (exit code was 0)
    assert completions and not errors


def test_handle_control_payload_acks_take_over(monkeypatch, tmp_path) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-1",
        capability_token="token-1",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)

    calls: list[dict[str, str]] = []
    logs: list[str] = []

    def fake_ack(server_url, run_id, token, command_id, status, detail):
        calls.append(
            {
                "server_url": server_url,
                "run_id": run_id,
                "token": token,
                "command_id": command_id,
                "status": status,
                "detail": detail,
            }
        )

    monkeypatch.setattr("bootstrap.orchestrator.ack_run_control", fake_ack)
    monkeypatch.setattr(
        orchestrator,
        "_log",
        lambda message, level="info", extra=None: logs.append(message),
    )

    orchestrator._handle_control_payload(
        {"command_id": "cmd-1", "command": "take_over"}
    )

    assert calls == [
        {
            "server_url": "http://server",
            "run_id": "run-1",
            "token": "token-1",
            "command_id": "cmd-1",
            "status": "applied",
            "detail": "take_over acknowledged; interactive resume mode active",
        }
    ]
    assert logs
    assert orchestrator._get_interaction_mode() == "interactive"


def test_take_over_terminates_active_invocation(monkeypatch, tmp_path) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-terminate",
        capability_token="token-terminate",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)

    terminated: list[bool] = []
    invocation = CodexInvocation(
        args=["codex", "exec"],
        env={},
        workdir=tmp_path,
        terminate=lambda: terminated.append(True),
    )
    with orchestrator._active_invocation_lock:
        orchestrator._active_invocation = invocation

    calls: list[dict[str, str]] = []

    def fake_ack(server_url, run_id, token, command_id, status, detail):
        calls.append(
            {
                "server_url": server_url,
                "run_id": run_id,
                "token": token,
                "command_id": command_id,
                "status": status,
                "detail": detail,
            }
        )

    monkeypatch.setattr("bootstrap.orchestrator.ack_run_control", fake_ack)
    monkeypatch.setattr(
        orchestrator,
        "_log",
        lambda message, level="info", extra=None: None,
    )

    orchestrator._handle_control_payload(
        {"command_id": "cmd-term", "command": "take_over"}
    )

    assert terminated == [True]
    assert calls[0]["status"] == "applied"
    assert "autonomous execution interrupted" in calls[0]["detail"]


def test_handle_control_payload_acks_resume_autonomous(monkeypatch, tmp_path) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-resume",
        capability_token="token-resume",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)
    orchestrator._set_interaction_mode("interactive")

    calls: list[dict[str, str]] = []

    def fake_ack(server_url, run_id, token, command_id, status, detail):
        calls.append(
            {
                "server_url": server_url,
                "run_id": run_id,
                "token": token,
                "command_id": command_id,
                "status": status,
                "detail": detail,
            }
        )

    monkeypatch.setattr("bootstrap.orchestrator.ack_run_control", fake_ack)
    monkeypatch.setattr(
        orchestrator,
        "_log",
        lambda message, level="info", extra=None: None,
    )

    orchestrator._handle_control_payload(
        {"command_id": "cmd-resume", "command": "resume_autonomous"}
    )

    assert orchestrator._get_interaction_mode() == "autonomous"
    assert calls == [
        {
            "server_url": "http://server",
            "run_id": "run-resume",
            "token": "token-resume",
            "command_id": "cmd-resume",
            "status": "applied",
            "detail": "resume_autonomous acknowledged; autonomous execution will resume",
        }
    ]


def test_handle_control_payload_acks_unsupported_as_ignored(
    monkeypatch, tmp_path
) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-2",
        capability_token="token-2",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)

    calls: list[dict[str, str]] = []

    def fake_ack(server_url, run_id, token, command_id, status, detail):
        calls.append(
            {
                "server_url": server_url,
                "run_id": run_id,
                "token": token,
                "command_id": command_id,
                "status": status,
                "detail": detail,
            }
        )

    monkeypatch.setattr("bootstrap.orchestrator.ack_run_control", fake_ack)
    monkeypatch.setattr(
        orchestrator,
        "_log",
        lambda message, level="info", extra=None: None,
    )

    orchestrator._handle_control_payload(
        {"command_id": "cmd-2", "command": "some_new_command"}
    )

    assert calls == [
        {
            "server_url": "http://server",
            "run_id": "run-2",
            "token": "token-2",
            "command_id": "cmd-2",
            "status": "ignored",
            "detail": "unsupported command 'some_new_command'",
        }
    ]


def test_handle_control_payload_acks_send_input_as_ignored(
    monkeypatch, tmp_path
) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-3",
        capability_token="token-3",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)

    calls: list[dict[str, str]] = []

    def fake_ack(server_url, run_id, token, command_id, status, detail):
        calls.append(
            {
                "server_url": server_url,
                "run_id": run_id,
                "token": token,
                "command_id": command_id,
                "status": status,
                "detail": detail,
            }
        )

    monkeypatch.setattr("bootstrap.orchestrator.ack_run_control", fake_ack)
    monkeypatch.setattr(
        orchestrator,
        "_log",
        lambda message, level="info", extra=None: None,
    )

    orchestrator._handle_control_payload(
        {"command_id": "cmd-3", "command": "send_input", "payload": {"text": "ping"}}
    )

    assert calls == [
        {
            "server_url": "http://server",
            "run_id": "run-3",
            "token": "token-3",
            "command_id": "cmd-3",
            "status": "ignored",
            "detail": "send_input ignored: not in interactive mode",
        }
    ]


def test_handle_control_payload_acks_send_input_as_applied(
    monkeypatch, tmp_path
) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-4",
        capability_token="token-4",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)

    calls: list[dict[str, str]] = []

    def fake_ack(server_url, run_id, token, command_id, status, detail):
        calls.append(
            {
                "server_url": server_url,
                "run_id": run_id,
                "token": token,
                "command_id": command_id,
                "status": status,
                "detail": detail,
            }
        )

    orchestrator._set_interaction_mode("interactive")
    monkeypatch.setattr("bootstrap.orchestrator.ack_run_control", fake_ack)
    monkeypatch.setattr(
        orchestrator,
        "_run_resume_prompt",
        lambda _text: (True, "resume prompt applied"),
    )
    monkeypatch.setattr(
        orchestrator,
        "_log",
        lambda message, level="info", extra=None: None,
    )

    orchestrator._handle_control_payload(
        {"command_id": "cmd-4", "command": "send_input", "payload": {"text": "hello"}}
    )

    assert calls == [
        {
            "server_url": "http://server",
            "run_id": "run-4",
            "token": "token-4",
            "command_id": "cmd-4",
            "status": "applied",
            "detail": "send_input resume: resume prompt applied",
        }
    ]


def test_handle_control_payload_acks_send_input_as_failed(
    monkeypatch, tmp_path
) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-failed",
        capability_token="token-failed",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)
    orchestrator._set_interaction_mode("interactive")

    calls: list[dict[str, str]] = []

    def fake_ack(server_url, run_id, token, command_id, status, detail):
        calls.append(
            {
                "server_url": server_url,
                "run_id": run_id,
                "token": token,
                "command_id": command_id,
                "status": status,
                "detail": detail,
            }
        )

    monkeypatch.setattr("bootstrap.orchestrator.ack_run_control", fake_ack)
    monkeypatch.setattr(
        orchestrator,
        "_run_resume_prompt",
        lambda _text: (False, "resume prompt failed with exit code 1"),
    )
    monkeypatch.setattr(
        orchestrator,
        "_log",
        lambda message, level="info", extra=None: None,
    )

    orchestrator._handle_control_payload(
        {
            "command_id": "cmd-failed",
            "command": "send_input",
            "payload": {"text": "hello"},
        }
    )

    assert calls == [
        {
            "server_url": "http://server",
            "run_id": "run-failed",
            "token": "token-failed",
            "command_id": "cmd-failed",
            "status": "failed",
            "detail": "send_input resume: resume prompt failed with exit code 1",
        }
    ]


def test_handle_event_marks_terminal_output_as_command_output(
    monkeypatch, tmp_path
) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-events",
        capability_token="token-events",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)

    calls: list[dict[str, Any]] = []

    def fake_post_log(server_url, run_id, token, level, message, extra):
        calls.append(
            {
                "server_url": server_url,
                "run_id": run_id,
                "token": token,
                "level": level,
                "message": message,
                "extra": extra,
            }
        )

    monkeypatch.setattr("bootstrap.orchestrator.post_log", fake_post_log)

    orchestrator._handle_event(CodexEvent(raw={"terminal_output": "ls -la"}))

    assert calls == [
        {
            "server_url": "http://server",
            "run_id": "run-events",
            "token": "token-events",
            "level": "info",
            "message": "ls -la",
            "extra": {
                "event": {"terminal_output": "ls -la"},
                "run_event_kind": "command_output",
                "run_event_source": "codex",
            },
        }
    ]


def test_handle_event_maps_command_execution_aggregated_output(
    monkeypatch, tmp_path
) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-cmd-output",
        capability_token="token-cmd-output",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)

    calls: list[dict[str, Any]] = []

    def fake_post_log(server_url, run_id, token, level, message, extra):
        calls.append({"message": message, "extra": extra})

    monkeypatch.setattr("bootstrap.orchestrator.post_log", fake_post_log)

    orchestrator._handle_event(
        CodexEvent(
            raw={
                "type": "item.completed",
                "item": {
                    "type": "command_execution",
                    "command": "/bin/zsh -lc pwd",
                    "aggregated_output": "/tmp/workspace\n",
                    "exit_code": 0,
                },
            }
        )
    )

    assert calls
    assert calls[0]["message"] == "/tmp/workspace"
    assert calls[0]["extra"]["run_event_kind"] == "command_output"


def test_handle_event_strips_ansi_from_command_output(monkeypatch, tmp_path) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-ansi",
        capability_token="token-ansi",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)

    calls: list[dict[str, Any]] = []

    def fake_post_log(server_url, run_id, token, level, message, extra):
        calls.append({"message": message, "extra": extra})

    monkeypatch.setattr("bootstrap.orchestrator.post_log", fake_post_log)

    orchestrator._handle_event(
        CodexEvent(raw={"terminal_output": "\u001b[31merror\u001b[0m line"})
    )

    assert calls
    assert calls[0]["message"] == "error line"
    assert calls[0]["extra"]["run_event_kind"] == "command_output"


def test_handle_event_truncates_long_command_output(monkeypatch, tmp_path) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-long",
        capability_token="token-long",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)

    calls: list[dict[str, Any]] = []

    def fake_post_log(server_url, run_id, token, level, message, extra):
        calls.append({"message": message, "extra": extra})

    monkeypatch.setattr("bootstrap.orchestrator.post_log", fake_post_log)

    long_output = "x" * (MAX_TERMINAL_OUTPUT_CHARS + 128)
    orchestrator._handle_event(CodexEvent(raw={"terminal_output": long_output}))

    assert calls
    message = calls[0]["message"]
    assert isinstance(message, str)
    assert message.endswith(" [truncated]")
    assert len(message) == MAX_TERMINAL_OUTPUT_CHARS


def test_handle_event_marks_codex_launch_as_command_started(
    monkeypatch, tmp_path
) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-started",
        capability_token="token-started",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)

    calls: list[dict[str, Any]] = []

    def fake_post_log(server_url, run_id, token, level, message, extra):
        calls.append({"level": level, "message": message, "extra": extra})

    monkeypatch.setattr("bootstrap.orchestrator.post_log", fake_post_log)

    orchestrator._handle_event(CodexEvent(raw={"bootstrap_debug": "codex_launch"}))

    assert calls
    assert calls[0]["message"] == "codex exec launched"
    assert calls[0]["extra"]["run_event_kind"] == "command_started"
    assert calls[0]["extra"]["run_event_source"] == "codex"


def test_handle_event_marks_codex_exit_as_command_finished(
    monkeypatch, tmp_path
) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-finished",
        capability_token="token-finished",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)

    calls: list[dict[str, Any]] = []

    def fake_post_log(server_url, run_id, token, level, message, extra):
        calls.append({"level": level, "message": message, "extra": extra})

    monkeypatch.setattr("bootstrap.orchestrator.post_log", fake_post_log)

    orchestrator._handle_event(
        CodexEvent(raw={"bootstrap_debug": "codex_exit", "exit_code": 0})
    )

    assert calls
    assert calls[0]["message"] == "codex exec exited with code 0"
    assert calls[0]["extra"]["run_event_kind"] == "command_finished"
    assert calls[0]["extra"]["run_event_source"] == "codex"


def test_handle_event_captures_thread_id(tmp_path, monkeypatch) -> None:
    cfg_file = tmp_path / "config.toml"
    cfg_file.write_text("", encoding="utf-8")
    cfg = BootstrapConfig(
        run_id="run-thread",
        capability_token="token-thread",
        config_path=cfg_file,
        server_url="http://server",
        run_root=tmp_path,
    )
    orchestrator = BootstrapOrchestrator(cfg)

    monkeypatch.setattr(
        "bootstrap.orchestrator.post_log",
        lambda server_url, run_id, token, level, message, extra: None,
    )

    orchestrator._handle_event(CodexEvent(raw={"thread_id": "thread-123"}))
    assert orchestrator.codex_thread_id == "thread-123"
