import importlib
import importlib.util
import json
import sys
import types
from contextlib import nullcontext
from unittest.mock import MagicMock


def _load_chat_textual_app():
    if importlib.util.find_spec("textual") is None and "textual" not in sys.modules:
        textual_mod = types.ModuleType("textual")
        app_mod = types.ModuleType("textual.app")
        containers_mod = types.ModuleType("textual.containers")
        widgets_mod = types.ModuleType("textual.widgets")

        class _StubApp:
            def __init__(self, *args, **kwargs):
                del args, kwargs

            def run(self):
                return None

            def set_interval(self, *args, **kwargs):
                del args, kwargs
                return None

            def query_one(self, *args, **kwargs):
                raise NotImplementedError("query_one should be monkeypatched in tests")

            def call_from_thread(self, fn, *args):
                return fn(*args)

            def suspend(self):
                return nullcontext()

        class _StubContainer:
            def __init__(self, *args, **kwargs):
                del args, kwargs

            def __enter__(self):
                return self

            def __exit__(self, exc_type, exc, tb):
                del exc_type, exc, tb
                return False

        class _StubWidget:
            def __init__(self, *args, **kwargs):
                del args, kwargs

        class _StubInput(_StubWidget):
            class Submitted:
                pass

            def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                self.value = ""
                self.password = False
                self.disabled = False
                self.placeholder = str(kwargs.get("placeholder", "") or "")

            def focus(self):
                return None

        class _StubRichLog(_StubWidget):
            def write(self, *_args, **_kwargs):
                return None

            def clear(self):
                return None

            def scroll_end(self, animate=False):
                del animate
                return None

        class _StubStatic(_StubWidget):
            def update(self, *_args, **_kwargs):
                return None

        app_mod.App = _StubApp
        app_mod.ComposeResult = object
        containers_mod.Horizontal = _StubContainer
        containers_mod.Vertical = _StubContainer
        widgets_mod.Footer = _StubWidget
        widgets_mod.Header = _StubWidget
        widgets_mod.Input = _StubInput
        widgets_mod.RichLog = _StubRichLog
        widgets_mod.Static = _StubStatic

        sys.modules["textual"] = textual_mod
        sys.modules["textual.app"] = app_mod
        sys.modules["textual.containers"] = containers_mod
        sys.modules["textual.widgets"] = widgets_mod

    return importlib.import_module("armorer.ui.chat_textual").ArmorerChatApp


ArmorerChatApp = _load_chat_textual_app()


def test_chat_on_mount_uses_config_dir_override(monkeypatch, tmp_path):
    app = ArmorerChatApp()
    config_dir = tmp_path / "alt-armorer"
    captured: dict[str, object] = {}

    class _FakeLog:
        def write(self, *_args, **_kwargs):
            return None

    class _FakeInput:
        def focus(self):
            return None

    def _fake_relay(*args, **kwargs):
        captured["config_dir"] = kwargs.get("config_dir")
        return MagicMock()

    def _fake_query_one(selector, cls=None):
        if selector == "#chatlog":
            return _FakeLog()
        if selector == "#input":
            return _FakeInput()
        raise AssertionError(selector)

    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.ui.chat_textual.ConfigManager", lambda: MagicMock(get_key=lambda _key: None))
    monkeypatch.setattr("armorer.ui.chat_textual.InboxOutboxRelay", _fake_relay)
    monkeypatch.setattr(app, "query_one", _fake_query_one)
    monkeypatch.setattr(app, "set_interval", lambda *args, **kwargs: None)

    app.on_mount()

    assert app.config_dir == config_dir
    assert captured["config_dir"] == config_dir


def test_chat_on_input_submitted_routes_blank_pending_ui_action(monkeypatch):
    app = ArmorerChatApp()
    app.busy = False
    app.pending_ui_action = {"type": "text", "message": "Enter model", "default": "llama3.2"}

    handled: list[str] = []

    class _FakeInput:
        value = "existing"

    monkeypatch.setattr(app, "query_one", lambda selector, cls=None: _FakeInput())
    monkeypatch.setattr(app, "_handle_ui_action_input", lambda text: handled.append(text))

    app.on_input_submitted(type("Evt", (), {"value": ""})())

    assert handled == [""]


def test_chat_text_ui_action_redacts_sensitive_followup_in_chatlog(monkeypatch):
    app = ArmorerChatApp()
    app.pending_ui_action = {"type": "text", "message": "Enter OPENAI_API_KEY"}

    appended: list[tuple[str, str]] = []
    prompts: list[str] = []
    busy_calls: list[tuple[bool, str]] = []

    class _ImmediateThread:
        def __init__(self, target=None, args=(), daemon=None):
            self._target = target
            self._args = args

        def start(self):
            if self._target:
                self._target(*self._args)

    monkeypatch.setattr("armorer.ui.chat_textual.Thread", _ImmediateThread)
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))
    monkeypatch.setattr(app, "_process_prompt", lambda prompt: prompts.append(prompt))

    app._handle_ui_action_input("sk-test-openai")

    assert appended == [("user", "UI input: [redacted]")]
    assert prompts == ["UI input: sk-test-openai"]
    assert busy_calls[0] == (True, "Armorer is thinking...")


