import json
import time
from collections import deque
from pathlib import Path
from unittest.mock import MagicMock

from armorer.messaging.bridge import (
    InboxOutboxRelay,
    SignalCoreBridge,
    TelegramCoreBridge,
    _extract_core_message,
    build_local_cli_task_metadata,
    format_progress_note,
    format_queue_cleanup_note,
)


def test_extract_core_message_json_payload():
    raw = '{"message": "operation complete", "ui_action": {"type": "confirm"}}'
    assert _extract_core_message(raw) == "operation complete"


def test_extract_core_message_can_append_ui_action_hints():
    raw = {
        "message": "Choose OpenClaw provider",
        "ui_action": {"type": "select", "options": ["openai", "ollama"]},
    }

    message = _extract_core_message(raw, include_ui_hints=True)

    assert message == "Choose OpenClaw provider\nOptions: openai, ollama. Reply `cancel` to abort."


def test_extract_core_message_redacts_secret_like_values_in_select_ui_action_hint():
    leaked_token = "123456:abcDEF_token_sample_value"

    raw = {
        "message": "Choose OpenClaw token",
        "ui_action": {"type": "select", "options": [leaked_token, "openai"]},
    }

    message = _extract_core_message(raw, include_ui_hints=True)

    assert message == "Choose OpenClaw token\nOptions: [REDACTED_TELEGRAM_TOKEN], openai. Reply `cancel` to abort."
    assert leaked_token not in message


def test_extract_core_message_can_append_text_ui_action_cancel_hint():
    raw = {
        "message": "Enter OpenAI API key",
        "ui_action": {"type": "text", "message": "Enter OpenAI API key"},
    }

    message = _extract_core_message(raw, include_ui_hints=True)

    assert message == "Enter OpenAI API key\nReply with the requested value or `cancel` to abort."


def test_extract_core_message_preserves_openclaw_rollback_failure_copy():
    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."
    )

    message = _extract_core_message({"message": rollback_message}, include_ui_hints=True)

    assert message == rollback_message


def test_extract_core_message_omits_sensitive_text_default_from_ui_action_hint():
    raw = {
        "message": "Enter OpenAI API key",
        "ui_action": {
            "type": "text",
            "message": "Enter OpenAI API key",
            "default": "sk-proj-abcdefghijklmnopqrstuvwxyz123456",
            "sensitive": True,
        },
    }

    message = _extract_core_message(raw, include_ui_hints=True)

    assert message == "Enter OpenAI API key\nReply with the requested value, or `skip` to keep the current value. Reply `cancel` to abort."
    assert "sk-proj-" not in message


def test_extract_core_message_omits_secret_looking_default_from_generic_text_ui_action_hint():
    raw = {
        "message": "Input required",
        "ui_action": {
            "type": "text",
            "message": "Input required",
            "default": "123456789:ABCdefGHIjklMNOpqrs_tuvw",
        },
    }

    message = _extract_core_message(raw, include_ui_hints=True)

    assert message == "Input required\nReply with the requested value, or `skip` to keep the current value. Reply `cancel` to abort."
    assert "123456789:" not in message


def test_extract_core_message_omits_gemini_like_default_from_generic_text_ui_action_hint():
    leaked_key = "AIzaSyAbCdEfGhIjKlMnOpQrStUvWxYz0123456"
    raw = {
        "message": "Input required",
        "ui_action": {
            "type": "text",
            "message": "Input required",
            "default": leaked_key,
        },
    }

    message = _extract_core_message(raw, include_ui_hints=True)

    assert message == "Input required\nReply with the requested value, or `skip` to keep the current value. Reply `cancel` to abort."
    assert leaked_key not in message


def test_extract_core_message_sensitive_text_ui_action_hint_without_message_mentions_skip_keep_current():
    raw = {
        "ui_action": {
            "type": "text",
            "message": "Enter TELEGRAM_BOT_TOKEN",
            "default": "123456789:ABCdefGHIjklMNOpqrs_tuvw",
            "sensitive": True,
        },
    }

    message = _extract_core_message(raw, include_ui_hints=True)

    assert message == "Reply with the requested value, or `skip` to keep the current value. Reply `cancel` to abort."
    assert "123456789:" not in message


def test_extract_core_message_can_fall_back_to_ui_action_hint_without_message():
    raw = {
        "ui_action": {"type": "select", "options": ["openai", "ollama"]},
    }

    message = _extract_core_message(raw, include_ui_hints=True)

    assert message == "Options: openai, ollama. Reply `cancel` to abort."


def test_extract_core_message_keeps_error_when_ui_action_hint_is_present():
    raw = {
        "error": "Provider selection is required.",
        "ui_action": {"type": "select", "options": ["openai", "ollama"]},
    }

    message = _extract_core_message(raw, include_ui_hints=True)

    assert message == "Provider selection is required.\nOptions: openai, ollama. Reply `cancel` to abort."


def test_extract_core_message_can_append_confirmation_hint():
    raw = '{"message": "Install and configure OpenClaw now?", "requires_confirmation": true}'

    message = _extract_core_message(raw, include_ui_hints=True)

    assert message == "Install and configure OpenClaw now?\nReply `yes` or `no`. Reply `cancel` to abort."


def test_extract_core_message_redacts_secret_like_values_in_message_body():
    leaked_openai = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    leaked_gemini = "AIzaSyAbCdEfGhIjKlMnOpQrStUvWxYz0123456"

    message = _extract_core_message(
        {
            "message": f"Configured keys: {leaked_openai} {leaked_token} {leaked_gemini}",
        }
    )

    assert "[REDACTED_OPENAI_KEY]" in message
    assert "[REDACTED_TELEGRAM_TOKEN]" in message
    assert "[REDACTED_GEMINI_KEY]" in message
    assert leaked_openai not in message
    assert leaked_token not in message
    assert leaked_gemini not in message


def test_extract_core_message_redacts_secret_like_values_in_error_body():
    leaked_openai = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_gemini = "AIzaSyAbCdEfGhIjKlMnOpQrStUvWxYz0123456"

    message = _extract_core_message(
        json.dumps(
            {
                "error": f"Provider rejected keys {leaked_openai} {leaked_gemini}",
            }
        )
    )

    assert "[REDACTED_OPENAI_KEY]" in message
    assert "[REDACTED_GEMINI_KEY]" in message
    assert leaked_openai not in message
    assert leaked_gemini not in message


def test_extract_core_message_redacts_interactive_command_hint_secrets():
    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    leaked_gemini = "AIzaSyAbCdEfGhIjKlMnOpQrStUvWxYz0123456"
    raw = {
        "message": "Run the setup helper",
        "ui_action": {
            "type": "interactive_command",
            "command": f"armorer gateway telegram --api-key {leaked_key} --token {leaked_token} --gemini {leaked_gemini}",
        },
    }

    message = _extract_core_message(raw, include_ui_hints=True)

    assert "Run locally if needed:" in message
    assert "[REDACTED_OPENAI_KEY]" in message
    assert "[REDACTED_TELEGRAM_TOKEN]" in message
    assert "[REDACTED_GEMINI_KEY]" in message
    assert leaked_key not in message
    assert leaked_token not in message
    assert leaked_gemini not in message


def test_extract_core_message_falls_back_to_redacted_interactive_command_hint_without_message():
    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    raw = {
        "ui_action": {
            "type": "interactive_command",
            "command": f"armorer gateway telegram --api-key {leaked_key}",
        },
    }

    message = _extract_core_message(raw, include_ui_hints=True)

    assert message.startswith("Run locally if needed:")
    assert "[REDACTED_OPENAI_KEY]" in message
    assert leaked_key not in message


