import os
import json
import sys
from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest.mock import MagicMock

os.environ.setdefault("ARMORER_SANDBOX", "0")

from armorer.main import (
    _check_self_dockerize,
    _parse_chat_response_result,
    _prompt_for_classic_chat_ui_action,
    _run_classic_chat_interactive_command,
    _run_classic_chat_task,
    _wait_for_chat_result,
    chat,
)

CLI_METADATA = {"channel_id": "tty:test"}


def test_parse_chat_response_result_extracts_ui_action():
    message, requires_confirmation, ui_action = _parse_chat_response_result(
        {
            "raw": {
                "response": '{"message":"Choose provider","ui_action":{"type":"select","options":["openai","ollama"]}}'
            }
        }
    )

    assert message == "Choose provider"
    assert requires_confirmation is False
    assert ui_action == {"type": "select", "options": ["openai", "ollama"]}


def test_parse_chat_response_result_accepts_dict_response_payload():
    message, requires_confirmation, ui_action = _parse_chat_response_result(
        {
            "raw": {
                "response": {
                    "message": "Choose provider",
                    "ui_action": {"type": "select", "options": ["openai", "ollama"]},
                }
            }
        }
    )

    assert message == "Choose provider"
    assert requires_confirmation is False
    assert ui_action == {"type": "select", "options": ["openai", "ollama"]}


def test_parse_chat_response_result_coerces_non_string_message_value():
    message, requires_confirmation, ui_action = _parse_chat_response_result(
        {
            "raw": {
                "response": {
                    "message": 123,
                    "requires_confirmation": True,
                }
            }
        }
    )

    assert message == "123"
    assert requires_confirmation is True
    assert ui_action is None


def test_parse_chat_response_result_uses_result_message_when_raw_message_missing():
    message, requires_confirmation, ui_action = _parse_chat_response_result(
        {
            "message": "Choose provider",
            "raw": {
                "response": {
                    "ui_action": {"type": "select", "options": ["openai", "ollama"]},
                }
            },
        }
    )

    assert message == "Choose provider"
    assert requires_confirmation is False
    assert ui_action == {"type": "select", "options": ["openai", "ollama"]}


def test_parse_chat_response_result_falls_back_to_input_required_when_messages_missing():
    message, requires_confirmation, ui_action = _parse_chat_response_result(
        {
            "raw": {
                "response": {
                    "ui_action": {"type": "text"},
                }
            },
        }
    )

    assert message == "Input required."
    assert requires_confirmation is False
    assert ui_action == {"type": "text"}


def test_parse_chat_response_result_uses_result_message_when_raw_placeholder_object():
    message, requires_confirmation, ui_action = _parse_chat_response_result(
        {
            "message": "Need provider",
            "raw": {"response": "{}"},
        }
    )

    assert message == "Need provider"
    assert requires_confirmation is False
    assert ui_action is None


def test_parse_chat_response_result_uses_result_message_when_raw_placeholder_null():
    message, requires_confirmation, ui_action = _parse_chat_response_result(
        {
            "message": "Need provider",
            "raw": {"response": "null"},
        }
    )

    assert message == "Need provider"
    assert requires_confirmation is False
    assert ui_action is None


def test_parse_chat_response_result_uses_decoded_json_string_without_quotes():
    message, requires_confirmation, ui_action = _parse_chat_response_result(
        {
            "message": "ok",
            "raw": {"response": '"Need provider"'},
        }
    )

    assert message == "Need provider"
    assert requires_confirmation is False
    assert ui_action is None


def test_run_classic_chat_task_handles_ui_action_followup(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.side_effect = [
        {
            "id": "task-1",
            "message": "ignored",
            "raw": {
                "id": "task-1",
                "response": '{"message":"Choose provider","ui_action":{"type":"select","message":"Pick provider","options":["openai","ollama"]}}',
            },
        },
        {
            "id": "task-2",
            "message": "ignored",
            "raw": {
                "id": "task-2",
                "response": '{"message":"OpenClaw setup complete"}',
            },
        },
    ]
    relay.submit_task.return_value = "task-2"

    monkeypatch.setattr(
        "armorer.main.questionary.select",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: "openai"),
    )

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    first_wait_kwargs = relay.wait_for_response.call_args_list[0].kwargs
    assert callable(first_wait_kwargs["status_callback"])
    relay.submit_task.assert_called_once_with(prompt="UI selection: openai", source="cli", metadata=CLI_METADATA)


def test_run_classic_chat_task_surfaces_followup_submit_failure_locally(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {
            "id": "task-1",
            "response": '{"message":"Choose provider","ui_action":{"type":"select","message":"Pick provider","options":["openai","ollama"]}}',
        },
    }
    relay.submit_task.side_effect = RuntimeError("submit failed")

    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.select",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: "openai"),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    assert any("Failed to contact Armorer Core: submit failed" in line for line in printed)


def test_run_classic_chat_task_submits_cancel_when_ui_action_is_dismissed(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.side_effect = [
        {
            "id": "task-1",
            "message": "ignored",
            "raw": {
                "id": "task-1",
                "response": '{"message":"Choose provider","ui_action":{"type":"select","message":"Pick provider","options":["openai","ollama"]}}',
            },
        },
        {
            "id": "task-2",
            "message": "ignored",
            "raw": {
                "id": "task-2",
                "response": '{"message":"Cancelled OpenClaw setup."}',
            },
        },
    ]
    relay.submit_task.return_value = "task-2"

    monkeypatch.setattr(
        "armorer.main.questionary.select",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: None),
    )

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    relay.submit_task.assert_called_once_with(prompt="cancel", source="cli", metadata=CLI_METADATA)