def test_chat_set_pending_action_enables_password_mode_for_sensitive_text(monkeypatch):
    app = ArmorerChatApp()

    class _FakeInput:
        def __init__(self):
            self.password = False

    fake_input = _FakeInput()
    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "query_one", lambda selector, cls=None: fake_input)
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._set_pending_action({"type": "text", "message": "Enter OPENAI_API_KEY"})

    assert fake_input.password is True
    assert appended == [("system", "Enter OPENAI_API_KEY\n[dim]Type `/cancel` to abort.[/dim]")]
    assert busy_calls == [(False, "Input required")]


def test_chat_set_pending_action_hides_sensitive_default_value(monkeypatch):
    app = ArmorerChatApp()

    class _FakeInput:
        def __init__(self):
            self.password = False

    fake_input = _FakeInput()
    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "query_one", lambda selector, cls=None: fake_input)
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._set_pending_action({"type": "text", "message": "Enter OPENAI_API_KEY", "default": "sk-existing-secret"})

    assert fake_input.password is True
    assert appended == [
        (
            "system",
            "Enter OPENAI_API_KEY\n[dim]Stored value available; leave blank to keep it.[/dim]\n[dim]Type `/cancel` to abort.[/dim]",
        )
    ]
    assert busy_calls == [(False, "Input required")]


def test_chat_set_pending_action_hides_secret_looking_default_with_generic_message(monkeypatch):
    app = ArmorerChatApp()

    class _FakeInput:
        def __init__(self):
            self.password = False

    fake_input = _FakeInput()
    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "query_one", lambda selector, cls=None: fake_input)
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._set_pending_action({"type": "text", "message": "Enter value", "default": "sk-existing-secret"})

    assert fake_input.password is True
    assert appended == [
        (
            "system",
            "Enter value\n[dim]Stored value available; leave blank to keep it.[/dim]\n[dim]Type `/cancel` to abort.[/dim]",
        )
    ]
    assert busy_calls == [(False, "Input required")]


def test_chat_set_pending_action_hides_gemini_like_default_with_generic_message(monkeypatch):
    app = ArmorerChatApp()

    class _FakeInput:
        def __init__(self):
            self.password = False

    fake_input = _FakeInput()
    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "query_one", lambda selector, cls=None: fake_input)
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    leaked_key = "AIzaSyD0123456789abcdefghijklmnopqrstuv"
    app._set_pending_action({"type": "text", "message": "Enter value", "default": leaked_key})

    assert fake_input.password is True
    assert appended == [
        (
            "system",
            "Enter value\n[dim]Stored value available; leave blank to keep it.[/dim]\n[dim]Type `/cancel` to abort.[/dim]",
        )
    ]
    assert busy_calls == [(False, "Input required")]


def test_chat_set_pending_action_hides_gemini_like_default_with_generic_message(monkeypatch):
    app = ArmorerChatApp()

    class _FakeInput:
        def __init__(self):
            self.password = False

    fake_input = _FakeInput()
    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "query_one", lambda selector, cls=None: fake_input)
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    leaked_key = "AIzaSyD0123456789abcdefghijklmnopqrstuv"
    app._set_pending_action({"type": "text", "message": "Enter value", "default": leaked_key})

    assert fake_input.password is True
    assert appended == [
        (
            "system",
            "Enter value\n[dim]Stored value available; leave blank to keep it.[/dim]\n[dim]Type `/cancel` to abort.[/dim]",
        )
    ]
    assert busy_calls == [(False, "Input required")]


def test_chat_set_pending_action_resets_password_mode_for_non_sensitive_text(monkeypatch):
    app = ArmorerChatApp()

    class _FakeInput:
        def __init__(self):
            self.password = True

    fake_input = _FakeInput()
    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "query_one", lambda selector, cls=None: fake_input)
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._set_pending_action({"type": "text", "message": "Enter model", "default": "llama3.2"})

    assert fake_input.password is False
    assert appended == [("system", "Enter model\n[dim]Default: llama3.2[/dim]\n[dim]Type `/cancel` to abort.[/dim]")]
    assert busy_calls == [(False, "Input required")]


def test_chat_set_pending_action_uses_input_required_when_message_missing(monkeypatch):
    app = ArmorerChatApp()

    class _FakeInput:
        def __init__(self):
            self.password = False

    fake_input = _FakeInput()
    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "query_one", lambda selector, cls=None: fake_input)
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._set_pending_action({"type": "text", "message": None})

    assert appended == [("system", "Input required\n[dim]Type `/cancel` to abort.[/dim]")]
    assert busy_calls == [(False, "Input required")]


def test_chat_set_pending_action_select_uses_input_required_when_message_missing(monkeypatch):
    app = ArmorerChatApp()

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._set_pending_action({"type": "select", "message": None, "options": ["openai", "ollama"]})

    assert appended == [
        (
            "system",
            "Input required\n\n1. openai\n2. ollama\n\nType option number or exact value.\nType `/cancel` to abort.",
        )
    ]
    assert busy_calls == [(False, "Select an option")]


def test_chat_set_pending_action_select_redacts_secret_like_titles(monkeypatch):
    app = ArmorerChatApp()
    leaked_token = "123456:abcDEF_token_sample_value"

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._set_pending_action(
        {
            "type": "select",
            "message": f"Pick option {leaked_token}",
            "options": [leaked_token, "openai"],
        }
    )

    assert len(appended) == 1
    assert appended[0][0] == "system"
    rendered = appended[0][1]
    assert "Pick option [REDACTED_TELEGRAM_TOKEN]" in rendered
    assert "1. [REDACTED_TELEGRAM_TOKEN]" in rendered
    assert leaked_token not in rendered
    assert busy_calls == [(False, "Select an option")]