def test_relay_submit_and_wait_for_response(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install agent", source="telegram", metadata={"channel_id": "7"})

    task_file = tmp_path / "inbox" / f"{task_id}.json"
    assert task_file.exists()
    task_data = json.loads(task_file.read_text())
    assert task_data["prompt"] == "install agent"
    assert task_data["source"] == "telegram"
    assert task_data["metadata"]["channel_id"] == "7"

    outbox_file = tmp_path / "outbox" / f"{task_id}.json"
    outbox_file.write_text(json.dumps({"id": task_id, "response": '{"message":"done"}'}))

    result = relay.wait_for_response(task_id, timeout=2, poll_interval=0.01)
    assert result["message"] == "done"
    assert not outbox_file.exists()
    assert not task_file.exists()
    assert "outbox" in result["cleaned_up"]


def test_relay_zero_timeout_still_returns_immediate_ready_response(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install agent", source="cli")
    task_file = tmp_path / "inbox" / f"{task_id}.json"
    outbox_file = tmp_path / "outbox" / f"{task_id}.json"
    outbox_file.write_text(json.dumps({"id": task_id, "response": '{"message":"done"}'}))

    result = relay.wait_for_response(task_id, timeout=0, poll_interval=0.01)

    assert result["message"] == "done"
    assert "inbox" in result["cleaned_up"]
    assert "outbox" in result["cleaned_up"]
    assert not task_file.exists()
    assert not outbox_file.exists()


def test_relay_success_cleans_lingering_inbox_and_status_files(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install openclaw", source="cli")
    inbox_file = tmp_path / "inbox" / f"{task_id}.json"
    status_file = tmp_path / "status" / f"{task_id}.json"
    outbox_file = tmp_path / "outbox" / f"{task_id}.json"
    status_file.write_text(json.dumps({"message": "Verifying OpenClaw status"}))
    outbox_file.write_text(json.dumps({"id": task_id, "response": '{"message":"ready"}'}))

    result = relay.wait_for_response(task_id, timeout=1, poll_interval=0.01)

    assert result["message"] == "ready"
    assert "inbox" in result["cleaned_up"]
    assert "outbox" in result["cleaned_up"]
    assert "status" in result["cleaned_up"]
    assert not inbox_file.exists()
    assert not outbox_file.exists()
    assert not status_file.exists()


def test_relay_timeout_cleans_queue_files_and_reports_last_status(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install openclaw", source="cli")
    inbox_file = tmp_path / "inbox" / f"{task_id}.json"
    status_file = tmp_path / "status" / f"{task_id}.json"
    status_file.write_text(json.dumps({"message": "Installing OpenClaw"}))

    seen: list[str] = []
    result = relay.wait_for_response(
        task_id,
        timeout=0.02,
        poll_interval=0.01,
        status_callback=seen.append,
    )

    assert result["timed_out"] is True
    assert result["last_status"] == "Installing OpenClaw"
    assert "Retry the request if needed." in result["message"]
    assert "inbox" in result["cleaned_up"]
    assert "status" in result["cleaned_up"]
    assert not inbox_file.exists()
    assert not status_file.exists()
    assert seen == ["Installing OpenClaw"]


def test_relay_timeout_with_zero_poll_interval_still_cleans_queue_files(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install openclaw", source="cli")
    inbox_file = tmp_path / "inbox" / f"{task_id}.json"
    status_file = tmp_path / "status" / f"{task_id}.json"
    status_file.write_text(json.dumps({"message": "Still working"}))

    started = time.monotonic()
    result = relay.wait_for_response(task_id, timeout=0.02, poll_interval=0)
    elapsed = time.monotonic() - started

    assert result["timed_out"] is True
    assert elapsed < 1.0
    assert "inbox" in result["cleaned_up"]
    assert "status" in result["cleaned_up"]
    assert not inbox_file.exists()
    assert not status_file.exists()


def test_relay_status_callback_receives_updates_before_response(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install openclaw", source="cli")
    status_file = tmp_path / "status" / f"{task_id}.json"
    outbox_file = tmp_path / "outbox" / f"{task_id}.json"
    status_file.write_text(json.dumps({"message": "Configuring OpenClaw"}))
    outbox_file.write_text(json.dumps({"id": task_id, "response": '{"message":"done"}'}))

    seen: list[str] = []
    result = relay.wait_for_response(task_id, timeout=1, poll_interval=0.01, status_callback=seen.append)

    assert result["message"] == "done"
    assert result["last_status"] == "Configuring OpenClaw"
    assert seen == ["Configuring OpenClaw"]


def test_relay_status_callback_failure_does_not_abort_wait_or_cleanup(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install openclaw", source="cli")
    inbox_file = tmp_path / "inbox" / f"{task_id}.json"
    status_file = tmp_path / "status" / f"{task_id}.json"
    outbox_file = tmp_path / "outbox" / f"{task_id}.json"
    status_file.write_text(json.dumps({"message": "Configuring OpenClaw"}))
    outbox_file.write_text(json.dumps({"id": task_id, "response": '{"message":"done"}'}))

    def _boom(_msg: str) -> None:
        raise RuntimeError("status sink unavailable")

    result = relay.wait_for_response(task_id, timeout=1, poll_interval=0.01, status_callback=_boom)

    assert result["message"] == "done"
    assert result["last_status"] == "Configuring OpenClaw"
    assert "inbox" in result["cleaned_up"]
    assert "outbox" in result["cleaned_up"]
    assert "status" in result["cleaned_up"]
    assert not inbox_file.exists()
    assert not status_file.exists()
    assert not outbox_file.exists()


def test_relay_wait_for_response_retries_when_outbox_json_is_temporarily_invalid(tmp_path: Path, monkeypatch):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install openclaw", source="cli")
    outbox_file = tmp_path / "outbox" / f"{task_id}.json"
    inbox_file = tmp_path / "inbox" / f"{task_id}.json"
    outbox_file.write_text("{\"id\":")

    from armorer.messaging import bridge as bridge_mod

    sleep_calls = {"count": 0}
    original_sleep = bridge_mod.time.sleep

    def _fake_sleep(seconds: float) -> None:
        sleep_calls["count"] += 1
        if sleep_calls["count"] == 1:
            outbox_file.write_text(json.dumps({"id": task_id, "response": '{"message":"ready"}'}))
        original_sleep(0)

    monkeypatch.setattr(bridge_mod.time, "sleep", _fake_sleep)

    result = relay.wait_for_response(task_id, timeout=0.3, poll_interval=0.05)

    assert result["message"] == "ready"
    assert "outbox" in result["cleaned_up"]
    assert "inbox" in result["cleaned_up"]
    assert not outbox_file.exists()
    assert not inbox_file.exists()


def test_relay_timeout_reports_invalid_outbox_json_hint(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install openclaw", source="cli")
    outbox_file = tmp_path / "outbox" / f"{task_id}.json"
    inbox_file = tmp_path / "inbox" / f"{task_id}.json"
    outbox_file.write_text("{\"id\":")

    result = relay.wait_for_response(task_id, timeout=0.02, poll_interval=0.01)

    assert result["timed_out"] is True
    assert "invalid JSON" in result["message"]
    assert "outbox" in result["cleaned_up"]
    assert "inbox" in result["cleaned_up"]
    assert not outbox_file.exists()
    assert not inbox_file.exists()


def test_relay_timeout_invalid_outbox_json_hint_redacts_secret_like_parse_error(tmp_path: Path, monkeypatch):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install openclaw", source="cli")
    outbox_file = tmp_path / "outbox" / f"{task_id}.json"
    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    outbox_file.write_text("{\"id\":")

    def _fake_loads(_payload):
        raise ValueError(f"parse failed for provider key {leaked_key}")

    monkeypatch.setattr("armorer.messaging.bridge.json.loads", _fake_loads)

    result = relay.wait_for_response(task_id, timeout=0.02, poll_interval=0.01)

    assert result["timed_out"] is True
    message = str(result.get("message") or "")
    assert "invalid JSON" in message
    assert "[REDACTED_OPENAI_KEY]" in message
    assert leaked_key not in message


def test_relay_timeout_reports_unreadable_status_json_hint(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install openclaw", source="cli")
    status_file = tmp_path / "status" / f"{task_id}.json"
    inbox_file = tmp_path / "inbox" / f"{task_id}.json"
    status_file.write_text("{\"message\":")

    result = relay.wait_for_response(task_id, timeout=0.02, poll_interval=0.01)

    assert result["timed_out"] is True
    assert "unreadable JSON" in result["message"]
    assert "status" in result["cleaned_up"]
    assert "inbox" in result["cleaned_up"]
    assert not status_file.exists()
    assert not inbox_file.exists()


def test_relay_timeout_unreadable_status_json_hint_redacts_secret_like_parse_error(tmp_path: Path, monkeypatch):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install openclaw", source="cli")
    status_file = tmp_path / "status" / f"{task_id}.json"
    leaked_token = "123456:abcDEF_token_sample_value"
    status_file.write_text("{\"message\":")

    def _fake_loads(_payload):
        raise ValueError(f"status parse failed near token {leaked_token}")

    monkeypatch.setattr("armorer.messaging.bridge.json.loads", _fake_loads)

    result = relay.wait_for_response(task_id, timeout=0.02, poll_interval=0.01)

    assert result["timed_out"] is True
    message = str(result.get("message") or "")
    assert "unreadable JSON" in message
    assert "[REDACTED_TELEGRAM_TOKEN]" in message
    assert leaked_token not in message


def test_relay_does_not_report_unreadable_status_hint_after_valid_status_recovery(tmp_path: Path, monkeypatch):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install openclaw", source="cli")
    status_file = tmp_path / "status" / f"{task_id}.json"
    inbox_file = tmp_path / "inbox" / f"{task_id}.json"
    outbox_file = tmp_path / "outbox" / f"{task_id}.json"
    status_file.write_text("{\"message\":")

    from armorer.messaging import bridge as bridge_mod

    sleep_calls = {"count": 0}
    original_sleep = bridge_mod.time.sleep

    def _fake_sleep(seconds: float) -> None:
        sleep_calls["count"] += 1
        if sleep_calls["count"] == 1:
            status_file.write_text(json.dumps({"message": "Recovered progress"}))
            outbox_file.write_text(json.dumps({"id": task_id, "response": '{"message":"done"}'}))
        original_sleep(0)

    monkeypatch.setattr(bridge_mod.time, "sleep", _fake_sleep)

    result = relay.wait_for_response(task_id, timeout=0.3, poll_interval=0.05)

    assert result["message"] == "done"
    assert result["last_status"] == "Recovered progress"
    assert "unreadable JSON" not in result["message"]
    assert "status" in result["cleaned_up"]
    assert "inbox" in result["cleaned_up"]
    assert "outbox" in result["cleaned_up"]
    assert not status_file.exists()
    assert not inbox_file.exists()
    assert not outbox_file.exists()


def test_relay_submit_task_cleans_stale_orphaned_artifacts(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    stale_inbox = tmp_path / "inbox" / "stale-inbox.json"
    stale_outbox = tmp_path / "outbox" / "stale-outbox.json"
    stale_status = tmp_path / "status" / "stale-status.json"
    stale_tmp = tmp_path / "inbox" / "stale-tmp.json.tmp"
    stale_inbox.write_text("{}")
    stale_outbox.write_text("{}")
    stale_status.write_text("{}")
    stale_tmp.write_text("{}")
    old = 0
    stale_inbox.touch()
    stale_outbox.touch()
    stale_status.touch()
    stale_tmp.touch()
    import os
    os.utime(stale_inbox, (old, old))
    os.utime(stale_outbox, (old, old))
    os.utime(stale_status, (old, old))
    os.utime(stale_tmp, (old, old))

    task_id = relay.submit_task("fresh task", source="cli")

    assert task_id
    assert not stale_inbox.exists()
    assert not stale_outbox.exists()
    assert not stale_status.exists()
    assert not stale_tmp.exists()


def test_relay_submit_task_cleans_stale_task_bundle_with_matching_peers(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    stale_inbox = tmp_path / "inbox" / "stale-task.json"
    stale_outbox = tmp_path / "outbox" / "stale-task.json"
    stale_status = tmp_path / "status" / "stale-task.json"
    stale_inbox.write_text("{}")
    stale_outbox.write_text("{}")
    stale_status.write_text("{}")

    old = 0
    import os
    os.utime(stale_inbox, (old, old))
    os.utime(stale_outbox, (old, old))
    os.utime(stale_status, (old, old))

    task_id = relay.submit_task("fresh task", source="cli")

    assert task_id
    assert not stale_inbox.exists()
    assert not stale_outbox.exists()
    assert not stale_status.exists()


def test_relay_submit_task_preserves_stale_in_progress_bundle_without_outbox(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    stale_task = {
        "id": "still-running",
        "prompt": "install openclaw",
        "source": "cli",
        "timestamp": 123.0,
        "metadata": {"channel_id": "other-session"},
    }
    stale_inbox = tmp_path / "inbox" / "still-running.json"
    stale_status = tmp_path / "status" / "still-running.json"
    stale_inbox.write_text(json.dumps(stale_task))
    stale_status.write_text(json.dumps({"message": "Installing OpenClaw"}))

    old = 0
    import os
    os.utime(stale_inbox, (old, old))
    os.utime(stale_status, (old, old))

    task_id = relay.submit_task("fresh task", source="cli", metadata={"channel_id": "new-session"})

    assert task_id
    assert relay.last_submit_cleanup == []
    assert stale_inbox.exists()
    assert stale_status.exists()


def test_relay_submit_task_cleans_recent_matching_conversation_bundle(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    stale_task = {
        "id": "old-task",
        "prompt": "install openclaw",
        "source": "cli",
        "timestamp": 123.0,
        "metadata": {"channel_id": "session-1"},
    }
    stale_inbox = tmp_path / "inbox" / "old-task.json"
    stale_outbox = tmp_path / "outbox" / "old-task.json"
    stale_status = tmp_path / "status" / "old-task.json"
    stale_inbox.write_text(json.dumps(stale_task))
    stale_outbox.write_text(json.dumps({"id": "old-task", "response": '{"message":"old"}'}))
    stale_status.write_text(json.dumps({"message": "Installing OpenClaw"}))

    task_id = relay.submit_task("retry openclaw", source="cli", metadata={"channel_id": "session-1"})

    assert task_id
    assert relay.last_submit_cleanup == [
        "inbox:old-task.json",
        "outbox:old-task.json",
        "status:old-task.json",
    ]
    assert not stale_inbox.exists()
    assert not stale_outbox.exists()
    assert not stale_status.exists()


def test_relay_submit_task_cleans_recent_matching_bundle_with_case_mismatched_source(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    stale_task = {
        "id": "old-case",
        "prompt": "install openclaw",
        "source": "CLI",
        "timestamp": 123.0,
        "metadata": {"channel_id": "session-1"},
    }
    stale_inbox = tmp_path / "inbox" / "old-case.json"
    stale_outbox = tmp_path / "outbox" / "old-case.json"
    stale_status = tmp_path / "status" / "old-case.json"
    stale_inbox.write_text(json.dumps(stale_task))
    stale_outbox.write_text(json.dumps({"id": "old-case", "response": '{"message":"old"}'}))
    stale_status.write_text(json.dumps({"message": "Installing OpenClaw"}))

    task_id = relay.submit_task("retry openclaw", source="cli", metadata={"channel_id": "session-1"})

    assert task_id
    assert relay.last_submit_cleanup == [
        "inbox:old-case.json",
        "outbox:old-case.json",
        "status:old-case.json",
    ]
    assert not stale_inbox.exists()
    assert not stale_outbox.exists()
    assert not stale_status.exists()


def test_relay_submit_task_keeps_other_local_sessions_isolated(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    existing_task = {
        "id": "other-session",
        "prompt": "install openclaw",
        "source": "cli",
        "timestamp": 123.0,
        "metadata": {"channel_id": "tty:ttys002"},
    }
    inbox_file = tmp_path / "inbox" / "other-session.json"
    outbox_file = tmp_path / "outbox" / "other-session.json"
    status_file = tmp_path / "status" / "other-session.json"
    inbox_file.write_text(json.dumps(existing_task))
    outbox_file.write_text(json.dumps({"id": "other-session", "response": '{"message":"old"}'}))
    status_file.write_text(json.dumps({"message": "Installing OpenClaw"}))

    task_id = relay.submit_task("retry openclaw", source="cli", metadata={"channel_id": "tty:ttys003"})

    assert task_id
    assert relay.last_submit_cleanup == []
    assert inbox_file.exists()
    assert outbox_file.exists()
    assert status_file.exists()


def test_relay_submit_task_cleans_recent_matching_messaging_user_bundle(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    existing_task = {
        "id": "same-user",
        "prompt": "install openclaw",
        "source": "telegram",
        "timestamp": 123.0,
        "metadata": {"channel_id": "-100group", "user_id": "111"},
    }
    stale_inbox = tmp_path / "inbox" / "same-user.json"
    stale_outbox = tmp_path / "outbox" / "same-user.json"
    stale_status = tmp_path / "status" / "same-user.json"
    stale_inbox.write_text(json.dumps(existing_task))
    stale_outbox.write_text(json.dumps({"id": "same-user", "response": '{"message":"old"}'}))
    stale_status.write_text(json.dumps({"message": "Installing OpenClaw"}))

    task_id = relay.submit_task(
        "retry openclaw",
        source="telegram",
        metadata={"channel_id": "-100group", "user_id": "111"},
    )

    assert task_id
    assert relay.last_submit_cleanup == [
        "inbox:same-user.json",
        "outbox:same-user.json",
        "status:same-user.json",
    ]
    assert not stale_inbox.exists()
    assert not stale_outbox.exists()
    assert not stale_status.exists()


def test_relay_submit_task_keeps_other_messaging_users_in_same_channel_isolated(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    existing_task = {
        "id": "other-user",
        "prompt": "install openclaw",
        "source": "telegram",
        "timestamp": 123.0,
        "metadata": {"channel_id": "-100group", "user_id": "111"},
    }
    inbox_file = tmp_path / "inbox" / "other-user.json"
    outbox_file = tmp_path / "outbox" / "other-user.json"
    status_file = tmp_path / "status" / "other-user.json"
    inbox_file.write_text(json.dumps(existing_task))
    outbox_file.write_text(json.dumps({"id": "other-user", "response": '{"message":"old"}'}))
    status_file.write_text(json.dumps({"message": "Installing OpenClaw"}))

    task_id = relay.submit_task(
        "retry openclaw",
        source="telegram",
        metadata={"channel_id": "-100group", "user_id": "222"},
    )

    assert task_id
    assert relay.last_submit_cleanup == []
    assert inbox_file.exists()
    assert outbox_file.exists()
    assert status_file.exists()


def test_relay_submit_task_keeps_other_signal_users_in_same_channel_isolated(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    existing_task = {
        "id": "other-signal-user",
        "prompt": "install openclaw",
        "source": "signal",
        "timestamp": 123.0,
        "metadata": {"channel_id": "group-1", "user_id": "+15550001111"},
    }
    inbox_file = tmp_path / "inbox" / "other-signal-user.json"
    outbox_file = tmp_path / "outbox" / "other-signal-user.json"
    status_file = tmp_path / "status" / "other-signal-user.json"
    inbox_file.write_text(json.dumps(existing_task))
    outbox_file.write_text(json.dumps({"id": "other-signal-user", "response": '{"message":"old"}'}))
    status_file.write_text(json.dumps({"message": "Installing OpenClaw"}))

    task_id = relay.submit_task(
        "retry openclaw",
        source="signal",
        metadata={"channel_id": "group-1", "user_id": "+15550002222"},
    )

    assert task_id
    assert relay.last_submit_cleanup == []
    assert inbox_file.exists()
    assert outbox_file.exists()
    assert status_file.exists()


def test_format_queue_cleanup_note_summarizes_entries():
    note = format_queue_cleanup_note(["inbox:old.json", "status:old.json"])
    assert note == "Cleaned stale queue files before retrying: inbox:old.json, status:old.json."


def test_format_progress_note_compacts_status():
    note = format_progress_note(" Starting   OpenClaw   services \n now ")
    assert note == "Progress: Starting OpenClaw services now"


def test_format_progress_note_redacts_secret_like_values():
    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    leaked_gemini = "AIzaSyAbCdEfGhIjKlMnOpQrStUvWxYz0123456"
    note = format_progress_note(
        f"setting provider key {leaked_key} and telegram token {leaked_token} and gemini key {leaked_gemini}"
    )
    assert "[REDACTED_OPENAI_KEY]" in note
    assert "[REDACTED_TELEGRAM_TOKEN]" in note
    assert "[REDACTED_GEMINI_KEY]" in note
    assert leaked_key not in note
    assert leaked_token not in note
    assert leaked_gemini not in note


def test_build_local_cli_task_metadata_uses_tty_name(monkeypatch):
    monkeypatch.setattr("armorer.messaging.bridge.sys.stdin", MagicMock(isatty=lambda: True, fileno=lambda: 0))
    monkeypatch.setattr("armorer.messaging.bridge.os.ttyname", lambda _fd: "/dev/ttys007")

    metadata = build_local_cli_task_metadata()

    assert metadata == {"channel_id": "tty:ttys007"}


def test_build_local_cli_task_metadata_falls_back_to_process_id(monkeypatch):
    monkeypatch.setattr("armorer.messaging.bridge.sys.stdin", MagicMock(isatty=lambda: False))
    monkeypatch.setattr("armorer.messaging.bridge.os.getpid", lambda: 4321)

    metadata = build_local_cli_task_metadata()

    assert metadata == {"channel_id": "pid:4321"}


def test_relay_submit_task_adds_signature_when_configured(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path, signing_key="test-signing-key")
    task_id = relay.submit_task("restart openclaw", source="telegram", metadata={"channel_id": "7"})
    task_data = json.loads((tmp_path / "inbox" / f"{task_id}.json").read_text())
    assert "signature" in task_data
    assert len(task_data["signature"]) == 64


def test_relay_timeout_message_redacts_secret_like_last_status(tmp_path: Path):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    task_id = relay.submit_task("install openclaw", source="cli")
    status_file = tmp_path / "status" / f"{task_id}.json"
    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    leaked_gemini = "AIzaSyAbCdEfGhIjKlMnOpQrStUvWxYz0123456"
    status_file.write_text(json.dumps({"message": f"setting keys {leaked_key} {leaked_token} {leaked_gemini}"}))

    result = relay.wait_for_response(task_id, timeout=0.02, poll_interval=0.01)

    assert result["timed_out"] is True
    message = str(result.get("message") or "")
    assert "Last progress:" in message
    assert "[REDACTED_OPENAI_KEY]" in message
    assert "[REDACTED_TELEGRAM_TOKEN]" in message
    assert "[REDACTED_GEMINI_KEY]" in message
    assert leaked_key not in message
    assert leaked_token not in message
    assert leaked_gemini not in message


def test_relay_submit_task_publishes_inbox_file_atomically(tmp_path: Path, monkeypatch):
    relay = InboxOutboxRelay(config_dir=tmp_path)
    seen: dict[str, object] = {}
    original_replace = Path.replace

    def _replace(self: Path, target):
        seen["source_name"] = self.name
        seen["target_name"] = Path(target).name
        seen["payload"] = json.loads(self.read_text())
        return original_replace(self, target)

    monkeypatch.setattr(Path, "replace", _replace, raising=True)

    task_id = relay.submit_task("install openclaw", source="cli")

    assert seen["source_name"] == f".{task_id}.json.tmp"
    assert seen["target_name"] == f"{task_id}.json"
    assert seen["payload"]["prompt"] == "install openclaw"
    assert not (tmp_path / "inbox" / f".{task_id}.json.tmp").exists()


def test_telegram_core_bridge_process_update():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()

    event = MagicMock()
    event.text = "check status"
    event.event_id = "1001"
    event.channel_id = "777"
    event.user_id = "9911"
    event.message_id = "42"
    gateway.normalize_event.return_value = event
    gateway.send_chat_action.return_value = {"ok": True}

    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {"id": "task-1", "message": "all green", "raw": {}}
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_update({"update_id": 1001})
    assert out == {"ok": True}
    relay.submit_task.assert_called_once()
    assert gateway.send_chat_action.called
    gateway.send_message.assert_called_once_with("777", "all green", reply_to="42")


def test_telegram_core_bridge_forwards_progress_updates():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    gateway.send_chat_action.return_value = {"ok": True}

    def _fake_wait_for_response(task_id, timeout, status_callback, include_ui_hints):
        assert task_id == "task-1"
        assert include_ui_hints is True
        status_callback("Starting OpenClaw services")
        return {"id": "task-1", "message": "all green", "raw": {}}

    relay.wait_for_response.side_effect = _fake_wait_for_response
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="check status", reply_to="42")

    assert out == {"ok": True}
    assert gateway.send_message.call_args_list[0].args == ("777", "Progress: Starting OpenClaw services")
    assert gateway.send_message.call_args_list[0].kwargs == {"reply_to": "42"}
    assert gateway.send_message.call_args_list[1].args == ("777", "all green")
    assert gateway.send_message.call_args_list[1].kwargs == {"reply_to": "42"}


def test_telegram_core_bridge_forwards_final_openclaw_verification_message():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": (
            "OpenClaw is installed and verified\n"
            "OpenClaw status: healthy\n"
            "Final status check passed via `armorer run openclaw status`."
        ),
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="install openclaw", reply_to="42")

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "777",
        "OpenClaw is installed and verified\nOpenClaw status: healthy\nFinal status check passed via `armorer run openclaw status`.",
        reply_to="42",
    )


def test_telegram_core_bridge_forwards_openclaw_rollback_failure_message():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    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": rollback_message,
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="install openclaw", reply_to="42")

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "777",
        rollback_message,
        reply_to="42",
    )


def test_telegram_core_bridge_forwards_openclaw_generic_failure_message():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    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": failure_message,
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="install openclaw", reply_to="42")

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with("777", failure_message, reply_to="42")


def test_telegram_core_bridge_forwards_openclaw_registry_only_rollback_message():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    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": rollback_message,
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="install openclaw", reply_to="42")

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with("777", rollback_message, reply_to="42")


def test_telegram_core_bridge_forwards_openclaw_rollback_warning_message():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    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": rollback_message,
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="install openclaw", reply_to="42")

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with("777", rollback_message, reply_to="42")


def test_telegram_core_bridge_forwards_openclaw_fresh_install_start_rollback_message():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    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": rollback_message,
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="install openclaw", reply_to="42")

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with("777", rollback_message, reply_to="42")


def test_telegram_core_bridge_forwards_openclaw_fresh_install_start_rollback_warning_message():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    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": rollback_message,
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="install openclaw", reply_to="42")

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with("777", rollback_message, reply_to="42")


def test_telegram_core_bridge_reports_queue_cleanup_before_waiting():
    gateway = MagicMock()
    relay = MagicMock()
    relay.last_submit_cleanup = ["inbox:old.json", "status:old.json"]
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {"id": "task-1", "message": "all green", "raw": {}}
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="check status", reply_to="42")

    assert out == {"ok": True}
    assert gateway.send_message.call_args_list[0].args == (
        "777",
        "Cleaned stale queue files before retrying: inbox:old.json, status:old.json.",
    )
    assert gateway.send_message.call_args_list[0].kwargs == {"reply_to": "42"}
    assert gateway.send_message.call_args_list[1].args == ("777", "all green")
    assert gateway.send_message.call_args_list[1].kwargs == {"reply_to": "42"}


def test_telegram_core_bridge_surfaces_ui_action_hints_for_remote_chat():
    gateway = MagicMock()
    relay = MagicMock()
    relay.last_submit_cleanup = []
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "Choose OpenClaw provider\nOptions: openai, ollama. Reply `cancel` to abort.",
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="install openclaw", reply_to="42")

    assert out == {"ok": True}
    relay.wait_for_response.assert_called_once_with(
        "task-1",
        timeout=30,
        status_callback=relay.wait_for_response.call_args.kwargs["status_callback"],
        include_ui_hints=True,
    )
    gateway.send_message.assert_called_once_with(
        "777",
        "Choose OpenClaw provider\nOptions: openai, ollama. Reply `cancel` to abort.",
        reply_to="42",
    )


def test_telegram_core_bridge_adds_select_buttons_for_remote_ui_action():
    gateway = MagicMock()
    relay = MagicMock()
    relay.last_submit_cleanup = []
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "Choose OpenClaw provider\nOptions: openai, ollama. Reply `cancel` to abort.",
        "raw": {
            "response": '{"message":"Choose OpenClaw provider","ui_action":{"type":"select","options":["openai","ollama"]}}'
        },
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="install openclaw", reply_to="42")

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "777",
        "Choose OpenClaw provider\nOptions: openai, ollama. Reply `cancel` to abort.",
        reply_to="42",
        reply_markup={
            "inline_keyboard": [
                [
                    {"text": "openai", "callback_data": "ui|select|openai"},
                    {"text": "ollama", "callback_data": "ui|select|ollama"},
                ],
                [
                    {"text": "Cancel", "callback_data": "ui|cancel|cancel"},
                ],
            ]
        },
    )


def test_telegram_core_bridge_omits_select_buttons_when_option_contains_secret_like_value():
    gateway = MagicMock()
    relay = MagicMock()
    relay.last_submit_cleanup = []
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    leaked_token = "123456:abcDEF_token_sample_value"
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "Choose OpenClaw token\nOptions: [REDACTED_TELEGRAM_TOKEN], openai. Reply `cancel` to abort.",
        "raw": {
            "response": json.dumps(
                {"message": "Choose OpenClaw token", "ui_action": {"type": "select", "options": [leaked_token, "openai"]}}
            )
        },
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="install openclaw", reply_to="42")

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "777",
        "Choose OpenClaw token\nOptions: [REDACTED_TELEGRAM_TOKEN], openai. Reply `cancel` to abort.",
        reply_to="42",
    )


def test_telegram_core_bridge_adds_confirm_buttons_for_remote_ui_action():
    gateway = MagicMock()
    relay = MagicMock()
    relay.last_submit_cleanup = []
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "Install and configure OpenClaw now?\nReply `yes` or `no`. Reply `cancel` to abort.",
        "raw": {
            "response": '{"message":"Install and configure OpenClaw now?","requires_confirmation":true}'
        },
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(channel_id="777", user_id="9911", prompt="install openclaw", reply_to="42")

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "777",
        "Install and configure OpenClaw now?\nReply `yes` or `no`. Reply `cancel` to abort.",
        reply_to="42",
        reply_markup={
            "inline_keyboard": [
                [
                    {"text": "Yes", "callback_data": "ui|confirm|yes"},
                    {"text": "No", "callback_data": "ui|confirm|no"},
                ],
                [
                    {"text": "Cancel", "callback_data": "ui|cancel|cancel"},
                ],
            ]
        },
    )