def test_prompt_for_classic_chat_ui_action_select_exposes_visible_cancel_choice(monkeypatch):
    captured: dict[str, object] = {}

    def _fake_select(message, choices):
        captured["message"] = message
        captured["choices"] = choices
        return SimpleNamespace(ask=lambda: "__cancel__")

    monkeypatch.setattr("armorer.main.questionary.select", _fake_select)

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "select", "message": "Pick provider", "options": ["openai", "ollama"]}
    )

    assert followup is None
    assert captured["message"] == "Pick provider"
    choice_titles = [
        choice if isinstance(choice, str) else getattr(choice, "title", choice)
        for choice in captured["choices"]
    ]
    assert choice_titles == ["openai", "ollama", "Cancel setup"]


def test_prompt_for_classic_chat_ui_action_select_redacts_secret_like_option_titles(monkeypatch):
    captured: dict[str, object] = {}
    leaked_token = "123456:abcDEF_token_sample_value"

    def _fake_select(message, choices):
        captured["message"] = message
        captured["choices"] = choices
        first_choice = choices[0]
        return SimpleNamespace(ask=lambda: getattr(first_choice, "value", first_choice))

    monkeypatch.setattr("armorer.main.questionary.select", _fake_select)

    followup = _prompt_for_classic_chat_ui_action(
        {
            "type": "select",
            "message": f"Pick option {leaked_token}",
            "options": [leaked_token, "openai"],
        }
    )

    assert followup == f"UI selection: {leaked_token}"
    assert captured["message"] == "Pick option [REDACTED_TELEGRAM_TOKEN]"
    choice_titles = [
        choice if isinstance(choice, str) else getattr(choice, "title", choice)
        for choice in captured["choices"]
    ]
    assert "[REDACTED_TELEGRAM_TOKEN]" in choice_titles[0]
    assert leaked_token not in "".join(str(x) for x in choice_titles)


def test_prompt_for_classic_chat_ui_action_text_uses_default(monkeypatch):
    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: kwargs.get("default", "")),
    )

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "text", "message": "Enter model", "default": "llama3.2"}
    )

    assert followup == "UI input: llama3.2"


def test_prompt_for_classic_chat_ui_action_text_reprompts_blank_without_default(monkeypatch):
    answers = iter(["", "llama3.2"])
    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: next(answers)),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "text", "message": "Enter model"}
    )

    assert followup == "UI input: llama3.2"
    assert printed == ["[yellow]Input required.[/yellow]"]


def test_prompt_for_classic_chat_ui_action_text_honors_explicit_cancel(monkeypatch):
    prompts: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda message, **kwargs: prompts.append(message) or SimpleNamespace(ask=lambda: "/cancel"),
    )

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "text", "message": "Enter model"}
    )

    assert followup is None
    assert prompts == ["Enter model\nType /cancel to abort."]


def test_prompt_for_classic_chat_ui_action_text_falls_back_to_input_required_message(monkeypatch):
    prompts: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda message, **kwargs: prompts.append(message) or SimpleNamespace(ask=lambda: "llama3.2"),
    )

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "text", "message": None}
    )

    assert followup == "UI input: llama3.2"
    assert prompts == ["Input required\nType /cancel to abort."]


def test_prompt_for_classic_chat_ui_action_select_falls_back_to_input_required_message(monkeypatch):
    prompts: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.select",
        lambda message, **kwargs: prompts.append(message) or SimpleNamespace(ask=lambda: "openai"),
    )

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "select", "message": None, "options": ["openai", "ollama"]}
    )

    assert followup == "UI selection: openai"
    assert prompts == ["Input required"]


def test_prompt_for_classic_chat_ui_action_confirm_falls_back_to_input_required_message(monkeypatch):
    prompts: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.confirm",
        lambda message, **kwargs: prompts.append(message) or SimpleNamespace(ask=lambda: True),
    )

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "confirm", "message": None}
    )

    assert followup == "UI confirmation: yes"
    assert prompts == ["Input required"]


def test_prompt_for_classic_chat_ui_action_sensitive_text_uses_password_prompt(monkeypatch):
    text_called = {"value": False}

    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda *args, **kwargs: text_called.__setitem__("value", True),
    )
    monkeypatch.setattr(
        "armorer.main.questionary.password",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: "sk-test-openai"),
    )

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "text", "message": "Enter OPENAI_API_KEY"}
    )

    assert followup == "UI input: sk-test-openai"
    assert text_called["value"] is False


def test_prompt_for_classic_chat_ui_action_sensitive_text_keeps_default_without_passing_it_to_prompt(monkeypatch):
    captured: dict[str, str] = {}

    def _fake_password(message, **kwargs):
        captured["message"] = str(message)
        captured["default"] = str(kwargs.get("default", ""))
        return SimpleNamespace(ask=lambda: "")

    monkeypatch.setattr("armorer.main.questionary.password", _fake_password)

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "text", "message": "Enter OPENAI_API_KEY", "default": "sk-existing-secret"}
    )

    assert followup == "UI input: skip"
    assert captured["default"] == ""
    assert captured["message"] == (
        "Enter OPENAI_API_KEY\nStored value available; press Enter to keep it.\nType /cancel to abort."
    )


