from __future__ import annotations

import argparse
import asyncio
import json
import os
import shutil
import socket
import struct
import sys
import threading
from pathlib import Path
from types import SimpleNamespace
from typing import Any, cast

import pytest

from strix.config.settings import DEFAULT_MAX_TURNS
from strix.interface.tui import runtime as go_tui
from strix.interface.tui import sidecar
from strix.interface.tui.runtime import GoTuiRuntime


def args() -> argparse.Namespace:
    return argparse.Namespace(
        needs_setup=True,
        targets_info=[],
        instruction=None,
        scan_mode="deep",
        max_budget_usd=None,
        max_turns=DEFAULT_MAX_TURNS,
        scope_mode="auto",
        diff_base=None,
        local_sources=[],
        diff_scope={"active": False},
        user_explicit_instruction=None,
        run_name="test-run",
    )


def test_binary_command_prefers_packaged_sidecar(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Any,
) -> None:
    sidecar = tmp_path / "strix-tui"
    sidecar.write_text("binary")
    monkeypatch.setattr(go_tui, "tui_source_dir", lambda: tmp_path / "tui-src")
    monkeypatch.setattr(go_tui, "get_strix_resource_path", lambda *_parts: sidecar)
    monkeypatch.setattr(
        shutil,
        "which",
        lambda _name: pytest.fail("PATH lookup should not run"),
    )

    assert GoTuiRuntime.binary_command() == [str(sidecar)]


def test_binary_command_prefers_current_source_over_packaged_sidecar(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Any,
) -> None:
    source = tmp_path / "tui-src"
    source.mkdir()
    (source / "go.mod").write_text("module test\n")
    sidecar = tmp_path / "strix-tui"
    sidecar.write_text("stale")
    monkeypatch.setattr(go_tui, "tui_source_dir", lambda: tmp_path / "tui-src")
    monkeypatch.setattr(go_tui, "get_strix_resource_path", lambda *_parts: sidecar)
    monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/go" if name == "go" else None)

    assert GoTuiRuntime.binary_command() == ["go", "run", "./cmd/strix-tui"]


def test_binary_command_reports_missing_sidecar(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Any,
) -> None:
    monkeypatch.setattr(go_tui, "get_strix_resource_path", lambda *_parts: tmp_path / "missing")
    monkeypatch.setattr(shutil, "which", lambda _name: None)
    monkeypatch.setattr(go_tui, "tui_source_dir", lambda: tmp_path / "tui-src")

    with pytest.raises(RuntimeError, match="Bubble Tea TUI binary not found"):
        GoTuiRuntime.binary_command()


def test_binary_command_ignores_unconstrained_path_sidecar(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Any,
) -> None:
    monkeypatch.setattr(go_tui, "get_strix_resource_path", lambda *_parts: tmp_path / "missing")
    monkeypatch.setattr(go_tui, "tui_source_dir", lambda: tmp_path / "tui-src")
    monkeypatch.setattr(shutil, "which", lambda _name: "/untrusted/path/strix-tui")

    with pytest.raises(RuntimeError, match="Bubble Tea TUI binary not found"):
        GoTuiRuntime.binary_command()


def test_child_environment_excludes_credentials(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("OPENAI_API_KEY", "openai-secret")
    monkeypatch.setenv("AWS_ACCESS_KEY_ID", "aws-id")
    monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "aws-secret")
    monkeypatch.setenv("AWS_SESSION_TOKEN", "aws-token")
    monkeypatch.setenv("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/aws-token")
    monkeypatch.setenv("VERTEXAI_CREDENTIALS", '{"private_key":"secret"}')
    monkeypatch.setenv("STRIX_TUI_TOKEN", "stale-transport-token")
    monkeypatch.setenv("TERM", "xterm-256color")

    env = sidecar.child_environment()

    assert env["TERM"] == "xterm-256color"
    assert "OPENAI_API_KEY" not in env
    assert "AWS_ACCESS_KEY_ID" not in env
    assert "AWS_SECRET_ACCESS_KEY" not in env
    assert "AWS_SESSION_TOKEN" not in env
    assert "AWS_WEB_IDENTITY_TOKEN_FILE" not in env
    assert "VERTEXAI_CREDENTIALS" not in env
    assert "STRIX_TUI_TOKEN" not in env