def test_telegram_core_bridge_reports_submit_failure_to_user():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()

    event = MagicMock()
    event.text = "check status"
    event.event_id = "1001"
    event.channel_id = "777"
    event.user_id = "9911"
    event.message_id = "42"
    gateway.normalize_event.return_value = event
    relay.submit_task.side_effect = RuntimeError("submit failed")
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_update({"update_id": 1001})

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with("777", "Failed to contact Armorer Core: submit failed", reply_to="42")


def test_telegram_core_bridge_reports_submit_failure_redacts_secret_like_error_detail():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()

    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    event = MagicMock()
    event.text = "check status"
    event.event_id = "1001"
    event.channel_id = "777"
    event.user_id = "9911"
    event.message_id = "42"
    gateway.normalize_event.return_value = event
    relay.submit_task.side_effect = RuntimeError(f"submit failed {leaked_key} {leaked_token}")
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_update({"update_id": 1001})

    assert out == {"ok": True}
    args, kwargs = gateway.send_message.call_args
    assert args[0] == "777"
    assert "Failed to contact Armorer Core:" in args[1]
    assert "[REDACTED_OPENAI_KEY]" in args[1]
    assert "[REDACTED_TELEGRAM_TOKEN]" in args[1]
    assert leaked_key not in args[1]
    assert leaked_token not in args[1]
    assert kwargs == {"reply_to": "42"}