def test_prompt_for_classic_chat_ui_action_secret_looking_default_uses_password_with_generic_message(monkeypatch):
    captured: dict[str, str] = {}
    text_called = {"value": False}

    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda *args, **kwargs: text_called.__setitem__("value", True),
    )

    def _fake_password(message, **kwargs):
        captured["message"] = str(message)
        captured["default"] = str(kwargs.get("default", ""))
        return SimpleNamespace(ask=lambda: "")

    monkeypatch.setattr("armorer.main.questionary.password", _fake_password)

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "text", "message": "Enter value", "default": "sk-existing-secret"}
    )

    assert followup == "UI input: skip"
    assert text_called["value"] is False
    assert captured["default"] == ""
    assert captured["message"] == (
        "Enter value\nStored value available; press Enter to keep it.\nType /cancel to abort."
    )


def test_prompt_for_classic_chat_ui_action_gemini_like_default_uses_password_with_generic_message(monkeypatch):
    captured: dict[str, str] = {}
    text_called = {"value": False}

    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda *args, **kwargs: text_called.__setitem__("value", True),
    )

    def _fake_password(message, **kwargs):
        captured["message"] = str(message)
        captured["default"] = str(kwargs.get("default", ""))
        return SimpleNamespace(ask=lambda: "")

    monkeypatch.setattr("armorer.main.questionary.password", _fake_password)

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "text", "message": "Enter value", "default": "AIzaSyD0123456789abcdefghijklmnopqrstuv"}
    )

    assert followup == "UI input: skip"
    assert text_called["value"] is False
    assert captured["default"] == ""
    assert captured["message"] == (
        "Enter value\nStored value available; press Enter to keep it.\nType /cancel to abort."
    )


def test_prompt_for_classic_chat_ui_action_gemini_like_default_uses_password_with_generic_message(monkeypatch):
    captured: dict[str, str] = {}
    text_called = {"value": False}

    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda *args, **kwargs: text_called.__setitem__("value", True),
    )

    def _fake_password(message, **kwargs):
        captured["message"] = str(message)
        captured["default"] = str(kwargs.get("default", ""))
        return SimpleNamespace(ask=lambda: "")

    monkeypatch.setattr("armorer.main.questionary.password", _fake_password)

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "text", "message": "Enter value", "default": "AIzaSyD0123456789abcdefghijklmnopqrstuv"}
    )

    assert followup == "UI input: skip"
    assert text_called["value"] is False
    assert captured["default"] == ""
    assert captured["message"] == (
        "Enter value\nStored value available; press Enter to keep it.\nType /cancel to abort."
    )