def test_chat_set_pending_action_confirm_uses_input_required_when_message_missing(monkeypatch):
    app = ArmorerChatApp()

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._set_pending_action({"type": "confirm", "message": None})

    assert appended == [("system", "Input required\n\nType `yes` or `no`.\nType `/cancel` to abort.")]
    assert busy_calls == [(False, "Confirmation required")]


def test_chat_handle_ui_action_input_resets_password_mode_after_sensitive_text(monkeypatch):
    app = ArmorerChatApp()
    app.pending_ui_action = {"type": "text", "message": "Enter OPENAI_API_KEY"}

    class _FakeInput:
        def __init__(self):
            self.password = True

    fake_input = _FakeInput()
    submitted: list[tuple[str, str | None]] = []

    monkeypatch.setattr(app, "query_one", lambda selector, cls=None: fake_input)
    monkeypatch.setattr(app, "_submit_followup", lambda prompt, display_text=None: submitted.append((prompt, display_text)))

    app._handle_ui_action_input("sk-test-openai")

    assert fake_input.password is False
    assert submitted == [("UI input: sk-test-openai", "UI input: [redacted]")]


def test_chat_select_ui_action_rejects_invalid_value_locally(monkeypatch):
    app = ArmorerChatApp()
    action = {"type": "select", "message": "Choose OpenClaw provider", "options": ["openai", "ollama"]}
    app.pending_ui_action = dict(action)

    appended: list[tuple[str, str]] = []
    submitted: list[str] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_submit_followup", lambda prompt, display_text=None: submitted.append(prompt))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._handle_ui_action_input("definitely-not-an-option")

    assert appended == [("system", "Please choose one of the listed options.")]
    assert submitted == []
    assert busy_calls == [(False, "Select an option")]
    assert app.pending_ui_action == action


def test_chat_select_ui_action_redacts_secret_like_followup_in_chatlog(monkeypatch):
    app = ArmorerChatApp()
    leaked_token = "123456:abcDEF_token_sample_value"
    app.pending_ui_action = {"type": "select", "message": "Pick token", "options": [leaked_token, "openai"]}

    appended: list[tuple[str, str]] = []
    prompts: list[str] = []
    busy_calls: list[tuple[bool, str]] = []

    class _ImmediateThread:
        def __init__(self, target=None, args=(), daemon=None):
            self._target = target
            self._args = args

        def start(self):
            if self._target:
                self._target(*self._args)

    monkeypatch.setattr("armorer.ui.chat_textual.Thread", _ImmediateThread)
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))
    monkeypatch.setattr(app, "_process_prompt", lambda prompt: prompts.append(prompt))

    app._handle_ui_action_input(leaked_token)

    assert prompts == [f"UI selection: {leaked_token}"]
    assert ("user", "UI selection: [REDACTED_TELEGRAM_TOKEN]") in appended
    assert not any(leaked_token in text for _, text in appended)
    assert busy_calls[0] == (True, "Armorer is thinking...")


def test_chat_text_ui_action_rejects_blank_value_without_default_locally(monkeypatch):
    app = ArmorerChatApp()
    action = {"type": "text", "message": "Enter model name"}
    app.pending_ui_action = dict(action)

    appended: list[tuple[str, str]] = []
    submitted: list[str] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_submit_followup", lambda prompt, display_text=None: submitted.append(prompt))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))
    monkeypatch.setattr(app, "query_one", lambda selector, cls=None: type("FakeInput", (), {"password": False})())

    app._handle_ui_action_input("")

    assert appended == [("system", "Enter model name\n[dim]Type `/cancel` to abort.[/dim]")]
    assert submitted == []
    assert busy_calls == [(False, "Input required")]
    assert app.pending_ui_action == action


def test_chat_text_ui_action_uses_sensitive_default_without_echoing_it(monkeypatch):
    app = ArmorerChatApp()
    app.pending_ui_action = {"type": "text", "message": "Enter OPENAI_API_KEY", "default": "sk-existing-secret"}

    submitted: list[tuple[str, str | None]] = []
    monkeypatch.setattr(app, "_submit_followup", lambda prompt, display_text=None: submitted.append((prompt, display_text)))

    app._handle_ui_action_input("")

    assert submitted == [("UI input: skip", "UI input: skip")]


def test_chat_text_ui_action_uses_secret_looking_default_without_echoing_it(monkeypatch):
    app = ArmorerChatApp()
    app.pending_ui_action = {"type": "text", "message": "Enter value", "default": "sk-existing-secret"}

    submitted: list[tuple[str, str | None]] = []
    monkeypatch.setattr(app, "_submit_followup", lambda prompt, display_text=None: submitted.append((prompt, display_text)))

    app._handle_ui_action_input("")

    assert submitted == [("UI input: skip", "UI input: skip")]


def test_chat_text_ui_action_uses_gemini_like_default_without_echoing_it(monkeypatch):
    app = ArmorerChatApp()
    app.pending_ui_action = {"type": "text", "message": "Enter value", "default": "AIzaSyD0123456789abcdefghijklmnopqrstuv"}

    submitted: list[tuple[str, str | None]] = []
    monkeypatch.setattr(app, "_submit_followup", lambda prompt, display_text=None: submitted.append((prompt, display_text)))

    app._handle_ui_action_input("")

    assert submitted == [("UI input: skip", "UI input: skip")]