def test_telegram_core_bridge_reports_wait_failure_to_user():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()

    event = MagicMock()
    event.text = "check status"
    event.event_id = "1001"
    event.channel_id = "777"
    event.user_id = "9911"
    event.message_id = "42"
    gateway.normalize_event.return_value = event
    gateway.send_chat_action.return_value = {"ok": True}
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.side_effect = RuntimeError("wait failed")
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_update({"update_id": 1001})

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with("777", "Failed to contact Armorer Core: wait failed", reply_to="42")


def test_telegram_core_bridge_callback_reports_submit_failure_to_user():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.side_effect = RuntimeError("submit failed")
    gateway.send_message.return_value = {"ok": True}

    update = {
        "update_id": 99,
        "callback_query": {
            "id": "cb-1",
            "data": "guard|agent-scan|openclaw",
            "from": {"id": 111},
            "message": {"chat": {"id": 222}, "message_id": 7},
        },
    }
    out = bridge.process_update(update)

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with("222", "Failed to contact Armorer Core: submit failed", reply_to="7")


def test_telegram_core_bridge_callback_submits_ui_selection_prompt():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {"id": "task-1", "message": "ok", "raw": {}}
    gateway.send_message.return_value = {"ok": True}
    gateway.answer_callback_query.return_value = {"ok": True}

    update = {
        "update_id": 99,
        "callback_query": {
            "id": "cb-1",
            "data": "ui|select|openai",
            "from": {"id": 111},
            "message": {"chat": {"id": 222}, "message_id": 7},
        },
    }
    out = bridge.process_update(update)

    assert out == {"ok": True}
    gateway.answer_callback_query.assert_called_once()
    relay.submit_task.assert_called_once_with(
        prompt="UI selection: openai",
        source="telegram",
        metadata={
            "provider": "telegram",
            "channel_id": "222",
            "user_id": "111",
            "message_id": "7",
        },
    )