def test_run_classic_chat_task_redacts_sensitive_text_ui_action_followup(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.side_effect = [
        {
            "id": "task-1",
            "message": "ignored",
            "raw": {
                "id": "task-1",
                "response": '{"message":"Paste key","ui_action":{"type":"text","message":"Enter OPENAI_API_KEY"}}',
            },
        },
        {
            "id": "task-2",
            "message": "ignored",
            "raw": {
                "id": "task-2",
                "response": '{"message":"Saved"}',
            },
        },
    ]
    relay.submit_task.return_value = "task-2"

    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.password",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: "sk-test-openai"),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    relay.submit_task.assert_called_once_with(prompt="UI input: sk-test-openai", source="cli", metadata=CLI_METADATA)
    assert any("👤 You: UI input: [redacted]" in line for line in printed)
    assert not any("sk-test-openai" in line for line in printed)


def test_run_classic_chat_task_redacts_sensitive_text_default_followup(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.side_effect = [
        {
            "id": "task-1",
            "message": "ignored",
            "raw": {
                "id": "task-1",
                "response": '{"message":"Paste key","ui_action":{"type":"text","message":"Enter OPENAI_API_KEY","default":"sk-existing-secret"}}',
            },
        },
        {
            "id": "task-2",
            "message": "ignored",
            "raw": {
                "id": "task-2",
                "response": '{"message":"Saved"}',
            },
        },
    ]
    relay.submit_task.return_value = "task-2"

    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.password",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: ""),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    relay.submit_task.assert_called_once_with(prompt="UI input: skip", source="cli", metadata=CLI_METADATA)
    assert any("👤 You: UI input: skip" in line for line in printed)
    assert not any("sk-existing-secret" in line for line in printed)


def test_run_classic_chat_task_redacts_secret_looking_default_followup_with_generic_message(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.side_effect = [
        {
            "id": "task-1",
            "message": "ignored",
            "raw": {
                "id": "task-1",
                "response": '{"message":"Paste value","ui_action":{"type":"text","message":"Enter value","default":"sk-existing-secret"}}',
            },
        },
        {
            "id": "task-2",
            "message": "ignored",
            "raw": {
                "id": "task-2",
                "response": '{"message":"Saved"}',
            },
        },
    ]
    relay.submit_task.return_value = "task-2"

    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.password",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: ""),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    relay.submit_task.assert_called_once_with(prompt="UI input: skip", source="cli", metadata=CLI_METADATA)
    assert any("👤 You: UI input: skip" in line for line in printed)
    assert not any("sk-existing-secret" in line for line in printed)


def test_run_classic_chat_task_redacts_gemini_like_default_followup_with_generic_message(monkeypatch):
    leaked_key = "AIzaSyD0123456789abcdefghijklmnopqrstuv"
    relay = MagicMock()
    relay.wait_for_response.side_effect = [
        {
            "id": "task-1",
            "message": "ignored",
            "raw": {
                "id": "task-1",
                "response": json.dumps(
                    {"message": "Paste value", "ui_action": {"type": "text", "message": "Enter value", "default": leaked_key}}
                ),
            },
        },
        {
            "id": "task-2",
            "message": "ignored",
            "raw": {
                "id": "task-2",
                "response": '{"message":"Saved"}',
            },
        },
    ]
    relay.submit_task.return_value = "task-2"

    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.password",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: ""),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    relay.submit_task.assert_called_once_with(prompt="UI input: skip", source="cli", metadata=CLI_METADATA)
    assert any("👤 You: UI input: skip" in line for line in printed)
    assert not any(leaked_key in line for line in printed)


def test_run_classic_chat_task_redacts_gemini_like_default_followup_with_generic_message(monkeypatch):
    leaked_key = "AIzaSyD0123456789abcdefghijklmnopqrstuv"
    relay = MagicMock()
    relay.wait_for_response.side_effect = [
        {
            "id": "task-1",
            "message": "ignored",
            "raw": {
                "id": "task-1",
                "response": json.dumps(
                    {"message": "Paste value", "ui_action": {"type": "text", "message": "Enter value", "default": leaked_key}}
                ),
            },
        },
        {
            "id": "task-2",
            "message": "ignored",
            "raw": {
                "id": "task-2",
                "response": '{"message":"Saved"}',
            },
        },
    ]
    relay.submit_task.return_value = "task-2"

    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.password",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: ""),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    relay.submit_task.assert_called_once_with(prompt="UI input: skip", source="cli", metadata=CLI_METADATA)
    assert any("👤 You: UI input: skip" in line for line in printed)
    assert not any(leaked_key in line for line in printed)


def test_run_classic_chat_task_redacts_secret_like_select_followup_display(monkeypatch):
    relay = MagicMock()
    leaked_token = "123456:abcDEF_token_sample_value"
    relay.wait_for_response.side_effect = [
        {
            "id": "task-1",
            "message": "ignored",
            "raw": {
                "id": "task-1",
                "response": json.dumps(
                    {
                        "message": "Choose token",
                        "ui_action": {"type": "select", "message": "Pick token", "options": [leaked_token, "openai"]},
                    }
                ),
            },
        },
        {
            "id": "task-2",
            "message": "ignored",
            "raw": {
                "id": "task-2",
                "response": '{"message":"done"}',
            },
        },
    ]
    relay.submit_task.return_value = "task-2"

    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.select",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: leaked_token),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    relay.submit_task.assert_called_once_with(prompt=f"UI selection: {leaked_token}", source="cli", metadata=CLI_METADATA)
    assert any("👤 You: UI selection: [REDACTED_TELEGRAM_TOKEN]" in line for line in printed)
    assert not any(f"👤 You: UI selection: {leaked_token}" in line for line in printed)


def test_run_classic_chat_task_prints_cleanup_note_after_followup_submit(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.side_effect = [
        {
            "id": "task-1",
            "message": "ignored",
            "raw": {
                "id": "task-1",
                "response": '{"message":"Choose provider","ui_action":{"type":"select","message":"Pick provider","options":["openai","ollama"]}}',
            },
        },
        {
            "id": "task-2",
            "message": "ignored",
            "raw": {
                "id": "task-2",
                "response": '{"message":"done"}',
            },
        },
    ]

    def _submit_task(prompt, source, metadata=None):
        relay.last_submit_cleanup = ["inbox:old.json", "status:old.json"]
        assert metadata == CLI_METADATA
        return "task-2"

    relay.submit_task.side_effect = _submit_task

    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.select",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: "openai"),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    assert any("Cleaned stale queue files before retrying: inbox:old.json, status:old.json." in line for line in printed)


def test_prompt_for_classic_chat_ui_action_confirm_no(monkeypatch):
    monkeypatch.setattr(
        "armorer.main.questionary.confirm",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: False),
    )

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "confirm", "message": "Proceed?", "default": True}
    )

    assert followup == "UI confirmation: no"


def test_prompt_for_classic_chat_ui_action_confirm_dismissed_fails_closed(monkeypatch):
    monkeypatch.setattr(
        "armorer.main.questionary.confirm",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: None),
    )

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "confirm", "message": "Proceed?", "default": True}
    )

    assert followup == "UI confirmation: no"


def test_run_classic_chat_task_submits_no_for_confirmation_cancel(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.side_effect = [
        {
            "id": "task-1",
            "message": "ignored",
            "raw": {
                "id": "task-1",
                "response": '{"message":"Dangerous action","requires_confirmation":true}',
            },
        },
        {
            "id": "task-2",
            "message": "ignored",
            "raw": {
                "id": "task-2",
                "response": '{"message":"Cancelled. No high-risk action was executed."}',
            },
        },
    ]
    relay.submit_task.return_value = "task-2"

    monkeypatch.setattr(
        "armorer.main.questionary.select",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: "No, cancel"),
    )

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    relay.submit_task.assert_called_once_with(prompt="No", source="cli", metadata=CLI_METADATA)