def test_chat_text_ui_action_uses_gemini_like_default_without_echoing_it(monkeypatch):
    app = ArmorerChatApp()
    app.pending_ui_action = {"type": "text", "message": "Enter value", "default": "AIzaSyD0123456789abcdefghijklmnopqrstuv"}

    submitted: list[tuple[str, str | None]] = []
    monkeypatch.setattr(app, "_submit_followup", lambda prompt, display_text=None: submitted.append((prompt, display_text)))

    app._handle_ui_action_input("")

    assert submitted == [("UI input: skip", "UI input: skip")]


def test_chat_pending_ui_action_accepts_explicit_cancel(monkeypatch):
    app = ArmorerChatApp()
    app.pending_ui_action = {"type": "text", "message": "Enter model name"}

    submitted: list[tuple[str, str | None]] = []

    monkeypatch.setattr(app, "_submit_followup", lambda prompt, display_text=None: submitted.append((prompt, display_text)))

    app._handle_ui_action_input("/cancel")

    assert submitted == [("cancel", "/cancel")]
    assert app.pending_ui_action is None


def test_chat_confirm_ui_action_rejects_invalid_value_locally(monkeypatch):
    app = ArmorerChatApp()
    action = {"type": "confirm", "message": "Proceed with OpenClaw setup?"}
    app.pending_ui_action = dict(action)

    appended: list[tuple[str, str]] = []
    submitted: list[str] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_submit_followup", lambda prompt, display_text=None: submitted.append(prompt))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._handle_ui_action_input("maybe")

    assert appended == [("system", "Please reply `yes` or `no`.")]
    assert submitted == []
    assert busy_calls == [(False, "Confirmation required")]
    assert app.pending_ui_action == action


def test_chat_interactive_command_rejects_invalid_value_locally(monkeypatch):
    app = ArmorerChatApp()
    action = {"type": "interactive_command", "command": "armorer gateway telegram"}
    app.pending_ui_action = dict(action)

    appended: list[tuple[str, str]] = []
    submitted: list[str] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_submit_followup", lambda prompt, display_text=None: submitted.append(prompt))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._handle_ui_action_input("later")

    assert appended == [("system", "Please reply `yes` to run or `no` to cancel.")]
    assert submitted == []
    assert busy_calls == [(False, "Interactive command pending")]
    assert app.pending_ui_action == action


def test_chat_process_prompt_surfaces_timeout_message(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-1"
    app.relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "Timed out waiting for Armorer Core response. Retry the request if needed.",
        "raw": None,
        "timed_out": True,
        "last_status": "Installing OpenClaw",
        "cleaned_up": ["inbox", "status"],
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    app.relay.submit_task.assert_called_once_with(
        prompt="install openclaw",
        source="cli",
        metadata={"channel_id": "tty:test"},
    )
    assert appended == [
        ("assistant", "Timed out waiting for Armorer Core response. Retry the request if needed.")
    ]
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_surfaces_pre_submit_cleanup_note(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-1"
    app.relay.last_submit_cleanup = ["inbox:old.json", "status:old.json"]
    app.relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "done",
        "raw": {"id": "task-1", "response": '{"message":"done"}'},
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []
    activity: list[str] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))
    monkeypatch.setattr(app, "_record_activity", lambda text: activity.append(text))

    app._process_prompt("install openclaw")

    app.relay.submit_task.assert_called_once_with(
        prompt="install openclaw",
        source="cli",
        metadata={"channel_id": "tty:test"},
    )
    assert appended[0] == (
        "system",
        "Cleaned stale queue files before retrying: inbox:old.json, status:old.json.",
    )
    assert appended[-1] == ("assistant", "done")
    assert activity == ["Cleaned stale queue files before retrying: inbox:old.json, status:old.json."]
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_clears_busy_when_submit_fails(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.side_effect = RuntimeError("submit failed")

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    app.relay.submit_task.assert_called_once_with(
        prompt="install openclaw",
        source="cli",
        metadata={"channel_id": "tty:test"},
    )
    assert appended == [("system", "Failed to contact Armorer Core: submit failed")]
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_forwards_status_updates_and_ui_action(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-2"

    def _fake_wait_for_response(task_id, timeout, poll_interval, status_callback):
        assert task_id == "task-2"
        status_callback("Starting OpenClaw services")
        return {
            "id": task_id,
            "message": "ok",
            "raw": {
                "id": task_id,
                "response": '{"message":"Need provider","ui_action":{"type":"select","options":["openai","ollama"]}}',
            },
            "last_status": "Starting OpenClaw services",
        }

    app.relay.wait_for_response.side_effect = _fake_wait_for_response

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []
    actions: list[dict] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))
    monkeypatch.setattr(app, "_set_pending_action", lambda action: actions.append(action))

    app._process_prompt("install openclaw")

    app.relay.submit_task.assert_called_once_with(
        prompt="install openclaw",
        source="cli",
        metadata={"channel_id": "tty:test"},
    )
    assert ("assistant", "Need provider") in appended
    assert (True, "Starting OpenClaw services") in busy_calls
    assert busy_calls[-1] == (False, "")
    assert actions == [{"type": "select", "options": ["openai", "ollama"]}]