def test_telegram_core_bridge_callback_submits_ui_confirm_prompt():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {"id": "task-1", "message": "ok", "raw": {}}
    gateway.send_message.return_value = {"ok": True}
    gateway.answer_callback_query.return_value = {"ok": True}

    update = {
        "update_id": 99,
        "callback_query": {
            "id": "cb-1",
            "data": "ui|confirm|yes",
            "from": {"id": 111},
            "message": {"chat": {"id": 222}, "message_id": 7},
        },
    }
    out = bridge.process_update(update)

    assert out == {"ok": True}
    gateway.answer_callback_query.assert_called_once()
    relay.submit_task.assert_called_once_with(
        prompt="UI confirmation: yes",
        source="telegram",
        metadata={
            "provider": "telegram",
            "channel_id": "222",
            "user_id": "111",
            "message_id": "7",
        },
    )


def test_telegram_core_bridge_callback_submits_ui_cancel_prompt():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {"id": "task-1", "message": "cancelled", "raw": {}}
    gateway.send_message.return_value = {"ok": True}
    gateway.answer_callback_query.return_value = {"ok": True}

    update = {
        "update_id": 99,
        "callback_query": {
            "id": "cb-1",
            "data": "ui|cancel|cancel",
            "from": {"id": 111},
            "message": {"chat": {"id": 222}, "message_id": 7},
        },
    }
    out = bridge.process_update(update)

    assert out == {"ok": True}
    gateway.answer_callback_query.assert_called_once()
    relay.submit_task.assert_called_once_with(
        prompt="cancel",
        source="telegram",
        metadata={
            "provider": "telegram",
            "channel_id": "222",
            "user_id": "111",
            "message_id": "7",
        },
    )