def test_run_classic_chat_task_submits_no_when_confirmation_prompt_is_dismissed(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.side_effect = [
        {
            "id": "task-1",
            "message": "ignored",
            "raw": {
                "id": "task-1",
                "response": '{"message":"Dangerous action","requires_confirmation":true}',
            },
        },
        {
            "id": "task-2",
            "message": "ignored",
            "raw": {
                "id": "task-2",
                "response": '{"message":"Cancelled. No high-risk action was executed."}',
            },
        },
    ]
    relay.submit_task.return_value = "task-2"

    monkeypatch.setattr(
        "armorer.main.questionary.select",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: None),
    )

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    relay.submit_task.assert_called_once_with(prompt="No", source="cli", metadata=CLI_METADATA)


def test_prompt_for_classic_chat_ui_action_interactive_command_cancel(monkeypatch):
    prompts: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.confirm",
        lambda message, **kwargs: prompts.append(message) or SimpleNamespace(ask=lambda: False),
    )

    followup = _prompt_for_classic_chat_ui_action(
        {
            "type": "interactive_command",
            "message": "Run setup command?",
            "command": "armorer gateway telegram",
            "reason": "Starts Telegram long-polling gateway in this terminal.",
        }
    )

    assert followup == "User canceled interactive command."
    assert prompts == [
        "Run setup command?\n\nStarts Telegram long-polling gateway in this terminal.\n\nCommand: armorer gateway telegram"
    ]


def test_prompt_for_classic_chat_ui_action_interactive_command_redacts_secret_like_command(monkeypatch):
    prompts: list[str] = []
    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"

    monkeypatch.setattr(
        "armorer.main.questionary.confirm",
        lambda message, **kwargs: prompts.append(message) or SimpleNamespace(ask=lambda: False),
    )

    followup = _prompt_for_classic_chat_ui_action(
        {
            "type": "interactive_command",
            "message": "Run setup command?",
            "command": f"armorer gateway telegram --api-key {leaked_key} --token {leaked_token}",
            "reason": f"Uses token {leaked_token}",
        }
    )

    assert followup == "User canceled interactive command."
    assert len(prompts) == 1
    assert "[REDACTED_OPENAI_KEY]" in prompts[0]
    assert "[REDACTED_TELEGRAM_TOKEN]" in prompts[0]
    assert leaked_key not in prompts[0]
    assert leaked_token not in prompts[0]


def test_run_classic_chat_interactive_command_splits_shell_like_arguments(monkeypatch):
    seen: dict[str, object] = {}

    def _fake_run(args):
        seen["args"] = args
        return SimpleNamespace(returncode=0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = _run_classic_chat_interactive_command('armorer gateway telegram --chat "ops room"')

    assert seen["args"] == ["armorer", "gateway", "telegram", "--chat", "ops room"]
    assert result == "Interactive command finished with exit code 0."


def test_run_classic_chat_interactive_command_reports_parse_error(monkeypatch):
    monkeypatch.setattr(
        "armorer.main.subprocess.run",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("subprocess.run should not be called")),
    )

    result = _run_classic_chat_interactive_command('armorer gateway "telegram')

    assert "Interactive command failed to parse:" in result


def test_run_classic_chat_interactive_command_redacts_secret_like_parse_error(monkeypatch):
    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"

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

    monkeypatch.setattr("armorer.main.shlex.split", _boom)

    result = _run_classic_chat_interactive_command("armorer gateway telegram")

    assert "[REDACTED_OPENAI_KEY]" in result
    assert leaked_key not in result


def test_run_classic_chat_task_keeps_interactive_command_cancel_local(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {
            "id": "task-1",
            "response": '{"message":"Run gateway?","ui_action":{"type":"interactive_command","message":"Run setup command?","command":"armorer gateway telegram"}}',
        },
    }

    monkeypatch.setattr(
        "armorer.main.questionary.confirm",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: False),
    )

    _run_classic_chat_task(relay, "task-1", ui_width=80)

    relay.submit_task.assert_not_called()


def test_run_classic_chat_task_keeps_interactive_command_parse_error_local(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {
            "id": "task-1",
            "response": '{"message":"Run gateway?","ui_action":{"type":"interactive_command","message":"Run setup command?","command":"armorer gateway \\"telegram"}}',
        },
    }

    printed: list[str] = []
    monkeypatch.setattr(
        "armorer.main.questionary.confirm",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: True),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    _run_classic_chat_task(relay, "task-1", ui_width=80)

    relay.submit_task.assert_not_called()
    assert any("Interactive command failed to parse:" in line for line in printed)


def test_run_classic_chat_task_redacts_secret_like_relay_failure(monkeypatch):
    relay = MagicMock()
    leaked_token = "123456:abcDEF_token_sample_value"
    relay.wait_for_response.side_effect = RuntimeError(f"core unavailable: {leaked_token}")

    printed: list[str] = []
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    _run_classic_chat_task(relay, "task-1", ui_width=80)

    assert any("[REDACTED_TELEGRAM_TOKEN]" in line for line in printed)
    assert not any(leaked_token in line for line in printed)