def test_chat_process_prompt_redacts_secret_like_status_updates(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-2"
    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"

    def _fake_wait_for_response(task_id, timeout, poll_interval, status_callback):
        assert task_id == "task-2"
        status_callback(f"Setting keys {leaked_key} {leaked_token}")
        return {
            "id": task_id,
            "message": "ok",
            "raw": {
                "id": task_id,
                "response": '{"message":"Need provider","ui_action":{"type":"select","options":["openai","ollama"]}}',
            },
            "last_status": "Starting OpenClaw services",
        }

    app.relay.wait_for_response.side_effect = _fake_wait_for_response

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []
    actions: list[dict] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))
    monkeypatch.setattr(app, "_set_pending_action", lambda action: actions.append(action))

    app._process_prompt("install openclaw")

    assert ("assistant", "Need provider") in appended
    redacted_updates = [placeholder for busy, placeholder in busy_calls if busy]
    assert len(redacted_updates) >= 1
    assert any("[REDACTED_OPENAI_KEY]" in placeholder for placeholder in redacted_updates)
    assert any("[REDACTED_TELEGRAM_TOKEN]" in placeholder for placeholder in redacted_updates)
    assert all(leaked_key not in placeholder for placeholder in redacted_updates)
    assert all(leaked_token not in placeholder for placeholder in redacted_updates)
    assert busy_calls[-1] == (False, "")
    assert actions == [{"type": "select", "options": ["openai", "ollama"]}]