def test_accept_authenticated_connection() -> None:
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listener.bind(("127.0.0.1", 0))
    listener.listen(1)
    address = listener.getsockname()

    def connect() -> None:
        with socket.create_connection(address) as connection:
            connection.sendall(b"one-use-token")

    thread = threading.Thread(target=connect)
    thread.start()
    connection = sidecar._accept_authenticated_connection(listener, "one-use-token")
    connection.close()
    listener.close()
    thread.join()


def test_rejects_invalid_connection_token() -> None:
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listener.bind(("127.0.0.1", 0))
    listener.listen(1)
    address = listener.getsockname()

    def connect() -> None:
        with socket.create_connection(address) as connection:
            connection.sendall(b"invalidd-token")

    thread = threading.Thread(target=connect)
    thread.start()
    with pytest.raises(PermissionError, match="authentication failed"):
        sidecar._accept_authenticated_connection(listener, "expected-token")
    listener.close()
    thread.join()


@pytest.mark.asyncio
async def test_windows_transport_launches_without_inherited_fd() -> None:
    child = """
import os
import socket

host, port = os.environ["STRIX_TUI_ADDR"].rsplit(":", 1)
with socket.create_connection((host, int(port))) as connection:
    connection.sendall(os.environ["STRIX_TUI_TOKEN"].encode("ascii"))
"""
    env = os.environ.copy()
    env.pop("STRIX_TUI_FD", None)

    process, connection = await sidecar._launch_windows_tui_process(
        [sys.executable, "-c", child], env, None
    )
    connection.close()

    assert await sidecar.wait_process(process) == 0


async def _receive_exactly(connection: socket.socket, size: int) -> bytes:
    result = b""
    while len(result) < size:
        chunk = await asyncio.get_running_loop().sock_recv(connection, size - len(result))
        if not chunk:
            raise EOFError
        result += chunk
    return result


async def _receive_message(connection: socket.socket) -> dict[str, Any]:
    size = struct.unpack(">I", await _receive_exactly(connection, 4))[0]
    message: dict[str, Any] = json.loads(await _receive_exactly(connection, size))
    return message


async def _send_message(connection: socket.socket, message: dict[str, Any]) -> None:
    raw = json.dumps(message).encode()
    await asyncio.get_running_loop().sock_sendall(connection, struct.pack(">I", len(raw)) + raw)


@pytest.mark.asyncio
async def test_runtime_does_not_initialize_or_scan_before_ready(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime_args = args()
    runtime_args.needs_setup = False
    runtime = GoTuiRuntime(runtime_args)
    backend, child = socket.socketpair()
    child.setblocking(False)  # noqa: FBT003
    calls: list[str] = []
    scan_started = asyncio.Event()

    async def launch(
        _command: list[str], _env: dict[str, str], _cwd: str | None
    ) -> tuple[SimpleNamespace, socket.socket]:
        return SimpleNamespace(returncode=None), backend

    async def wait_process(_process: object) -> int:
        await scan_started.wait()
        return 0

    def init_state() -> None:
        calls.append("state")

    def start_scan() -> None:
        calls.append("scan")
        scan_started.set()

    async def preflight(_model: str) -> None:
        calls.append("preflight")

    monkeypatch.setattr(runtime, "binary_command", lambda: ["test-sidecar"])
    monkeypatch.setattr(go_tui, "launch_tui_process", launch)
    monkeypatch.setattr(go_tui, "wait_process", wait_process)
    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
    monkeypatch.setattr(go_tui, "persist_current", lambda: None)
    monkeypatch.setattr(go_tui, "prepare_run", lambda _args: None)
    monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: None)
    monkeypatch.setattr(runtime, "init_run_state", init_state)
    monkeypatch.setattr(runtime, "start_scan", start_scan)

    run_task = asyncio.create_task(runtime.run())
    try:
        hello = await _receive_message(child)
        assert hello["type"] == "hello"
        assert calls == []
        await _send_message(
            child,
            {
                "version": 3,
                "type": "ready",
                "payload": {
                    "capabilities": [
                        "state-revisions",
                        "collection-deltas",
                        "structured-command-errors",
                        "agents-collection",
                    ]
                },
            },
        )
        await asyncio.wait_for(run_task, timeout=2)
        assert calls == ["preflight", "state", "scan"]
    finally:
        child.close()
        if not run_task.done():
            run_task.cancel()