def test_run_classic_chat_task_keeps_empty_interactive_command_error_local():
    relay = MagicMock()
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {
            "id": "task-1",
            "response": '{"message":"Run gateway?","ui_action":{"type":"interactive_command","message":"Run setup command?","command":""}}',
        },
    }

    _run_classic_chat_task(relay, "task-1", ui_width=80)

    relay.submit_task.assert_not_called()


def test_run_classic_chat_task_keeps_interactive_command_result_local(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {
            "id": "task-1",
            "response": '{"message":"Run gateway?","ui_action":{"type":"interactive_command","message":"Run setup command?","command":"armorer gateway telegram"}}',
        },
    }

    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.confirm",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: True),
    )
    monkeypatch.setattr(
        "armorer.main._run_classic_chat_interactive_command",
        lambda command: f"Interactive command finished with exit code 0. ({command})",
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    _run_classic_chat_task(relay, "task-1", ui_width=80)

    relay.submit_task.assert_not_called()
    assert any("Interactive command finished with exit code 0. (armorer gateway telegram)" in line for line in printed)


def test_run_classic_chat_task_redacts_secret_like_interactive_command_result_local(monkeypatch):
    relay = MagicMock()
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {
            "id": "task-1",
            "response": '{"message":"Run gateway?","ui_action":{"type":"interactive_command","message":"Run setup command?","command":"armorer gateway telegram"}}',
        },
    }

    leaked_token = "123456:abcDEF_token_sample_value"
    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.questionary.confirm",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: True),
    )
    monkeypatch.setattr(
        "armorer.main._run_classic_chat_interactive_command",
        lambda command: f"Interactive command failed: leaked {leaked_token} via {command}",
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    _run_classic_chat_task(relay, "task-1", ui_width=80)

    relay.submit_task.assert_not_called()
    assert any("[REDACTED_TELEGRAM_TOKEN]" in line for line in printed)
    assert not any(leaked_token in line for line in printed)


def test_prompt_for_classic_chat_ui_action_interactive_command_runs(monkeypatch):
    monkeypatch.setattr(
        "armorer.main.questionary.confirm",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: True),
    )
    monkeypatch.setattr(
        "armorer.main._run_classic_chat_interactive_command",
        lambda command: f"ran: {command}",
    )

    followup = _prompt_for_classic_chat_ui_action(
        {"type": "interactive_command", "message": "Run setup command?", "command": "armorer gateway telegram"}
    )

    assert followup == "ran: armorer gateway telegram"


def test_chat_surfaces_initial_submit_failure_locally(monkeypatch):
    relay = MagicMock()
    relay.submit_task.side_effect = RuntimeError("submit failed")

    prompts = iter(["install openclaw", "exit"])
    printed: list[str] = []

    monkeypatch.setattr("armorer.main.load_dotenv", lambda **kwargs: None)
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda: None)
    monkeypatch.setattr("armorer.main._print_logo", lambda: None)
    monkeypatch.setattr("armorer.main.sys.stdin", SimpleNamespace(isatty=lambda: False))
    monkeypatch.setattr("armorer.main.build_local_cli_task_metadata", lambda: CLI_METADATA)
    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: next(prompts)),
    )
    monkeypatch.setattr("armorer.main.InboxOutboxRelay", lambda *args, **kwargs: relay)
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    chat()

    relay.submit_task.assert_called_once_with(prompt="install openclaw", source="cli", metadata=CLI_METADATA)
    assert any("Failed to contact Armorer Core: submit failed" in line for line in printed)


def test_chat_surfaces_initial_submit_failure_redacts_secret_like_detail(monkeypatch):
    relay = MagicMock()
    leaked_token = "123456:abcDEF_token_sample_value"
    relay.submit_task.side_effect = RuntimeError(f"submit failed: {leaked_token}")

    prompts = iter(["install openclaw", "exit"])
    printed: list[str] = []

    monkeypatch.setattr("armorer.main.load_dotenv", lambda **kwargs: None)
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda: None)
    monkeypatch.setattr("armorer.main._print_logo", lambda: None)
    monkeypatch.setattr("armorer.main.sys.stdin", SimpleNamespace(isatty=lambda: False))
    monkeypatch.setattr("armorer.main.build_local_cli_task_metadata", lambda: CLI_METADATA)
    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: next(prompts)),
    )
    monkeypatch.setattr("armorer.main.InboxOutboxRelay", lambda *args, **kwargs: relay)
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    chat()

    relay.submit_task.assert_called_once_with(prompt="install openclaw", source="cli", metadata=CLI_METADATA)
    assert any("Failed to contact Armorer Core: submit failed: [REDACTED_TELEGRAM_TOKEN]" in line for line in printed)
    assert not any(leaked_token in line for line in printed)