def test_telegram_core_bridge_replaces_stale_pending_confirmation_for_same_chat(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = TelegramCoreBridge(
        gateway=gateway,
        relay=relay,
        response_timeout=30,
        state_file=tmp_path / "telegram_gateway_state.json",
    )
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    bridge._pending_confirmations = {
        "deadbeef": {
            "channel_id": "222",
            "user_id": "111",
            "prompt": "please shutdown openclaw",
            "created_at": 100.0,
            "expires_at": time.time() + 60,
        }
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._issue_confirmation(
        channel_id="222",
        user_id="111",
        prompt="please install openclaw",
        reply_to="7",
    )

    assert out == {"ok": True}
    assert list(bridge._pending_confirmations) != ["deadbeef"]
    assert len(bridge._pending_confirmations) == 1
    token = next(iter(bridge._pending_confirmations))
    pending = bridge._pending_confirmations[token]
    assert pending["prompt"] == "please install openclaw"
    gateway.send_message.assert_called_once_with(
        "222",
        "High-risk action detected.\n"
        "Reply `confirm` or `cancel` within 60s to act on this request.\n"
        f"Explicit approval also works: `/confirm {token}`.",
        reply_to="7",
        reply_markup={
            "inline_keyboard": [[
                {"text": "Confirm", "callback_data": f"confirm|{token}|ok"},
                {"text": "Cancel", "callback_data": f"confirm|{token}|cancel"},
            ]]
        },
    )


def test_telegram_core_bridge_callback_confirm_cancel_clears_matching_token(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = TelegramCoreBridge(
        gateway=gateway,
        relay=relay,
        response_timeout=30,
        state_file=tmp_path / "telegram_gateway_state.json",
    )
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    bridge._pending_confirmations = {
        "deadbeef": {
            "channel_id": "222",
            "user_id": "111",
            "prompt": "install openclaw",
            "created_at": 100.0,
            "expires_at": time.time() + 60,
        }
    }
    gateway.send_message.return_value = {"ok": True}
    gateway.answer_callback_query.return_value = {"ok": True}

    update = {
        "update_id": 99,
        "callback_query": {
            "id": "cb-1",
            "data": "confirm|deadbeef|cancel",
            "from": {"id": 111},
            "message": {"chat": {"id": 222}, "message_id": 7},
        },
    }
    out = bridge.process_update(update)

    assert out == {"ok": True}
    gateway.answer_callback_query.assert_called_once()
    relay.submit_task.assert_not_called()
    gateway.send_message.assert_called_once_with("222", "Action cancelled.", reply_to="7")
    assert bridge._pending_confirmations == {}


def test_telegram_core_bridge_plain_confirm_submits_latest_pending_request(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = TelegramCoreBridge(
        gateway=gateway,
        relay=relay,
        response_timeout=30,
        state_file=tmp_path / "telegram_gateway_state.json",
    )
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    bridge._pending_confirmations = {
        "deadbeef": {
            "channel_id": "222",
            "user_id": "111",
            "prompt": "install openclaw",
            "created_at": 100.0,
            "expires_at": time.time() + 60,
        }
    }
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {"id": "task-1", "message": "ok", "raw": {}}
    gateway.send_message.return_value = {"ok": True}

    event = MagicMock()
    event.text = "confirm"
    event.channel_id = "222"
    event.user_id = "111"
    event.message_id = "7"
    gateway.normalize_event.return_value = event

    out = bridge.process_update({"update_id": 1001, "message": {}})

    assert out == {"ok": True}
    relay.submit_task.assert_called_once_with(
        prompt="install openclaw",
        source="telegram",
        metadata={
            "provider": "telegram",
            "channel_id": "222",
            "user_id": "111",
            "message_id": "7",
        },
    )
    gateway.send_message.assert_called_once_with("222", "ok", reply_to="7")
    assert bridge._pending_confirmations == {}


def test_telegram_core_bridge_plain_cancel_clears_latest_pending_request(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = TelegramCoreBridge(
        gateway=gateway,
        relay=relay,
        response_timeout=30,
        state_file=tmp_path / "telegram_gateway_state.json",
    )
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    bridge._pending_confirmations = {
        "deadbeef": {
            "channel_id": "222",
            "user_id": "111",
            "prompt": "install openclaw",
            "created_at": 100.0,
            "expires_at": time.time() + 60,
        }
    }
    gateway.send_message.return_value = {"ok": True}

    event = MagicMock()
    event.text = "cancel"
    event.channel_id = "222"
    event.user_id = "111"
    event.message_id = "7"
    gateway.normalize_event.return_value = event

    out = bridge.process_update({"update_id": 1001, "message": {}})

    assert out == {"ok": True}
    relay.submit_task.assert_not_called()
    gateway.send_message.assert_called_once_with("222", "Action cancelled.", reply_to="7")
    assert bridge._pending_confirmations == {}


def test_telegram_core_bridge_plain_confirm_requires_explicit_token_when_multiple_pending_exist(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = TelegramCoreBridge(
        gateway=gateway,
        relay=relay,
        response_timeout=30,
        state_file=tmp_path / "telegram_gateway_state.json",
    )
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    bridge._pending_confirmations = {
        "deadbeef": {
            "channel_id": "222",
            "user_id": "111",
            "prompt": "install openclaw",
            "created_at": 100.0,
            "expires_at": time.time() + 60,
        },
        "feedface": {
            "channel_id": "222",
            "user_id": "111",
            "prompt": "restart openclaw",
            "created_at": 200.0,
            "expires_at": time.time() + 60,
        },
    }
    gateway.send_message.return_value = {"ok": True}

    event = MagicMock()
    event.text = "confirm"
    event.channel_id = "222"
    event.user_id = "111"
    event.message_id = "7"
    gateway.normalize_event.return_value = event

    out = bridge.process_update({"update_id": 1001, "message": {}})

    assert out == {"ok": True}
    relay.submit_task.assert_not_called()
    gateway.send_message.assert_called_once_with(
        "222",
        "Multiple pending confirmations found. Use `/confirm <token>` from the latest prompt.",
        reply_to="7",
    )
    assert set(bridge._pending_confirmations) == {"deadbeef", "feedface"}


def test_telegram_core_bridge_callback_confirm_cancel_rejects_mismatched_user(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = TelegramCoreBridge(
        gateway=gateway,
        relay=relay,
        response_timeout=30,
        state_file=tmp_path / "telegram_gateway_state.json",
    )
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    bridge._pending_confirmations = {
        "deadbeef": {
            "channel_id": "222",
            "user_id": "111",
            "prompt": "install openclaw",
            "created_at": 100.0,
            "expires_at": time.time() + 60,
        }
    }
    gateway.send_message.return_value = {"ok": True}
    gateway.answer_callback_query.return_value = {"ok": True}

    update = {
        "update_id": 99,
        "callback_query": {
            "id": "cb-1",
            "data": "confirm|deadbeef|cancel",
            "from": {"id": 999},
            "message": {"chat": {"id": 222}, "message_id": 7},
        },
    }
    out = bridge.process_update(update)

    assert out == {"ok": True}
    gateway.answer_callback_query.assert_called_once()
    relay.submit_task.assert_not_called()
    gateway.send_message.assert_called_once_with("222", "Token does not match this user/chat.", reply_to="7")
    assert "deadbeef" in bridge._pending_confirmations


def test_telegram_core_bridge_run_once_advances_offset(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(
        gateway=gateway,
        relay=relay,
        response_timeout=30,
        state_file=tmp_path / "telegram_gateway_state.json",
    )
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()

    gateway.get_updates.return_value = [{"update_id": 1001}, {"update_id": 1002}]
    bridge.process_update = MagicMock(return_value={"ok": True})  # type: ignore[method-assign]

    processed = bridge.run_once(poll_timeout=5)
    assert processed == 2
    assert bridge.next_offset == 1003
    gateway.get_updates.assert_called_once()


def test_telegram_core_bridge_run_once_replies_on_unexpected_processing_error(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(
        gateway=gateway,
        relay=relay,
        response_timeout=30,
        state_file=tmp_path / "telegram_gateway_state.json",
    )
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()

    update = {
        "update_id": 100001,
        "message": {
            "message_id": 42,
            "date": 1739646000,
            "text": "hello from telegram",
            "chat": {"id": 777123, "type": "private"},
            "from": {"id": 9911, "username": "alice", "first_name": "Alice", "last_name": "Walker"},
        },
    }
    gateway.get_updates.return_value = [update]
    bridge.process_update = MagicMock(side_effect=RuntimeError("boom"))  # type: ignore[method-assign]
    gateway.normalize_event.return_value = MagicMock(channel_id="777123", message_id="42")
    gateway.send_message.return_value = {"ok": True}

    processed = bridge.run_once(poll_timeout=5)

    assert processed == 0
    gateway.send_message.assert_called_once_with(
        "777123",
        "Armorer hit an internal error while processing this request: boom",
        reply_to="42",
    )
    assert bridge.next_offset == 100002


def test_telegram_core_bridge_run_once_redacts_secret_like_unexpected_processing_error(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(
        gateway=gateway,
        relay=relay,
        response_timeout=30,
        state_file=tmp_path / "telegram_gateway_state.json",
    )
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()

    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    update = {
        "update_id": 100001,
        "message": {
            "message_id": 42,
            "date": 1739646000,
            "text": "hello from telegram",
            "chat": {"id": 777123, "type": "private"},
            "from": {"id": 9911},
        },
    }
    gateway.get_updates.return_value = [update]
    bridge.process_update = MagicMock(side_effect=RuntimeError(f"boom {leaked_key} {leaked_token}"))  # type: ignore[method-assign]
    gateway.normalize_event.return_value = MagicMock(channel_id="777123", message_id="42")
    gateway.send_message.return_value = {"ok": True}

    processed = bridge.run_once(poll_timeout=5)

    assert processed == 0
    args, kwargs = gateway.send_message.call_args
    assert args[0] == "777123"
    assert "Armorer hit an internal error while processing this request:" in args[1]
    assert "[REDACTED_OPENAI_KEY]" in args[1]
    assert "[REDACTED_TELEGRAM_TOKEN]" in args[1]
    assert leaked_key not in args[1]
    assert leaked_token not in args[1]
    assert kwargs == {"reply_to": "42"}
    assert bridge.next_offset == 100002


def test_telegram_core_bridge_run_once_recovers_when_normalize_event_also_fails(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(
        gateway=gateway,
        relay=relay,
        response_timeout=30,
        state_file=tmp_path / "telegram_gateway_state.json",
    )
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()

    update = {
        "update_id": 100001,
        "message": {
            "message_id": 42,
            "date": 1739646000,
            "text": "hello from telegram",
            "chat": {"id": 777123, "type": "private"},
            "from": {"id": 9911},
        },
    }
    gateway.get_updates.return_value = [update]
    bridge.process_update = MagicMock(side_effect=RuntimeError("boom"))  # type: ignore[method-assign]
    gateway.normalize_event.side_effect = RuntimeError("normalize failed")
    gateway.send_message.return_value = {"ok": True}

    processed = bridge.run_once(poll_timeout=5)

    assert processed == 0
    gateway.send_message.assert_called_once_with(
        "777123",
        "Armorer hit an internal error while processing this request: boom",
        reply_to="42",
    )
    assert bridge.next_offset == 100002


def test_telegram_core_bridge_run_once_threads_callback_processing_failure_reply(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(
        gateway=gateway,
        relay=relay,
        response_timeout=30,
        state_file=tmp_path / "telegram_gateway_state.json",
    )
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()

    update = {
        "update_id": 100002,
        "callback_query": {
            "id": "cb-1",
            "data": "ui|select|openai",
            "from": {"id": 9911},
            "message": {"chat": {"id": 777123, "type": "private"}, "message_id": 42},
        },
    }
    gateway.get_updates.return_value = [update]
    bridge.process_update = MagicMock(side_effect=RuntimeError("boom"))  # type: ignore[method-assign]
    gateway.send_message.return_value = {"ok": True}

    processed = bridge.run_once(poll_timeout=5)

    assert processed == 0
    gateway.send_message.assert_called_once_with(
        "777123",
        "Armorer hit an internal error while processing this request: boom",
        reply_to="42",
    )
    assert bridge.next_offset == 100003


def test_telegram_core_bridge_persists_offset_and_dedup(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    bridge.process_update = MagicMock(return_value={"ok": True})  # type: ignore[method-assign]

    gateway.get_updates.return_value = [{"update_id": 2001}, {"update_id": 2001}]
    processed = bridge.run_once(poll_timeout=5)
    assert processed == 1
    assert bridge.next_offset == 2002

    state_file = tmp_path / "telegram_gateway_state.json"
    assert state_file.exists()

    gateway2 = MagicMock()
    relay2 = MagicMock()
    relay2.config_dir = tmp_path
    bridge2 = TelegramCoreBridge(gateway=gateway2, relay=relay2, response_timeout=30)
    assert bridge2.next_offset == 2002


def test_telegram_core_bridge_callback_action_routes_to_core():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()
    relay.submit_task.return_value = "task-cb"
    relay.wait_for_response.return_value = {"id": "task-cb", "message": "Applied action.", "raw": {}}
    gateway.send_message.return_value = {"ok": True}

    update = {
        "update_id": 99,
        "callback_query": {
            "id": "cb-1",
            "data": "guard|agent-scan|openclaw",
            "from": {"id": 111},
            "message": {"chat": {"id": 222}, "message_id": 7},
        },
    }
    out = bridge.process_update(update)
    assert out == {"ok": True}
    gateway.answer_callback_query.assert_called_once()
    relay.submit_task.assert_called_once()


def test_telegram_core_bridge_blocks_non_admin():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._admin_user_ids = {"123"}
    bridge._admin_chat_ids = {"123"}

    event = MagicMock()
    event.text = "status"
    event.event_id = "1001"
    event.channel_id = "777"
    event.user_id = "9911"
    event.message_id = "42"
    gateway.normalize_event.return_value = event
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_update({"update_id": 1001})
    assert out == {"ok": True}
    relay.submit_task.assert_not_called()


def test_telegram_core_bridge_does_not_treat_alert_chat_as_admin(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge.config = MagicMock()
    bridge.config.get_key.side_effect = lambda key: {
        "TELEGRAM_ALERT_CHAT_ID": "-100alerts",
        "ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS": "false",
    }.get(key)

    bridge._load_security_policy()

    assert "-100alerts" not in bridge._admin_chat_ids
    assert bridge._is_admin("u1", "-100alerts") is False


def test_telegram_core_bridge_requires_confirmation_for_dangerous_prompt():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()

    event = MagicMock()
    event.text = "please shutdown openclaw"
    event.event_id = "1001"
    event.channel_id = "777"
    event.user_id = "9911"
    event.message_id = "42"
    gateway.normalize_event.return_value = event
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_update({"update_id": 1001})
    assert out == {"ok": True}
    relay.submit_task.assert_not_called()
    token = next(iter(bridge._pending_confirmations))
    gateway.send_message.assert_called_once_with(
        "777",
        "High-risk action detected.\n"
        "Reply `confirm` or `cancel` within 60s to act on this request.\n"
        f"Explicit approval also works: `/confirm {token}`.",
        reply_to="42",
        reply_markup={
            "inline_keyboard": [[
                {"text": "Confirm", "callback_data": f"confirm|{token}|ok"},
                {"text": "Cancel", "callback_data": f"confirm|{token}|cancel"},
            ]]
        },
    )


def test_signal_core_bridge_process_and_dedup(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True

    event = MagicMock()
    event.text = "hello"
    event.event_id = "evt-1"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171000"
    gateway.normalize_event.return_value = event
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {"id": "task-1", "message": "ok", "raw": {}}
    gateway.send_message.return_value = {"ok": True}

    out1 = bridge.process_event({"envelope": {}})
    out2 = bridge.process_event({"envelope": {}})
    assert out1 == {"ok": True}
    assert out2 is None


def test_signal_core_bridge_forwards_progress_updates(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    relay.submit_task.return_value = "task-1"

    def _fake_wait_for_response(task_id, timeout, status_callback, include_ui_hints):
        assert task_id == "task-1"
        assert include_ui_hints is True
        status_callback("Configuring OpenClaw")
        return {"id": "task-1", "message": "ok", "raw": {}}

    relay.wait_for_response.side_effect = _fake_wait_for_response
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(
        channel_id="+15550001111",
        user_id="+15550001111",
        prompt="check status",
        event_id="evt-1",
        message_id="171000",
    )

    assert out == {"ok": True}
    assert gateway.send_message.call_args_list[0].args == ("+15550001111", "Progress: Configuring OpenClaw")
    assert gateway.send_message.call_args_list[0].kwargs == {"reply_to": "171000"}
    assert gateway.send_message.call_args_list[1].args == ("+15550001111", "ok")
    assert gateway.send_message.call_args_list[1].kwargs == {"reply_to": "171000"}


def test_signal_core_bridge_forwards_final_openclaw_verification_message(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": (
            "OpenClaw is installed and verified\n"
            "OpenClaw status: healthy\n"
            "Final status check passed via `armorer run openclaw status`."
        ),
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(
        channel_id="+15550001111",
        user_id="+15550001111",
        prompt="install openclaw",
        event_id="evt-1",
        message_id="171000",
    )

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        "OpenClaw is installed and verified\nOpenClaw status: healthy\nFinal status check passed via `armorer run openclaw status`.",
        reply_to="171000",
    )


def test_signal_core_bridge_forwards_openclaw_rollback_failure_message(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    relay.submit_task.return_value = "task-1"
    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": rollback_message,
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(
        channel_id="+15550001111",
        user_id="+15550001111",
        prompt="install openclaw",
        event_id="evt-1",
        message_id="171000",
    )

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        rollback_message,
        reply_to="171000",
    )


def test_signal_core_bridge_forwards_openclaw_generic_failure_message(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    relay.submit_task.return_value = "task-1"
    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": failure_message,
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(
        channel_id="+15550001111",
        user_id="+15550001111",
        prompt="install openclaw",
        event_id="evt-1",
        message_id="171000",
    )

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        failure_message,
        reply_to="171000",
    )


def test_signal_core_bridge_forwards_openclaw_registry_only_rollback_message(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    relay.submit_task.return_value = "task-1"
    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": rollback_message,
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(
        channel_id="+15550001111",
        user_id="+15550001111",
        prompt="install openclaw",
        event_id="evt-1",
        message_id="171000",
    )

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        rollback_message,
        reply_to="171000",
    )


def test_signal_core_bridge_forwards_openclaw_rollback_warning_message(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    relay.submit_task.return_value = "task-1"
    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": rollback_message,
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(
        channel_id="+15550001111",
        user_id="+15550001111",
        prompt="install openclaw",
        event_id="evt-1",
        message_id="171000",
    )

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        rollback_message,
        reply_to="171000",
    )


def test_signal_core_bridge_forwards_openclaw_fresh_install_start_rollback_message(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    relay.submit_task.return_value = "task-1"
    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": rollback_message, "raw": {}}
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(
        channel_id="+15550001111",
        user_id="+15550001111",
        prompt="install openclaw",
        event_id="evt-1",
        message_id="171000",
    )

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        rollback_message,
        reply_to="171000",
    )


def test_signal_core_bridge_forwards_openclaw_fresh_install_start_rollback_warning_message(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    relay.submit_task.return_value = "task-1"
    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": rollback_message, "raw": {}}
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(
        channel_id="+15550001111",
        user_id="+15550001111",
        prompt="install openclaw",
        event_id="evt-1",
        message_id="171000",
    )

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        rollback_message,
        reply_to="171000",
    )


def test_signal_core_bridge_reports_queue_cleanup_before_waiting(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    relay.last_submit_cleanup = ["inbox:old.json", "status:old.json"]
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {"id": "task-1", "message": "ok", "raw": {}}
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(
        channel_id="+15550001111",
        user_id="+15550001111",
        prompt="check status",
        event_id="evt-1",
        message_id="171000",
    )

    assert out == {"ok": True}
    assert gateway.send_message.call_args_list[0].args == (
        "+15550001111",
        "Cleaned stale queue files before retrying: inbox:old.json, status:old.json.",
    )
    assert gateway.send_message.call_args_list[0].kwargs == {"reply_to": "171000"}
    assert gateway.send_message.call_args_list[1].args == ("+15550001111", "ok")
    assert gateway.send_message.call_args_list[1].kwargs == {"reply_to": "171000"}


def test_signal_core_bridge_surfaces_ui_action_hints_for_remote_chat(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    relay.last_submit_cleanup = []
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {
        "id": "task-1",
        "message": "Install and configure OpenClaw now?\nReply `yes` or `no`. Reply `cancel` to abort.",
        "raw": {},
    }
    gateway.send_message.return_value = {"ok": True}

    out = bridge._submit_to_core(
        channel_id="+15550001111",
        user_id="+15550001111",
        prompt="install openclaw",
        event_id="evt-1",
        message_id="171000",
    )

    assert out == {"ok": True}
    relay.wait_for_response.assert_called_once_with(
        "task-1",
        timeout=30,
        status_callback=relay.wait_for_response.call_args.kwargs["status_callback"],
        include_ui_hints=True,
    )
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        "Install and configure OpenClaw now?\nReply `yes` or `no`. Reply `cancel` to abort.",
        reply_to="171000",
    )


def test_signal_core_bridge_run_once_replies_on_unexpected_processing_error(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge.process_event = MagicMock(side_effect=RuntimeError("boom"))  # type: ignore[method-assign]

    raw_event = {
        "envelope": {
            "source": "+15550001111",
            "sourceNumber": "+15550001111",
            "timestamp": 1710000000000,
            "dataMessage": {"message": "hello", "timestamp": 1710000000000},
        }
    }
    gateway.receive_messages.return_value = [raw_event]
    gateway.normalize_event.return_value = MagicMock(
        channel_id="+15550001111",
        message_id="1710000000000",
        event_id="evt-1",
    )
    gateway.send_message.return_value = {"ok": True}

    processed = bridge.run_once(receive_timeout=1, max_messages=10)

    assert processed == 0
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        "Armorer hit an internal error while processing this request: boom",
        reply_to="1710000000000",
    )


def test_signal_core_bridge_run_once_recovers_when_normalize_event_also_fails(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge.process_event = MagicMock(side_effect=RuntimeError("boom"))  # type: ignore[method-assign]

    raw_event = {
        "envelope": {
            "source": "+15550001111",
            "sourceNumber": "+15550001111",
            "timestamp": 1710000000000,
            "dataMessage": {"message": "hello", "timestamp": 1710000000000},
        }
    }
    gateway.receive_messages.return_value = [raw_event]
    gateway.normalize_event.side_effect = RuntimeError("normalize failed")
    gateway.send_message.return_value = {"ok": True}

    processed = bridge.run_once(receive_timeout=1, max_messages=10)

    assert processed == 0
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        "Armorer hit an internal error while processing this request: boom",
        reply_to="1710000000000",
    )
    assert "1710000000000" in bridge._recent_event_ids


def test_signal_core_bridge_run_once_redacts_secret_like_unexpected_processing_error(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True

    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    bridge.process_event = MagicMock(side_effect=RuntimeError(f"boom {leaked_key} {leaked_token}"))  # type: ignore[method-assign]

    raw_event = {
        "envelope": {
            "source": "+15550001111",
            "sourceNumber": "+15550001111",
            "timestamp": 1710000000000,
            "dataMessage": {"message": "hello", "timestamp": 1710000000000},
        }
    }
    gateway.receive_messages.return_value = [raw_event]
    gateway.normalize_event.return_value = MagicMock(
        channel_id="+15550001111",
        message_id="1710000000000",
        event_id="evt-1",
    )
    gateway.send_message.return_value = {"ok": True}

    processed = bridge.run_once(receive_timeout=1, max_messages=10)

    assert processed == 0
    args, kwargs = gateway.send_message.call_args
    assert args[0] == "+15550001111"
    assert "Armorer hit an internal error while processing this request:" in args[1]
    assert "[REDACTED_OPENAI_KEY]" in args[1]
    assert "[REDACTED_TELEGRAM_TOKEN]" in args[1]
    assert leaked_key not in args[1]
    assert leaked_token not in args[1]
    assert kwargs == {"reply_to": "1710000000000"}


def test_signal_core_bridge_reports_wait_failure_to_user(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True

    event = MagicMock()
    event.text = "hello"
    event.event_id = "evt-1"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171000"
    gateway.normalize_event.return_value = event
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.side_effect = RuntimeError("wait failed")
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_event({"envelope": {}})

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        "Failed to contact Armorer Core: wait failed",
        reply_to="171000",
    )


def test_signal_core_bridge_reports_wait_failure_redacts_secret_like_error_detail(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True

    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    event = MagicMock()
    event.text = "hello"
    event.event_id = "evt-1"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171000"
    gateway.normalize_event.return_value = event
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.side_effect = RuntimeError(f"wait failed {leaked_key} {leaked_token}")
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_event({"envelope": {}})

    assert out == {"ok": True}
    args, kwargs = gateway.send_message.call_args
    assert args[0] == "+15550001111"
    assert "Failed to contact Armorer Core:" in args[1]
    assert "[REDACTED_OPENAI_KEY]" in args[1]
    assert "[REDACTED_TELEGRAM_TOKEN]" in args[1]
    assert leaked_key not in args[1]
    assert leaked_token not in args[1]
    assert kwargs == {"reply_to": "171000"}


def test_signal_core_bridge_reports_submit_failure_to_user(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True

    event = MagicMock()
    event.text = "hello"
    event.event_id = "evt-1"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171000"
    gateway.normalize_event.return_value = event
    relay.submit_task.side_effect = RuntimeError("submit failed")
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_event({"envelope": {}})

    assert out == {"ok": True}
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        "Failed to contact Armorer Core: submit failed",
        reply_to="171000",
    )


def test_signal_core_bridge_blocks_non_admin(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = False
    bridge._admin_user_ids = {"+15550009999"}
    bridge._admin_chat_ids = set()

    event = MagicMock()
    event.text = "status"
    event.event_id = "evt-1"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171000"
    gateway.normalize_event.return_value = event
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_event({"envelope": {}})
    assert out == {"ok": True}
    relay.submit_task.assert_not_called()
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        (
            "Access denied. Configure ARMORER_SIGNAL_ADMIN_USER_IDS/CHAT_IDS "
            "or explicitly enable ARMORER_SIGNAL_ALLOW_UNCONFIGURED_ADMINS=true."
        ),
        reply_to="171000",
    )


def test_signal_core_bridge_non_admin_reply_dedups_replayed_event(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = False
    bridge._admin_user_ids = {"+15550009999"}
    bridge._admin_chat_ids = set()

    event = MagicMock()
    event.text = "status"
    event.event_id = "evt-1"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171000"
    gateway.normalize_event.return_value = event
    gateway.send_message.return_value = {"ok": True}

    out1 = bridge.process_event({"envelope": {}})
    out2 = bridge.process_event({"envelope": {}})

    assert out1 == {"ok": True}
    assert out2 is None
    relay.submit_task.assert_not_called()
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        (
            "Access denied. Configure ARMORER_SIGNAL_ADMIN_USER_IDS/CHAT_IDS "
            "or explicitly enable ARMORER_SIGNAL_ALLOW_UNCONFIGURED_ADMINS=true."
        ),
        reply_to="171000",
    )
    assert "evt-1" in bridge._recent_event_ids


def test_signal_core_bridge_threads_rate_limit_reply(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._rate_limit_count = 1
    bridge._rate_limit_window_s = 60
    bridge._rate_limit_events["+15550001111"] = deque([time.time()])

    event = MagicMock()
    event.text = "status"
    event.event_id = "evt-1"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171000"
    gateway.normalize_event.return_value = event
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_event({"envelope": {}})

    assert out == {"ok": True}
    relay.submit_task.assert_not_called()
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        "Rate limit exceeded. Max 1 requests/60s.",
        reply_to="171000",
    )


def test_signal_core_bridge_rate_limit_reply_dedups_replayed_event(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._rate_limit_count = 1
    bridge._rate_limit_window_s = 60
    bridge._rate_limit_events["+15550001111"] = deque([time.time()])

    event = MagicMock()
    event.text = "status"
    event.event_id = "evt-1"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171000"
    gateway.normalize_event.return_value = event
    gateway.send_message.return_value = {"ok": True}

    out1 = bridge.process_event({"envelope": {}})
    out2 = bridge.process_event({"envelope": {}})

    assert out1 == {"ok": True}
    assert out2 is None
    relay.submit_task.assert_not_called()
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        "Rate limit exceeded. Max 1 requests/60s.",
        reply_to="171000",
    )
    assert "evt-1" in bridge._recent_event_ids


def test_signal_core_bridge_requires_confirmation_for_dangerous_prompt(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()

    event = MagicMock()
    event.text = "please shutdown openclaw"
    event.event_id = "evt-1"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171000"
    gateway.normalize_event.return_value = event
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_event({"envelope": {}})
    assert out == {"ok": True}
    relay.submit_task.assert_not_called()
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        "High-risk action detected.\n"
        "Reply `confirm` or `cancel` within 60s to act on this request.\n"
        f"Explicit approval also works: `/confirm {next(iter(bridge._pending_confirmations))}`.",
        reply_to="171000",
    )

    assert bridge._pending_confirmations


def test_signal_core_bridge_confirmation_prompt_dedups_replayed_event(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    gateway.send_message.return_value = {"ok": True}

    event = MagicMock()
    event.text = "please shutdown openclaw"
    event.event_id = "evt-1"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171000"
    gateway.normalize_event.return_value = event

    out1 = bridge.process_event({"envelope": {}})
    out2 = bridge.process_event({"envelope": {}})

    assert out1 == {"ok": True}
    assert out2 is None
    relay.submit_task.assert_not_called()
    gateway.send_message.assert_called_once()
    assert len(bridge._pending_confirmations) == 1
    assert "evt-1" in bridge._recent_event_ids


def test_signal_core_bridge_replaces_stale_pending_confirmation_for_same_chat(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._pending_confirmations = {
        "deadbeef": {
            "channel_id": "+15550001111",
            "user_id": "+15550001111",
            "prompt": "please shutdown openclaw",
            "created_at": 100.0,
            "expires_at": time.time() + 60,
        }
    }
    gateway.send_message.return_value = {"ok": True}

    event = MagicMock()
    event.text = "please restart openclaw"
    event.event_id = "evt-2"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171001"
    gateway.normalize_event.return_value = event

    out = bridge.process_event({"envelope": {}})

    assert out == {"ok": True}
    relay.submit_task.assert_not_called()
    assert list(bridge._pending_confirmations) != ["deadbeef"]
    assert len(bridge._pending_confirmations) == 1
    pending = next(iter(bridge._pending_confirmations.values()))
    assert pending["prompt"] == "please restart openclaw"
    gateway.send_message.assert_called_once()
    assert "evt-2" in bridge._recent_event_ids


def test_signal_core_bridge_plain_confirm_submits_latest_pending_request(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._pending_confirmations = {
        "deadbeef": {
            "channel_id": "+15550001111",
            "user_id": "+15550001111",
            "prompt": "please shutdown openclaw",
            "created_at": 100.0,
            "expires_at": time.time() + 60,
        }
    }

    event = MagicMock()
    event.text = "confirm"
    event.event_id = "evt-2"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171001"
    gateway.normalize_event.return_value = event
    relay.submit_task.return_value = "task-1"
    relay.wait_for_response.return_value = {"id": "task-1", "message": "ok", "raw": {}}
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_event({"envelope": {}})

    assert out == {"ok": True}
    relay.submit_task.assert_called_once_with(
        prompt="please shutdown openclaw",
        source="signal",
        metadata={
            "provider": "signal",
            "event_id": "",
            "channel_id": "+15550001111",
            "user_id": "+15550001111",
            "message_id": "171001",
        },
    )
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        "ok",
        reply_to="171001",
    )
    assert bridge._pending_confirmations == {}


def test_signal_core_bridge_plain_cancel_clears_latest_pending_request(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._pending_confirmations = {
        "deadbeef": {
            "channel_id": "+15550001111",
            "user_id": "+15550001111",
            "prompt": "please shutdown openclaw",
            "created_at": 100.0,
            "expires_at": time.time() + 60,
        }
    }

    event = MagicMock()
    event.text = "cancel"
    event.event_id = "evt-2"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171001"
    gateway.normalize_event.return_value = event
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_event({"envelope": {}})

    assert out == {"ok": True}
    relay.submit_task.assert_not_called()
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        "Action cancelled.",
        reply_to="171001",
    )
    assert bridge._pending_confirmations == {}


def test_signal_core_bridge_plain_confirm_requires_explicit_token_when_multiple_pending_exist(tmp_path: Path):
    gateway = MagicMock()
    relay = MagicMock()
    relay.config_dir = tmp_path
    bridge = SignalCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = True
    bridge._pending_confirmations = {
        "deadbeef": {
            "channel_id": "+15550001111",
            "user_id": "+15550001111",
            "prompt": "please shutdown openclaw",
            "created_at": 100.0,
            "expires_at": time.time() + 60,
        },
        "feedface": {
            "channel_id": "+15550001111",
            "user_id": "+15550001111",
            "prompt": "please restart openclaw",
            "created_at": 200.0,
            "expires_at": time.time() + 60,
        },
    }

    event = MagicMock()
    event.text = "confirm"
    event.event_id = "evt-2"
    event.channel_id = "+15550001111"
    event.user_id = "+15550001111"
    event.message_id = "171001"
    gateway.normalize_event.return_value = event
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_event({"envelope": {}})

    assert out == {"ok": True}
    relay.submit_task.assert_not_called()
    gateway.send_message.assert_called_once_with(
        "+15550001111",
        "Multiple pending confirmations found. Use `/confirm <token>` from the latest prompt.",
        reply_to="171001",
    )
    assert set(bridge._pending_confirmations) == {"deadbeef", "feedface"}


def test_telegram_core_bridge_secure_default_blocks_without_allowlist():
    gateway = MagicMock()
    relay = MagicMock()
    bridge = TelegramCoreBridge(gateway=gateway, relay=relay, response_timeout=30)
    bridge._allow_unconfigured_admins = False
    bridge._admin_user_ids = set()
    bridge._admin_chat_ids = set()

    event = MagicMock()
    event.text = "status"
    event.event_id = "1001"
    event.channel_id = "777"
    event.user_id = "9911"
    event.message_id = "42"
    gateway.normalize_event.return_value = event
    gateway.send_message.return_value = {"ok": True}

    out = bridge.process_update({"update_id": 1001})
    assert out == {"ok": True}
    relay.submit_task.assert_not_called()