@pytest.mark.asyncio
async def test_pre_activation_failure_propagates_to_dispatcher(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    class FailedRuntime:
        async def run(self) -> None:
            raise go_tui.GoTuiPreActivationError("protocol mismatch")

    monkeypatch.setattr(go_tui, "GoTuiRuntime", lambda _args: FailedRuntime())

    with pytest.raises(go_tui.GoTuiPreActivationError, match="protocol mismatch"):
        await go_tui.run_go_tui(args())


@pytest.mark.asyncio
async def test_post_activation_failure_is_surfaced(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    class ActivatedRuntime:
        async def run(self) -> None:
            raise RuntimeError("sidecar failed after ready")

    monkeypatch.setattr(go_tui, "GoTuiRuntime", lambda _args: ActivatedRuntime())

    with pytest.raises(RuntimeError, match="after ready"):
        await go_tui.run_go_tui(args())


@pytest.mark.asyncio
async def test_setup_preflights_model_before_starting(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime_args = args()
    runtime_args.instruction = "CLI instruction"
    runtime = GoTuiRuntime(runtime_args)
    assert runtime.controller.instruction == "CLI instruction"
    runtime.controller.targets = ["https://example.com", "/workspace/mounted"]
    runtime.controller.scan_mode = "quick"
    runtime.controller.instruction = ""
    runtime.controller.max_budget_usd = 8.5
    runtime.controller.max_turns = 321
    runtime.controller.scope_mode = "diff"
    runtime.controller.diff_base = "origin/main"
    calls: list[str] = []

    async def preflight(model: str) -> None:
        assert model == "openrouter/test-model"
        calls.append("preflight")

    monkeypatch.setattr(
        go_tui,
        "load_settings",
        lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
    )
    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)

    def build(candidate: argparse.Namespace, **_: object) -> None:
        calls.append("targets")
        assert candidate.target == ["https://example.com", "/workspace/mounted"]
        candidate.targets_info = [
            {
                "type": "web",
                "details": {"target_url": "https://example.com"},
                "original": "https://example.com",
            },
            {
                "type": "local_code",
                "details": {"target_path": "/workspace/mounted"},
                "original": "/workspace/mounted",
            },
        ]

    def prepare(candidate: argparse.Namespace) -> None:
        calls.append("prepare")
        assert candidate.max_budget_usd == 8.5
        assert candidate.max_turns == 321
        assert candidate.scope_mode == "diff"
        assert candidate.diff_base == "origin/main"

    monkeypatch.setattr(go_tui, "build_targets_info", build)
    monkeypatch.setattr(go_tui, "prepare_run", prepare)
    monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: calls.append("telemetry"))
    monkeypatch.setattr(runtime, "init_run_state", lambda: calls.append("state"))
    monkeypatch.setattr(runtime, "start_scan", lambda: calls.append("scan"))

    await runtime.start_from_setup()

    assert calls == ["preflight", "targets", "prepare", "telemetry", "state", "scan"]
    assert runtime.args.scan_mode == "quick"
    assert runtime.args.instruction == ""
    assert runtime.args.max_budget_usd == 8.5
    assert runtime.args.max_turns == 321
    assert runtime.args.scope_mode == "diff"
    assert runtime.args.diff_base == "origin/main"


@pytest.mark.asyncio
async def test_optimistic_setup_skips_model_preflight(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime = GoTuiRuntime(args())
    runtime.controller.targets = [str(Path.cwd())]
    calls: list[str] = []

    async def preflight(_model: str) -> None:
        calls.append("preflight")

    monkeypatch.setattr(
        go_tui,
        "load_settings",
        lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
    )
    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
    monkeypatch.setattr(go_tui, "build_targets_info", lambda _args, **_kw: calls.append("targets"))
    monkeypatch.setattr(go_tui, "prepare_run", lambda _args: calls.append("prepare"))
    monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: calls.append("telemetry"))
    monkeypatch.setattr(runtime, "init_run_state", lambda: calls.append("state"))
    monkeypatch.setattr(runtime, "start_scan", lambda: calls.append("scan"))

    await runtime.start_from_setup(verify=False)

    # No preflight: the scan launches straight through and any model error
    # surfaces once the agent runs.
    assert "preflight" not in calls
    assert calls == ["targets", "prepare", "telemetry", "state", "scan"]


@pytest.mark.asyncio
async def test_confirmed_target_less_launch_mounts_workspace_without_targets(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """The working directory reaches the run as a workspace, never as a target."""
    runtime = GoTuiRuntime(args())
    runtime.controller.workspace_mount = str(Path.home())
    prepared: list[argparse.Namespace] = []

    async def preflight(_model: str) -> None:
        return None

    monkeypatch.setattr(
        go_tui,
        "load_settings",
        lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
    )
    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
    monkeypatch.setattr(
        go_tui,
        "build_targets_info",
        lambda _args, **_kw: pytest.fail("a target-less launch must not build targets"),
    )
    monkeypatch.setattr(go_tui, "prepare_run", prepared.append)
    monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: None)
    monkeypatch.setattr(runtime, "init_run_state", lambda: None)
    monkeypatch.setattr(runtime, "start_scan", lambda: None)

    await runtime.start_from_setup(verify=False)

    assert prepared[0].workspace_mount == str(Path.home())
    assert prepared[0].targets_info == []


@pytest.mark.asyncio
async def test_setup_preserves_prepared_cli_targets(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime_args = args()
    runtime_args.target = ["https://example.com"]
    runtime_args.target_list = []
    runtime_args.targets_info = [
        {
            "type": "web",
            "details": {"url": "https://example.com"},
            "original": "https://example.com",
        }
    ]
    runtime = GoTuiRuntime(runtime_args)
    calls: list[str] = []

    async def preflight(_model: str) -> None:
        calls.append("preflight")

    monkeypatch.setattr(
        go_tui,
        "load_settings",
        lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
    )
    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
    monkeypatch.setattr(
        go_tui,
        "build_targets_info",
        lambda _args, **_kw: pytest.fail("prepared targets should not be rebuilt"),
    )
    monkeypatch.setattr(go_tui, "prepare_run", lambda _args: calls.append("prepare"))
    monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: calls.append("telemetry"))
    monkeypatch.setattr(runtime, "init_run_state", lambda: calls.append("state"))
    monkeypatch.setattr(runtime, "start_scan", lambda: calls.append("scan"))

    await runtime.start_from_setup()

    assert runtime.controller.targets == ["https://example.com"]
    assert runtime.args.targets_info[0]["type"] == "web"
    assert calls == ["preflight", "prepare", "telemetry", "state", "scan"]


@pytest.mark.asyncio
async def test_setup_target_change_preserves_local_targets(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime_args = args()
    runtime_args.target = []
    runtime_args.target_list = ["targets.txt"]
    runtime_args.targets_info = [
        {
            "type": "local_code",
            "details": {"target_path": "/workspace/source"},
            "original": "/workspace/source",
        }
    ]
    runtime = GoTuiRuntime(runtime_args)
    runtime.controller.targets.append("https://example.com")

    async def preflight(_model: str) -> None:
        return None

    def build(target_args: argparse.Namespace, **_: object) -> None:
        assert target_args.target == ["/workspace/source", "https://example.com"]
        target_args.targets_info = [
            {
                "type": "web",
                "details": {"url": "https://example.com"},
                "original": "https://example.com",
            },
            {
                "type": "local_code",
                "details": {"target_path": "/workspace/source"},
                "original": "/workspace/source",
            },
        ]

    monkeypatch.setattr(
        go_tui,
        "load_settings",
        lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
    )
    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
    monkeypatch.setattr(go_tui, "build_targets_info", build)
    monkeypatch.setattr(go_tui, "prepare_run", lambda _args: None)
    monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: None)
    monkeypatch.setattr(runtime, "init_run_state", lambda: None)
    monkeypatch.setattr(runtime, "start_scan", lambda: None)

    await runtime.start_from_setup()

    assert runtime.args.target_list == []
    assert runtime.args.targets_info[0]["type"] == "web"
    assert runtime.args.targets_info[1]["type"] == "local_code"


@pytest.mark.asyncio
async def test_setup_same_basename_uses_combined_workspace_names_on_retry(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    existing_repo = "https://example.com/first/app.git"
    added_repo = "https://example.com/second/app.git"
    runtime_args = args()
    runtime_args.target = []
    runtime_args.target_list = ["targets.txt"]
    runtime_args.targets_info = [
        {
            "type": "repository",
            "details": {
                "target_repo": existing_repo,
                "workspace_subdir": "app",
                "cloned_repo_path": "/clones/app",
            },
            "original": existing_repo,
        }
    ]
    runtime = GoTuiRuntime(runtime_args)
    runtime.controller.targets.append(added_repo)
    prepare_attempts = 0
    started: list[str] = []

    async def preflight(_model: str) -> None:
        return None

    def build(target_args: argparse.Namespace, **_: object) -> None:
        assert target_args.target == [existing_repo, added_repo]
        target_args.targets_info = [
            {
                "type": "repository",
                "details": {
                    "target_repo": existing_repo,
                    "workspace_subdir": "app",
                },
                "original": existing_repo,
            },
            {
                "type": "repository",
                "details": {
                    "target_repo": added_repo,
                    "workspace_subdir": "app-2",
                },
                "original": added_repo,
            },
        ]

    def prepare(candidate: argparse.Namespace) -> None:
        nonlocal prepare_attempts
        prepare_attempts += 1
        assert [target["details"]["workspace_subdir"] for target in candidate.targets_info] == [
            "app",
            "app-2",
        ]
        if prepare_attempts == 1:
            candidate.targets_info[0]["details"]["target_repo"] = "/mutated"
            candidate.targets_info[1]["details"]["workspace_subdir"] = "mutated"
            raise ValueError("retry setup")

    monkeypatch.setattr(
        go_tui,
        "load_settings",
        lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
    )
    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
    monkeypatch.setattr(go_tui, "build_targets_info", build)
    monkeypatch.setattr(go_tui, "prepare_run", prepare)
    monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: None)
    monkeypatch.setattr(runtime, "init_run_state", lambda: started.append("state"))
    monkeypatch.setattr(runtime, "start_scan", lambda: started.append("scan"))

    with pytest.raises(ValueError, match="retry setup"):
        await runtime.start_from_setup()

    assert runtime.args.targets_info[0]["details"] == {
        "target_repo": existing_repo,
        "workspace_subdir": "app",
        "cloned_repo_path": "/clones/app",
    }
    assert started == []

    await runtime.start_from_setup()

    assert prepare_attempts == 2
    assert runtime.args.target_list == []
    assert [target["details"]["workspace_subdir"] for target in runtime.args.targets_info] == [
        "app",
        "app-2",
    ]
    assert runtime.args.targets_info[0]["details"]["target_repo"] == existing_repo
    assert started == ["state", "scan"]


@pytest.mark.asyncio
async def test_setup_target_rebuild_restores_all_target_fields_on_failure(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime_args = args()
    runtime_args.target = None
    runtime_args.target_list = ["targets.txt"]
    runtime_args.targets_info = [
        {
            "type": "local_code",
            "details": {"target_path": "/workspace/source"},
            "original": "/workspace/source",
        }
    ]
    original_targets_info = json.loads(json.dumps(runtime_args.targets_info))
    runtime = GoTuiRuntime(runtime_args)
    runtime.controller.targets.append("https://example.com")

    async def preflight(_model: str) -> None:
        return None

    def fail_rebuild(target_args: argparse.Namespace, **_: object) -> None:
        target_args.target = ["mutated"]
        target_args.target_list = ["mutated.txt"]
        target_args.targets_info = [{"original": "partial"}]
        raise ValueError("bad target")

    monkeypatch.setattr(
        go_tui,
        "load_settings",
        lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
    )
    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
    monkeypatch.setattr(go_tui, "build_targets_info", fail_rebuild)

    with pytest.raises(ValueError, match="bad target"):
        await runtime.start_from_setup()

    assert runtime.args.target is None
    assert runtime.args.target_list == ["targets.txt"]
    assert runtime.args.targets_info == original_targets_info


@pytest.mark.asyncio
async def test_setup_rebuild_canonicalizes_relative_local_target(
    tmp_path: Any,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    source = tmp_path / "source"
    source.mkdir()
    monkeypatch.chdir(tmp_path)
    runtime_args = args()
    runtime_args.target = []
    runtime_args.target_list = []
    runtime = GoTuiRuntime(runtime_args)
    runtime.controller.targets = ["source"]
    prepared = False

    async def preflight(_model: str) -> None:
        return None

    def prepare(candidate: argparse.Namespace) -> None:
        nonlocal prepared
        prepared = True
        assert len(candidate.targets_info) == 1
        assert candidate.targets_info[0]["details"]["target_path"] == str(source.resolve())

    monkeypatch.setattr(
        go_tui,
        "load_settings",
        lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
    )
    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
    monkeypatch.setattr(go_tui, "prepare_run", prepare)
    monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: None)
    monkeypatch.setattr(runtime, "init_run_state", lambda: None)
    monkeypatch.setattr(runtime, "start_scan", lambda: None)

    await runtime.start_from_setup()

    assert prepared is True
    assert runtime.args.targets_info[0]["original"] == str(source.resolve())


@pytest.mark.asyncio
@pytest.mark.asyncio
async def test_setup_prepare_system_exit_is_recoverable_and_transactional(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime_args = args()
    runtime_args.instruction = "CLI instruction"
    runtime_args.scan_mode = "deep"
    runtime_args.target = ["https://example.com"]
    runtime_args.target_list = []
    runtime_args.targets_info = [
        {
            "type": "web",
            "details": {"url": "https://example.com"},
            "original": "https://example.com",
        }
    ]
    original_args = json.loads(json.dumps(vars(runtime_args)))
    runtime = GoTuiRuntime(runtime_args)
    runtime.controller.scan_mode = "quick"
    runtime.controller.instruction = ""
    telemetry_started = False

    async def preflight(_model: str) -> None:
        return None

    def fail_prepare(candidate: argparse.Namespace) -> None:
        assert candidate is not runtime.args
        candidate.run_name = "mutated-run"
        candidate.targets_info[0]["details"]["url"] = "https://mutated.example"
        raise ValueError("invalid diff scope")

    def telemetry(_candidate: argparse.Namespace) -> None:
        nonlocal telemetry_started
        telemetry_started = True

    monkeypatch.setattr(
        go_tui,
        "load_settings",
        lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
    )
    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
    monkeypatch.setattr(go_tui, "prepare_run", fail_prepare)
    monkeypatch.setattr(go_tui, "telemetry_start", telemetry)

    with pytest.raises(ValueError, match="invalid diff scope"):
        await runtime.start_from_setup()

    assert vars(runtime.args) == original_args
    assert telemetry_started is False
    assert runtime.scan_task is None


@pytest.mark.asyncio
async def test_scan_passes_max_turns_and_budget(monkeypatch: pytest.MonkeyPatch) -> None:
    runtime_args = args()
    runtime_args.max_turns = 37
    runtime_args.max_budget_usd = 4.25
    runtime = GoTuiRuntime(runtime_args)
    runtime.scan_config = {"run_name": "test-run"}
    captured: dict[str, Any] = {}

    async def run_scan(**kwargs: Any) -> None:
        captured.update(kwargs)
        coordinator = kwargs["coordinator"]
        await coordinator.register("root", "Root", parent_id=None)
        await coordinator.set_status("root", "stopped")

    monkeypatch.setattr(
        go_tui,
        "load_settings",
        lambda: SimpleNamespace(runtime=SimpleNamespace(image="test-image")),
    )
    monkeypatch.setattr(go_tui, "run_strix_scan", run_scan)

    await runtime._run_scan()

    assert captured["max_turns"] == 37
    assert captured["max_budget_usd"] == 4.25
    assert runtime.controller.scan_state == "stopped"


@pytest.mark.asyncio
async def test_setup_preflight_failure_does_not_start_scan(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime = GoTuiRuntime(args())
    runtime.controller.targets = ["https://example.com"]
    started = False

    async def preflight(_model: str) -> None:
        raise ValueError("401 Unauthorized")

    def mark_started(*_args: Any) -> None:
        nonlocal started
        started = True

    monkeypatch.setattr(
        go_tui,
        "load_settings",
        lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
    )
    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
    monkeypatch.setattr(go_tui, "build_targets_info", mark_started)
    monkeypatch.setattr(runtime, "init_run_state", mark_started)
    monkeypatch.setattr(runtime, "start_scan", mark_started)

    with pytest.raises(RuntimeError, match="Model connection failed: 401 Unauthorized"):
        await runtime.start_from_setup()

    assert started is False
    assert runtime.scan_task is None


@pytest.mark.asyncio
async def test_agent_state_sync_uses_latest_graph_snapshot_shape() -> None:
    runtime = GoTuiRuntime(args())
    await runtime.coordinator.register("root", "Strix", parent_id=None)
    await runtime.coordinator.register("child", "Recon", parent_id="root")
    await runtime.coordinator.set_status("child", "failed", error="provider rejected request")

    await runtime._sync_agent_state()

    assert runtime.live_view.agents["root"]["name"] == "Strix"
    child = runtime.live_view.agents["child"]
    assert child["name"] == "Recon"
    assert child["parent_id"] == "root"
    assert child["status"] == "failed"
    assert child["error_message"] == "provider rejected request"


@pytest.mark.asyncio
async def test_agent_state_sync_projects_completed_report() -> None:
    runtime = GoTuiRuntime(args())
    runtime.report_state = cast("Any", SimpleNamespace(run_record={"status": "completed"}))
    await runtime.coordinator.register("root", "Strix", parent_id=None)
    await runtime.coordinator.set_status("root", "completed")

    await runtime._sync_agent_state()

    assert runtime.controller.scan_state == "completed"


@pytest.mark.asyncio
async def test_agent_state_sync_does_not_mask_root_failure_with_completed_report() -> None:
    runtime = GoTuiRuntime(args())
    runtime.report_state = cast("Any", SimpleNamespace(run_record={"status": "completed"}))
    await runtime.coordinator.register("root", "Strix", parent_id=None)
    await runtime.coordinator.set_status("root", "failed", error="finalization failed")

    await runtime._sync_agent_state()

    assert runtime.controller.scan_state == "failed"
    assert runtime.controller.error == "finalization failed"


def _direct_launch_args() -> argparse.Namespace:
    launch_args = args()
    launch_args.needs_setup = False
    return launch_args


@pytest.mark.asyncio
async def test_prepare_and_start_reports_ordinary_connection_failures(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime = GoTuiRuntime(_direct_launch_args())
    started: list[str] = []

    async def preflight(_model: str) -> None:
        raise TimeoutError("connection timed out")

    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
    monkeypatch.setattr(runtime, "start_scan", lambda: started.append("scan"))

    await runtime.prepare_and_start()

    assert started == []
    assert runtime.controller.setup_mode is False
    assert runtime.controller.scan_state == "failed"
    assert "connection timed out" in (runtime.controller.error or "")


@pytest.mark.asyncio
async def test_prepare_and_start_runs_the_scan_after_preparation(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime = GoTuiRuntime(_direct_launch_args())
    order: list[str] = []

    async def preflight(_model: str) -> None:
        order.append("preflight")

    monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
    monkeypatch.setattr(go_tui, "persist_current", lambda: order.append("persist"))
    monkeypatch.setattr(go_tui, "prepare_run", lambda _args: order.append("prepare"))
    monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: order.append("telemetry"))
    monkeypatch.setattr(runtime, "init_run_state", lambda: order.append("state"))
    monkeypatch.setattr(runtime, "start_scan", lambda: order.append("scan"))

    await runtime.prepare_and_start()

    assert order == ["preflight", "persist", "prepare", "telemetry", "state", "scan"]
    assert runtime.controller.scan_state == "running"