def test_chat_textual_fallback_redacts_secret_like_error_detail(monkeypatch):
    relay = MagicMock()
    prompts = iter(["exit"])
    printed: list[str] = []
    leaked_token = "123456:abcDEF_token_sample_value"

    class _FakeTextualApp:
        def run(self):
            raise RuntimeError(f"textual failed: {leaked_token}")

    fake_mod = ModuleType("armorer.ui.chat_textual")
    fake_mod.ArmorerChatApp = _FakeTextualApp

    monkeypatch.setitem(sys.modules, "armorer.ui.chat_textual", fake_mod)
    monkeypatch.setattr("armorer.main.load_dotenv", lambda **kwargs: None)
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda: None)
    monkeypatch.setattr("armorer.main._print_logo", lambda: None)
    monkeypatch.setattr("armorer.main.sys.stdin", SimpleNamespace(isatty=lambda: True))
    monkeypatch.setattr("armorer.main.build_local_cli_task_metadata", lambda: CLI_METADATA)
    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: next(prompts)),
    )
    monkeypatch.setattr("armorer.main.InboxOutboxRelay", lambda *args, **kwargs: relay)
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    chat()

    assert any(
        "Textual chat unavailable, falling back to classic chat: textual failed: [REDACTED_TELEGRAM_TOKEN]" in line
        for line in printed
    )
    assert not any(leaked_token in line for line in printed)


def test_check_self_dockerize_redacts_secret_like_startup_failure(monkeypatch, tmp_path: Path):
    leaked_token = "123456:abcDEF_token_sample_value"
    printed: list[str] = []

    class _FakeStatus:
        def __enter__(self):
            return SimpleNamespace(update=lambda *args, **kwargs: None)

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

    def _boom(*args, **kwargs):
        raise RuntimeError(f"docker failed: {leaked_token}")

    monkeypatch.delenv("ARMORER_SANDBOX", raising=False)
    monkeypatch.setattr("armorer.main.os.path.exists", lambda _path: False)
    monkeypatch.setattr("armorer.main._armorer_config_dir", lambda: tmp_path / ".armorer")
    monkeypatch.setattr("armorer.main.console.status", lambda *args, **kwargs: _FakeStatus())
    monkeypatch.setattr("armorer.main.subprocess.run", _boom)
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    raised: SystemExit | None = None
    try:
        _check_self_dockerize()
    except SystemExit as exc:
        raised = exc

    assert raised is not None
    assert int(getattr(raised, "code", 0)) == 1
    assert any("Failed to start Dockerized Armorer: docker failed: [REDACTED_TELEGRAM_TOKEN]" in line for line in printed)
    assert not any(leaked_token in line for line in printed)


def test_chat_uses_config_dir_override_for_relay(monkeypatch, tmp_path: Path):
    config_dir = tmp_path / "alt-armorer"
    prompts = iter(["exit"])
    captured: dict[str, object] = {}

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

    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.load_dotenv", lambda **kwargs: None)
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda: None)
    monkeypatch.setattr("armorer.main._print_logo", lambda: None)
    monkeypatch.setattr("armorer.main.sys.stdin", SimpleNamespace(isatty=lambda: False))
    monkeypatch.setattr("armorer.main.build_local_cli_task_metadata", lambda: CLI_METADATA)
    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: next(prompts)),
    )
    monkeypatch.setattr("armorer.main.InboxOutboxRelay", _fake_relay)

    chat()

    assert captured["config_dir"] == config_dir


def test_chat_runs_managed_openclaw_flow_end_to_end_in_classic_fallback(monkeypatch):
    relay = MagicMock()
    submitted: list[tuple[str, str, dict | None]] = []
    task_ids = iter(["task-1", "task-2"])
    status_updates: list[str] = []
    printed: list[str] = []
    renderables: list[object] = []

    def _submit_task(prompt, source, metadata=None):
        submitted.append((prompt, source, metadata))
        if prompt == "UI selection: openai":
            relay.last_submit_cleanup = ["inbox:stale.json", "status:stale.json"]
        else:
            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)

    class _FakeStatus:
        def __enter__(self):
            return SimpleNamespace(update=lambda message: status_updates.append(message))

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

    relay.submit_task.side_effect = _submit_task
    relay.wait_for_response.side_effect = _wait_for_response

    prompts = iter(["install openclaw", "exit"])

    monkeypatch.setattr("armorer.main.load_dotenv", lambda **kwargs: None)
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda: None)
    monkeypatch.setattr("armorer.main._print_logo", lambda: None)
    monkeypatch.setattr("armorer.main.sys.stdin", SimpleNamespace(isatty=lambda: False))
    monkeypatch.setattr("armorer.main.build_local_cli_task_metadata", lambda: CLI_METADATA)
    monkeypatch.setattr(
        "armorer.main.questionary.text",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: next(prompts)),
    )
    monkeypatch.setattr(
        "armorer.main.questionary.select",
        lambda *args, **kwargs: SimpleNamespace(ask=lambda: "openai"),
    )
    monkeypatch.setattr("armorer.main.InboxOutboxRelay", lambda *args, **kwargs: relay)
    monkeypatch.setattr("armorer.main.console.status", lambda *args, **kwargs: _FakeStatus())
    monkeypatch.setattr(
        "armorer.main.console.print",
        lambda *args, **kwargs: (renderables.extend(args), printed.append(" ".join(str(a) for a in args))),
    )

    chat()

    assert submitted == [
        ("install openclaw", "cli", CLI_METADATA),
        ("UI selection: openai", "cli", CLI_METADATA),
    ]
    assert status_updates == [
        "Collecting OpenClaw setup details",
        "Starting OpenClaw services",
    ]
    assert any(
        getattr(getattr(renderable, "renderable", None), "markup", "")
        == "OpenClaw is installed and verified\nOpenClaw status: healthy\nFinal status check passed via `armorer run openclaw status`."
        for renderable in renderables
    )
    assert any("Cleaned stale queue files before retrying: inbox:stale.json, status:stale.json." in line for line in printed)