def test_chat_process_prompt_redacts_secret_like_assistant_message(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-2"
    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    app.relay.wait_for_response.return_value = {
        "id": "task-2",
        "message": "ok",
        "raw": {
            "id": "task-2",
            "response": json.dumps({"message": f"Need provider {leaked_key} {leaked_token}"}),
        },
        "last_status": "Starting OpenClaw services",
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assistant_messages = [text for role, text in appended if role == "assistant"]
    assert len(assistant_messages) >= 1
    assert any("[REDACTED_OPENAI_KEY]" in text for text in assistant_messages)
    assert any("[REDACTED_TELEGRAM_TOKEN]" in text for text in assistant_messages)
    assert all(leaked_key not in text for text in assistant_messages)
    assert all(leaked_token not in text for text in assistant_messages)
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_surfaces_openclaw_rollback_failure_message(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-2"
    rollback_message = (
        "OpenClaw setup failed: status verification failed\n"
        "Armorer restored the previous OpenClaw runtime state before retry.\n"
        "Retry with the same settings or cancel to stop."
    )
    app.relay.wait_for_response.return_value = {
        "id": "task-2",
        "message": "ok",
        "raw": {"id": "task-2", "response": json.dumps({"message": rollback_message})},
        "last_status": "Verifying OpenClaw status",
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assert ("assistant", rollback_message) in appended
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_surfaces_openclaw_generic_failure_message(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-2"
    failure_message = (
        "OpenClaw setup failed: install exploded\n"
        "No further changes were applied after the failure.\n"
        "Retry with the same settings or cancel to stop."
    )
    app.relay.wait_for_response.return_value = {
        "id": "task-2",
        "message": "ok",
        "raw": {"id": "task-2", "response": json.dumps({"message": failure_message})},
        "last_status": "Installing OpenClaw",
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assert ("assistant", failure_message) in appended
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_surfaces_openclaw_registry_only_rollback_message(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-2"
    rollback_message = (
        "OpenClaw setup failed: registry recovery failed\n"
        "Armorer rolled back the partial OpenClaw setup state before retry.\n"
        "Retry with the same settings or cancel to stop."
    )
    app.relay.wait_for_response.return_value = {
        "id": "task-2",
        "message": "ok",
        "raw": {"id": "task-2", "response": json.dumps({"message": rollback_message})},
        "last_status": "Recovering OpenClaw registry metadata",
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assert ("assistant", rollback_message) in appended
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_surfaces_openclaw_rollback_warning_message(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-2"
    rollback_message = (
        "OpenClaw setup failed: status verification failed\n"
        "Armorer restored the previous OpenClaw runtime state before retry, "
        "but rollback reported warnings: reload services: [REDACTED_OPENAI_KEY] [REDACTED_TELEGRAM_TOKEN]\n"
        "Retry with the same settings or cancel to stop."
    )
    app.relay.wait_for_response.return_value = {
        "id": "task-2",
        "message": "ok",
        "raw": {"id": "task-2", "response": json.dumps({"message": rollback_message})},
        "last_status": "Verifying OpenClaw status",
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assert ("assistant", rollback_message) in appended
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_surfaces_openclaw_fresh_install_start_rollback_message(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-2"
    rollback_message = (
        "OpenClaw setup failed: Starting OpenClaw services failed: startup exploded\n"
        "Armorer removed the partial OpenClaw install before retry.\n"
        "Retry with the same settings or cancel to stop."
    )
    app.relay.wait_for_response.return_value = {
        "id": "task-2",
        "message": "ok",
        "raw": {"id": "task-2", "response": json.dumps({"message": rollback_message})},
        "last_status": "Starting OpenClaw services",
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assert ("assistant", rollback_message) in appended
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_surfaces_openclaw_fresh_install_start_rollback_warning_message(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-2"
    rollback_message = (
        "OpenClaw setup failed: Starting OpenClaw services failed: startup exploded\n"
        "Armorer removed the partial OpenClaw install before retry, "
        "but rollback reported warnings: remove install root openclaw: permission denied\n"
        "Retry with the same settings or cancel to stop."
    )
    app.relay.wait_for_response.return_value = {
        "id": "task-2",
        "message": "ok",
        "raw": {"id": "task-2", "response": json.dumps({"message": rollback_message})},
        "last_status": "Starting OpenClaw services",
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assert ("assistant", rollback_message) in appended
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_accepts_dict_raw_response_payload(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-2"
    app.relay.wait_for_response.return_value = {
        "id": "task-2",
        "message": "ok",
        "raw": {
            "id": "task-2",
            "response": {
                "message": "Need provider",
                "ui_action": {"type": "select", "options": ["openai", "ollama"]},
            },
        },
        "last_status": "Starting OpenClaw services",
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []
    actions: list[dict] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))
    monkeypatch.setattr(app, "_set_pending_action", lambda action: actions.append(action))

    app._process_prompt("install openclaw")

    assert ("assistant", "Need provider") in appended
    assert busy_calls[-1] == (False, "")
    assert actions == [{"type": "select", "options": ["openai", "ollama"]}]


def test_chat_process_prompt_coerces_non_string_message_value(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-3"
    app.relay.wait_for_response.return_value = {
        "id": "task-3",
        "message": "ok",
        "raw": {
            "id": "task-3",
            "response": {"message": 123},
        },
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assert ("assistant", "123") in appended
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_uses_result_message_when_raw_message_missing(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-4"
    app.relay.wait_for_response.return_value = {
        "id": "task-4",
        "message": "Need provider",
        "raw": {
            "id": "task-4",
            "response": {
                "ui_action": {"type": "select", "options": ["openai", "ollama"]},
            },
        },
    }

    appended: list[tuple[str, str]] = []
    actions: list[dict] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_pending_action", lambda action: actions.append(action))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assert ("assistant", "Need provider") in appended
    assert actions == [{"type": "select", "options": ["openai", "ollama"]}]
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_falls_back_to_input_required_when_messages_missing(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-5"
    app.relay.wait_for_response.return_value = {
        "id": "task-5",
        "raw": {
            "id": "task-5",
            "response": {
                "ui_action": {"type": "text"},
            },
        },
    }

    appended: list[tuple[str, str]] = []
    actions: list[dict] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_pending_action", lambda action: actions.append(action))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assert ("assistant", "Input required.") in appended
    assert actions == [{"type": "text"}]
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_uses_result_message_when_raw_placeholder_object(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-6"
    app.relay.wait_for_response.return_value = {
        "id": "task-6",
        "message": "Need provider",
        "raw": {"id": "task-6", "response": "{}"},
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assert ("assistant", "Need provider") in appended
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_uses_result_message_when_raw_placeholder_null(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-7"
    app.relay.wait_for_response.return_value = {
        "id": "task-7",
        "message": "Need provider",
        "raw": {"id": "task-7", "response": "null"},
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assert ("assistant", "Need provider") in appended
    assert busy_calls[-1] == (False, "")


def test_chat_process_prompt_uses_decoded_json_string_without_quotes(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.relay.submit_task.return_value = "task-8"
    app.relay.wait_for_response.return_value = {
        "id": "task-8",
        "message": "ok",
        "raw": {"id": "task-8", "response": '"Need provider"'},
    }

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._process_prompt("install openclaw")

    assert ("assistant", "Need provider") in appended
    assert busy_calls[-1] == (False, "")


def test_chat_interactive_command_cancel_stays_local(monkeypatch):
    app = ArmorerChatApp()
    app.pending_ui_action = {"type": "interactive_command", "command": "armorer gateway telegram"}

    appended: list[tuple[str, str]] = []
    submitted: list[str] = []

    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_submit_followup", lambda prompt: submitted.append(prompt))

    app._handle_ui_action_input("no")

    assert appended == [("system", "Interactive command canceled.")]
    assert submitted == []


def test_chat_interactive_command_slash_cancel_stays_local(monkeypatch):
    app = ArmorerChatApp()
    app.pending_ui_action = {"type": "interactive_command", "command": "armorer gateway telegram"}

    appended: list[tuple[str, str]] = []
    submitted: list[str] = []

    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_submit_followup", lambda prompt: submitted.append(prompt))

    app._handle_ui_action_input("/cancel")

    assert appended == [("system", "Interactive command canceled.")]
    assert submitted == []


def test_chat_interactive_command_empty_command_stays_local(monkeypatch):
    app = ArmorerChatApp()
    app.pending_ui_action = {"type": "interactive_command", "command": ""}

    appended: list[tuple[str, str]] = []
    submitted: list[str] = []

    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_submit_followup", lambda prompt: submitted.append(prompt))

    app._handle_ui_action_input("yes")

    assert appended == [("system", "Interactive command could not be run because no command was provided.")]
    assert submitted == []


def test_chat_set_pending_action_interactive_command_without_command_fails_fast(monkeypatch):
    app = ArmorerChatApp()

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []

    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._set_pending_action({"type": "interactive_command", "command": "", "reason": "missing command"})

    assert app.pending_ui_action is None
    assert appended == [("system", "Interactive command could not be run because no command was provided.")]
    assert busy_calls == [(False, "")]


def test_chat_set_pending_action_interactive_command_redacts_secret_like_command(monkeypatch):
    app = ArmorerChatApp()

    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []
    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"

    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))

    app._set_pending_action(
        {
            "type": "interactive_command",
            "command": f"armorer gateway telegram --api-key {leaked_key} --token {leaked_token}",
            "reason": f"Uses token {leaked_token}",
        }
    )

    assert "[REDACTED_OPENAI_KEY]" in appended[0][1]
    assert "[REDACTED_TELEGRAM_TOKEN]" in appended[0][1]
    assert leaked_key not in appended[0][1]
    assert leaked_token not in appended[0][1]
    assert busy_calls == [(False, "Interactive command pending")]


def test_chat_interactive_command_result_stays_local(monkeypatch):
    app = ArmorerChatApp()

    appended: list[tuple[str, str]] = []
    activity: list[str] = []
    busy_calls: list[tuple[bool, str]] = []
    submitted: list[str] = []

    class _ImmediateThread:
        def __init__(self, target=None, args=(), daemon=None):
            self._target = target
            self._args = args

        def start(self):
            if self._target:
                self._target(*self._args)

    monkeypatch.setattr("armorer.ui.chat_textual.Thread", _ImmediateThread)
    monkeypatch.setattr("armorer.ui.chat_textual.subprocess.run", lambda args: MagicMock(returncode=0))
    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "suspend", lambda: type("Ctx", (), {"__enter__": lambda self: None, "__exit__": lambda self, exc_type, exc, tb: False})())
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_record_activity", lambda text: activity.append(text))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))
    monkeypatch.setattr(app, "_submit_followup", lambda prompt, display_text=None: submitted.append(prompt))

    app._run_interactive_command_and_resume("armorer gateway telegram")

    assert appended == [
        ("system", "Launching interactive command in terminal: armorer gateway telegram"),
        ("system", "Interactive command finished with exit code 0."),
    ]
    assert activity == ["Interactive command finished with exit code 0."]
    assert busy_calls == [
        (True, "Running interactive command..."),
        (False, ""),
    ]
    assert submitted == []


def test_chat_interactive_command_parse_error_stays_local(monkeypatch):
    app = ArmorerChatApp()

    appended: list[tuple[str, str]] = []
    activity: list[str] = []
    busy_calls: list[tuple[bool, str]] = []
    submitted: list[str] = []

    class _ImmediateThread:
        def __init__(self, target=None, args=(), daemon=None):
            self._target = target
            self._args = args

        def start(self):
            if self._target:
                self._target(*self._args)

    monkeypatch.setattr("armorer.ui.chat_textual.Thread", _ImmediateThread)
    monkeypatch.setattr(
        "armorer.ui.chat_textual.subprocess.run",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("subprocess.run should not be called")),
    )
    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "suspend", lambda: type("Ctx", (), {"__enter__": lambda self: None, "__exit__": lambda self, exc_type, exc, tb: False})())
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_record_activity", lambda text: activity.append(text))
    monkeypatch.setattr(app, "_set_busy", lambda busy, placeholder="": busy_calls.append((busy, placeholder)))
    monkeypatch.setattr(app, "_submit_followup", lambda prompt, display_text=None: submitted.append(prompt))

    app._run_interactive_command_and_resume('armorer gateway "telegram')

    assert appended[0] == ("system", "Launching interactive command in terminal: armorer gateway \"telegram")
    assert appended[1][0] == "system"
    assert "Interactive command failed to parse:" in appended[1][1]
    assert activity and "Interactive command failed to parse:" in activity[0]
    assert busy_calls == [
        (True, "Running interactive command..."),
        (False, ""),
    ]
    assert submitted == []


def test_chat_interactive_command_parse_error_redacts_secret_like_detail(monkeypatch):
    app = ArmorerChatApp()

    appended: list[tuple[str, str]] = []
    activity: list[str] = []

    class _ImmediateThread:
        def __init__(self, target=None, args=(), daemon=None):
            self._target = target
            self._args = args

        def start(self):
            if self._target:
                self._target(*self._args)

    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"

    def _boom(_command: str):
        raise ValueError(f"cannot parse provider key {leaked_key}")

    monkeypatch.setattr("armorer.ui.chat_textual.Thread", _ImmediateThread)
    monkeypatch.setattr("armorer.ui.chat_textual.shlex.split", _boom)
    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_record_activity", lambda text: activity.append(text))
    monkeypatch.setattr(app, "_set_busy", lambda *args, **kwargs: None)

    app._run_interactive_command_and_resume("armorer gateway telegram")

    assert "[REDACTED_OPENAI_KEY]" in appended[1][1]
    assert leaked_key not in appended[1][1]
    assert activity and "[REDACTED_OPENAI_KEY]" in activity[0]


def test_chat_interactive_command_launch_message_redacts_secret_like_command(monkeypatch):
    app = ArmorerChatApp()

    appended: list[tuple[str, str]] = []
    leaked_token = "123456:abcDEF_token_sample_value"

    class _ImmediateThread:
        def __init__(self, target=None, args=(), daemon=None):
            self._target = target
            self._args = args

        def start(self):
            if self._target:
                self._target(*self._args)

    monkeypatch.setattr("armorer.ui.chat_textual.Thread", _ImmediateThread)
    monkeypatch.setattr("armorer.ui.chat_textual.subprocess.run", lambda args: MagicMock(returncode=0))
    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "suspend", lambda: type("Ctx", (), {"__enter__": lambda self: None, "__exit__": lambda self, exc_type, exc, tb: False})())
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_record_activity", lambda text: None)
    monkeypatch.setattr(app, "_set_busy", lambda *args, **kwargs: None)

    app._run_interactive_command_and_resume(f"armorer gateway telegram --token {leaked_token}")

    assert "[REDACTED_TELEGRAM_TOKEN]" in appended[0][1]
    assert leaked_token not in appended[0][1]


def test_chat_process_prompt_redacts_secret_like_core_contact_failure(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    leaked_token = "123456:abcDEF_token_sample_value"
    app.relay.submit_task.side_effect = RuntimeError(f"submit failed: {leaked_token}")
    app.cli_metadata = {"channel_id": "tty:test"}

    appended: list[tuple[str, str]] = []
    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_set_busy", lambda *args, **kwargs: None)

    app._process_prompt("install openclaw")

    assert appended == [("system", "Failed to contact Armorer Core: submit failed: [REDACTED_TELEGRAM_TOKEN]")]


def test_chat_textual_runs_managed_openclaw_flow_end_to_end(monkeypatch):
    app = ArmorerChatApp()
    app.relay = MagicMock()
    app.cli_metadata = {"channel_id": "tty:test"}
    app.busy = False

    submitted: list[tuple[str, str, dict | None]] = []
    appended: list[tuple[str, str]] = []
    busy_calls: list[tuple[bool, str]] = []
    activity: list[str] = []

    class _FakeInput:
        def __init__(self):
            self.value = ""
            self.password = False
            self.disabled = False
            self.placeholder = ""

        def focus(self):
            return None

    class _ImmediateThread:
        def __init__(self, target=None, args=(), daemon=None):
            self._target = target
            self._args = args

        def start(self):
            if self._target:
                self._target(*self._args)

    fake_input = _FakeInput()
    task_ids = iter(["task-1", "task-2"])

    def _submit_task(prompt, source, metadata=None):
        submitted.append((prompt, source, metadata))
        if prompt == "UI selection: openai":
            app.relay.last_submit_cleanup = ["inbox:stale.json", "status:stale.json"]
        else:
            app.relay.last_submit_cleanup = []
        return next(task_ids)

    def _wait_for_response(task_id, timeout, poll_interval, status_callback):
        if task_id == "task-1":
            status_callback("Collecting OpenClaw setup details")
            return {
                "id": task_id,
                "message": "ignored",
                "raw": {
                    "id": task_id,
                    "response": '{"message":"Choose OpenClaw provider","ui_action":{"type":"select","message":"Pick provider","options":["openai","ollama"]}}',
                },
            }
        if task_id == "task-2":
            status_callback("Starting OpenClaw services")
            return {
                "id": task_id,
                "message": "ignored",
                "raw": {
                    "id": task_id,
                    "response": '{"message":"OpenClaw is installed and verified\\nOpenClaw status: healthy\\nFinal status check passed via `armorer run openclaw status`."}',
                },
            }
        raise AssertionError(task_id)

    def _set_busy(busy, placeholder=""):
        app.busy = busy
        busy_calls.append((busy, placeholder))

    app.relay.submit_task.side_effect = _submit_task
    app.relay.wait_for_response.side_effect = _wait_for_response
    monkeypatch.setattr("armorer.ui.chat_textual.Thread", _ImmediateThread)
    monkeypatch.setattr(app, "call_from_thread", lambda fn, *args: fn(*args))
    monkeypatch.setattr(app, "query_one", lambda selector, cls=None: fake_input)
    monkeypatch.setattr(app, "_append", lambda role, text: appended.append((role, text)))
    monkeypatch.setattr(app, "_record_activity", lambda text: activity.append(text))
    monkeypatch.setattr(app, "_set_busy", _set_busy)

    app.on_input_submitted(type("Evt", (), {"value": "install openclaw"})())

    assert app.pending_ui_action == {
        "type": "select",
        "message": "Pick provider",
        "options": ["openai", "ollama"],
    }

    app.on_input_submitted(type("Evt", (), {"value": "openai"})())

    assert submitted == [
        ("install openclaw", "cli", {"channel_id": "tty:test"}),
        ("UI selection: openai", "cli", {"channel_id": "tty:test"}),
    ]
    assert ("assistant", "Choose OpenClaw provider") in appended
    assert ("user", "UI selection: openai") in appended
    assert (
        "assistant",
        "OpenClaw is installed and verified\nOpenClaw status: healthy\nFinal status check passed via `armorer run openclaw status`.",
    ) in appended
    assert (
        "system",
        "Cleaned stale queue files before retrying: inbox:stale.json, status:stale.json.",
    ) in appended
    assert activity == [
        "Cleaned stale queue files before retrying: inbox:stale.json, status:stale.json.",
    ]
    assert busy_calls == [
        (True, "Armorer is thinking..."),
        (True, "Collecting OpenClaw setup details"),
        (False, "Select an option"),
        (False, ""),
        (True, "Armorer is thinking..."),
        (True, "Starting OpenClaw services"),
        (False, ""),
    ]
    assert app.pending_ui_action is None