def test_wait_for_chat_result_redacts_secret_like_status_updates(monkeypatch):
    relay = MagicMock()
    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    status_updates: list[str] = []

    def _fake_wait_for_response(task_id, timeout, poll_interval, status_callback):
        assert task_id == "task-1"
        status_callback(f"Setting provider key {leaked_key} and telegram token {leaked_token}")
        return {"id": task_id, "message": "done", "raw": {"id": task_id, "response": '{"message":"done"}'}}

    class _FakeStatus:
        def __enter__(self):
            return SimpleNamespace(update=lambda message: status_updates.append(str(message)))

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

    relay.wait_for_response.side_effect = _fake_wait_for_response
    monkeypatch.setattr("armorer.main.console.status", lambda *args, **kwargs: _FakeStatus())

    result = _wait_for_chat_result(relay, "task-1")

    assert result["message"] == "done"
    assert len(status_updates) == 1
    assert "[REDACTED_OPENAI_KEY]" in status_updates[0]
    assert "[REDACTED_TELEGRAM_TOKEN]" in status_updates[0]
    assert leaked_key not in status_updates[0]
    assert leaked_token not in status_updates[0]


def test_run_classic_chat_task_redacts_secret_like_assistant_message(monkeypatch):
    relay = MagicMock()
    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {
            "id": "task-1",
            "response": json.dumps({"message": f"Setup details {leaked_key} {leaked_token}"}),
        },
    }

    rendered_markdown: list[str] = []
    monkeypatch.setattr("armorer.main.Markdown", lambda text: rendered_markdown.append(str(text)) or str(text))
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: None)

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    assert len(rendered_markdown) >= 1
    assert any("[REDACTED_OPENAI_KEY]" in text for text in rendered_markdown)
    assert any("[REDACTED_TELEGRAM_TOKEN]" in text for text in rendered_markdown)
    assert all(leaked_key not in text for text in rendered_markdown)
    assert all(leaked_token not in text for text in rendered_markdown)


def test_run_classic_chat_task_surfaces_openclaw_rollback_failure_message(monkeypatch):
    relay = MagicMock()
    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."
    )
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {"id": "task-1", "response": json.dumps({"message": rollback_message})},
    }

    rendered_markdown: list[str] = []
    monkeypatch.setattr("armorer.main.Markdown", lambda text: rendered_markdown.append(str(text)) or str(text))
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: None)

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    assert rollback_message in rendered_markdown


def test_run_classic_chat_task_surfaces_openclaw_generic_failure_message(monkeypatch):
    relay = MagicMock()
    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."
    )
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {"id": "task-1", "response": json.dumps({"message": failure_message})},
    }

    rendered_markdown: list[str] = []
    monkeypatch.setattr("armorer.main.Markdown", lambda text: rendered_markdown.append(str(text)) or str(text))
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: None)

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    assert failure_message in rendered_markdown


def test_run_classic_chat_task_surfaces_openclaw_registry_only_rollback_message(monkeypatch):
    relay = MagicMock()
    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."
    )
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {"id": "task-1", "response": json.dumps({"message": rollback_message})},
    }

    rendered_markdown: list[str] = []
    monkeypatch.setattr("armorer.main.Markdown", lambda text: rendered_markdown.append(str(text)) or str(text))
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: None)

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    assert rollback_message in rendered_markdown


def test_run_classic_chat_task_surfaces_openclaw_rollback_warning_message(monkeypatch):
    relay = MagicMock()
    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."
    )
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {"id": "task-1", "response": json.dumps({"message": rollback_message})},
    }

    rendered_markdown: list[str] = []
    monkeypatch.setattr("armorer.main.Markdown", lambda text: rendered_markdown.append(str(text)) or str(text))
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: None)

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    assert rollback_message in rendered_markdown


def test_run_classic_chat_task_surfaces_openclaw_fresh_install_start_rollback_message(monkeypatch):
    relay = MagicMock()
    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."
    )
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {"id": "task-1", "response": json.dumps({"message": rollback_message})},
    }

    rendered_markdown: list[str] = []
    monkeypatch.setattr("armorer.main.Markdown", lambda text: rendered_markdown.append(str(text)) or str(text))
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: None)

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    assert rollback_message in rendered_markdown


def test_run_classic_chat_task_surfaces_openclaw_fresh_install_start_rollback_warning_message(monkeypatch):
    relay = MagicMock()
    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."
    )
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "ignored",
        "raw": {"id": "task-1", "response": json.dumps({"message": rollback_message})},
    }

    rendered_markdown: list[str] = []
    monkeypatch.setattr("armorer.main.Markdown", lambda text: rendered_markdown.append(str(text)) or str(text))
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: None)

    _run_classic_chat_task(relay, "task-1", ui_width=80, metadata=CLI_METADATA)

    assert rollback_message in rendered_markdown
