import json
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

from typer.testing import CliRunner

from armorer.apps.openclaw import OpenClawInstanceConfig, apply_openclaw_instance_config
from armorer.tools.app_setup import apply_openclaw_config, get_openclaw_setup_context, validate_openclaw_selection
from armorer.main import _configure_openclaw_instance, _prompt_openclaw_pairing_followup, app


runner = CliRunner()


@patch("armorer.main._prepare_openclaw_skills")
@patch("armorer.main._prompt_secret_with_help")
@patch("armorer.main.discover_openclaw_skills")
@patch("armorer.main._ui_confirm")
@patch("armorer.main._ui_checkbox")
@patch("armorer.main._ui_text")
@patch("armorer.main._ui_select")
@patch("armorer.main.ConfigManager")
@patch("armorer.main.load_openclaw_instance_config")
def test_configure_openclaw_instance_confirms_empty_channel_selection(
    mock_load_config,
    mock_config_cls,
    mock_ui_select,
    mock_ui_text,
    mock_ui_checkbox,
    mock_ui_confirm,
    mock_discover_skills,
    mock_prompt_secret,
    mock_prepare_skills,
    tmp_path,
):
    install_dir = tmp_path / "openclaw"
    install_dir.mkdir()
    app_meta = SimpleNamespace(name="openclaw", app_config_state={})
    mock_load_config.return_value = OpenClawInstanceConfig(
        access_mode="local",
        primary_provider="openai",
        default_model="gpt-4o",
        enabled_channels=["telegram"],
        enabled_skills=[],
        trusted_proxies=[],
        skill_settings={},
        web_search_provider="disabled",
    )
    mock_config_cls.return_value = MagicMock()
    mock_ui_select.side_effect = ["openai", "gpt-5.4", "disabled"]
    mock_ui_text.return_value = "gpt-5.4"
    mock_ui_checkbox.side_effect = [[], ["discord"], []]
    mock_ui_confirm.side_effect = [False, True, True]
    mock_discover_skills.return_value = []
    mock_prompt_secret.return_value = None
    mock_prepare_skills.return_value = ([], {}, {})

    config_state, secrets = _configure_openclaw_instance(app_meta, install_dir)

    assert config_state.enabled_channels == ["discord"]
    assert secrets["__ARMORER_BIND_CHANNELS__"] == "true"
    assert mock_ui_checkbox.call_count == 3
    assert mock_ui_confirm.call_args_list[0].args[0] == "You did not select any chat connections. Continue without enabling any channels?"


@patch("armorer.main.approve_openclaw_pairings")
@patch("armorer.main.discover_openclaw_pairing_requests")
@patch("armorer.main._ui_text")
@patch("armorer.main._ui_confirm")
def test_prompt_openclaw_pairing_followup_uses_detected_code_when_bot_reply_is_missing(
    mock_confirm,
    mock_text,
    mock_discover,
    mock_approve,
    tmp_path,
):
    mock_discover.return_value = [
        {
            "id": "350926187",
            "code": "B3KHK7HN",
            "username": "Shot2kill",
        }
    ]
    mock_confirm.return_value = True
    mock_text.return_value = ""
    mock_approve.return_value = {
        "statuses": {"telegram": {"approved": True, "detail": "approved"}},
        "warnings": [],
    }

    result = _prompt_openclaw_pairing_followup(tmp_path, ["telegram"])

    mock_approve.assert_called_once_with(tmp_path, {"telegram": "B3KHK7HN"})
    assert result["statuses"]["telegram"]["approved"] is True


@patch("armorer.apps.openclaw.config_adapter.ensure_openclaw_instance_ports")
def test_apply_openclaw_instance_config_writes_env_and_json(mock_ports, tmp_path):
    (tmp_path / ".env").write_text("OPENCLAW_CONFIG_DIR=./.openclaw\nOPENCLAW_WORKSPACE_DIR=./workspace\n")
    cfg = OpenClawInstanceConfig(
        access_mode="reverse_proxy",
        primary_provider="openai",
        default_model="gpt-4o",
        enabled_channels=["telegram"],
        enabled_skills=["filesystem"],
        trusted_proxies=["10.0.0.2"],
        skill_settings={"filesystem": {"enabled": True, "env": {"ROOT_DIR": "/tmp"}}},
        web_search_provider="brave",
    )

    result = apply_openclaw_instance_config(
        tmp_path,
        cfg,
        secrets_map={
            "OPENAI_API_KEY": "sk-test",
            "TELEGRAM_BOT_TOKEN": "tg-token",
            "BRAVE_API_KEY": "brave-key",
        },
    )

    env_text = (tmp_path / ".env").read_text()
    runtime_env_text = (tmp_path / ".openclaw" / ".env").read_text()
    data = json.loads((tmp_path / ".openclaw" / "openclaw.json").read_text())

    assert "OPENAI_API_KEY=sk-test" in env_text
    assert "TELEGRAM_BOT_TOKEN=tg-token" in env_text
    assert "OPENAI_API_KEY=sk-test" in runtime_env_text
    assert "TELEGRAM_BOT_TOKEN=tg-token" in runtime_env_text
    assert "BRAVE_API_KEY=brave-key" in runtime_env_text
    assert "OPENCLAW_GATEWAY_TOKEN=" in env_text
    assert "OPENCLAW_GATEWAY_BIND=lan" in env_text
    assert "MODEL_PROVIDER=openai" in env_text
    assert "Armorer_MODEL=gpt-4o" in env_text
    assert data["gateway"]["controlUi"]["allowedOrigins"]
    assert data["gateway"]["trustedProxies"] == ["10.0.0.2"]
    assert data["agents"]["defaults"]["model"]["primary"] == "openai/gpt-4o"
    assert data["channels"]["telegram"]["enabled"] is True
    assert data["tools"]["web"]["search"]["enabled"] is True
    assert data["tools"]["web"]["search"]["provider"] == "brave"
    assert data["tools"]["web"]["search"]["apiKey"] == "brave-key"
    assert data["skills"]["entries"]["filesystem"]["enabled"] is True
    assert data["skills"]["entries"]["filesystem"]["env"]["ROOT_DIR"] == "/tmp"
    assert "armorer" not in data
    assert result["channels_enabled"] == ["telegram"]


@patch("armorer.apps.openclaw.config_adapter.ensure_openclaw_instance_ports")
def test_apply_openclaw_instance_config_writes_ollama_runtime_config(mock_ports, tmp_path):
    (tmp_path / ".env").write_text("OPENCLAW_CONFIG_DIR=./.openclaw\nOPENCLAW_WORKSPACE_DIR=./workspace\n")
    cfg = OpenClawInstanceConfig(
        access_mode="local",
        primary_provider="ollama",
        default_model="armorer-qwen3.5-9b",
        enabled_channels=[],
        enabled_skills=[],
        trusted_proxies=[],
        skill_settings={},
        web_search_provider="disabled",
    )

    apply_openclaw_instance_config(
        tmp_path,
        cfg,
        secrets_map={
            "OLLAMA_BASE_URL": "http://localhost:11434/v1/",
        },
    )

    env_text = (tmp_path / ".env").read_text()
    runtime_env_text = (tmp_path / ".openclaw" / ".env").read_text()
    data = json.loads((tmp_path / ".openclaw" / "openclaw.json").read_text())

    assert "MODEL_PROVIDER=ollama" in env_text
    assert "Armorer_MODEL=armorer-qwen3.5-9b" in env_text
    assert "OLLAMA_BASE_URL=http://localhost:11434/v1/" in env_text
    assert "OLLAMA_API_KEY=ollama-local" in env_text
    assert "OLLAMA_BASE_URL=http://localhost:11434/v1/" in runtime_env_text
    assert "OLLAMA_API_KEY=ollama-local" in runtime_env_text
    assert data["agents"]["defaults"]["model"]["primary"] == "ollama/armorer-qwen3.5-9b"
    assert data["models"]["providers"]["ollama"]["baseUrl"] == "http://host.docker.internal:11434"
    assert data["models"]["providers"]["ollama"]["api"] == "ollama"
    assert data["models"]["providers"]["ollama"]["apiKey"] == "ollama-local"
    assert data["models"]["providers"]["ollama"]["models"][0]["id"] == "armorer-qwen3.5-9b"


@patch("armorer.apps.openclaw.config_adapter.ensure_openclaw_instance_ports")
def test_apply_openclaw_instance_config_maps_gemini_to_google_provider(mock_ports, tmp_path):
    (tmp_path / ".env").write_text("OPENCLAW_CONFIG_DIR=./.openclaw\nOPENCLAW_WORKSPACE_DIR=./workspace\n")
    cfg = OpenClawInstanceConfig(
        access_mode="local",
        primary_provider="gemini",
        default_model="gemini-flash-lite-latest",
        enabled_channels=[],
        enabled_skills=[],
        trusted_proxies=[],
        skill_settings={},
        web_search_provider="disabled",
    )

    apply_openclaw_instance_config(
        tmp_path,
        cfg,
        secrets_map={"GEMINI_API_KEY": "gm-test"},
    )

    runtime_env_text = (tmp_path / ".openclaw" / ".env").read_text()
    data = json.loads((tmp_path / ".openclaw" / "openclaw.json").read_text())

    assert data["agents"]["defaults"]["model"]["primary"] == "google/gemini-flash-lite-latest"
    assert "GEMINI_API_KEY=gm-test" in runtime_env_text


@patch("armorer.apps.openclaw.config_adapter.ensure_openclaw_instance_ports")
def test_apply_openclaw_instance_config_removes_stale_runtime_env_for_unselected_managed_keys(mock_ports, tmp_path):
    (tmp_path / ".env").write_text("OPENCLAW_CONFIG_DIR=./.openclaw\nOPENCLAW_WORKSPACE_DIR=./workspace\n")
    (tmp_path / ".openclaw").mkdir()
    (tmp_path / ".openclaw" / ".env").write_text(
        "OPENAI_API_KEY=old-openai\n"
        "TELEGRAM_BOT_TOKEN=old-telegram\n"
        "UNMANAGED_KEY=keep-me\n"
    )
    cfg = OpenClawInstanceConfig(
        access_mode="local",
        primary_provider="gemini",
        default_model="gemini-flash-lite-latest",
        enabled_channels=[],
        enabled_skills=[],
        trusted_proxies=[],
        skill_settings={},
        web_search_provider="disabled",
    )

    apply_openclaw_instance_config(
        tmp_path,
        cfg,
        secrets_map={"GEMINI_API_KEY": "gm-test"},
    )

    runtime_env_text = (tmp_path / ".openclaw" / ".env").read_text()
    assert "GEMINI_API_KEY=gm-test" in runtime_env_text
    assert "OPENAI_API_KEY=" not in runtime_env_text
    assert "TELEGRAM_BOT_TOKEN=" not in runtime_env_text
    assert "UNMANAGED_KEY=keep-me" in runtime_env_text


@patch("armorer.apps.openclaw.config_adapter.ensure_openclaw_instance_ports")
def test_apply_openclaw_instance_config_disables_existing_unselected_channel(mock_ports, tmp_path):
    (tmp_path / ".env").write_text("OPENCLAW_CONFIG_DIR=./.openclaw\nOPENCLAW_WORKSPACE_DIR=./workspace\n")
    (tmp_path / ".openclaw").mkdir()
    (tmp_path / ".openclaw" / "openclaw.json").write_text(
        json.dumps(
            {
                "channels": {
                    "telegram": {"enabled": True},
                    "discord": {"enabled": True},
                }
            }
        )
    )
    cfg = OpenClawInstanceConfig(
        access_mode="local",
        primary_provider="openai",
        default_model="gpt-4o",
        enabled_channels=["telegram"],
        enabled_skills=[],
        trusted_proxies=[],
        skill_settings={},
        web_search_provider="disabled",
    )

    apply_openclaw_instance_config(
        tmp_path,
        cfg,
        secrets_map={
            "OPENAI_API_KEY": "sk-test",
            "TELEGRAM_BOT_TOKEN": "tg-token",
        },
    )

    data = json.loads((tmp_path / ".openclaw" / "openclaw.json").read_text())
    assert data["channels"]["telegram"]["enabled"] is True
    assert data["channels"]["discord"]["enabled"] is False


@patch("armorer.apps.openclaw.config_adapter.ensure_openclaw_instance_ports")
def test_apply_openclaw_instance_config_disables_previously_managed_unselected_skill(mock_ports, tmp_path):
    (tmp_path / ".env").write_text("OPENCLAW_CONFIG_DIR=./.openclaw\nOPENCLAW_WORKSPACE_DIR=./workspace\n")
    (tmp_path / ".openclaw").mkdir()
    (tmp_path / ".openclaw" / "openclaw.json").write_text(
        json.dumps(
            {
                "skills": {
                    "entries": {
                        "blogwatcher": {"enabled": True},
                        "github": {"enabled": True},
                        "xurl": {"enabled": True},
                        "manual-skill": {"enabled": True},
                    }
                }
            }
        )
    )
    cfg = OpenClawInstanceConfig(
        access_mode="local",
        primary_provider="gemini",
        default_model="gemini-flash-lite-latest",
        enabled_channels=[],
        enabled_skills=["blogwatcher"],
        trusted_proxies=[],
        skill_settings={"blogwatcher": {"enabled": True}},
        web_search_provider="disabled",
    )

    apply_openclaw_instance_config(
        tmp_path,
        cfg,
        secrets_map={"GEMINI_API_KEY": "gem-key"},
        managed_skill_keys=["blogwatcher", "github", "xurl"],
    )

    data = json.loads((tmp_path / ".openclaw" / "openclaw.json").read_text())
    assert data["skills"]["entries"]["blogwatcher"]["enabled"] is True
    assert data["skills"]["entries"]["github"]["enabled"] is False
    assert data["skills"]["entries"]["xurl"]["enabled"] is False
    assert data["skills"]["entries"]["manual-skill"]["enabled"] is True


@patch("armorer.main._check_notifications")
@patch("armorer.main.load_dotenv")
@patch("armorer.main._configure_openclaw_instance")
@patch("armorer.main.apply_openclaw_config")
@patch("armorer.main.AppRegistry")
def test_app_configure_updates_registry_for_openclaw(
    mock_registry_cls,
    mock_apply_tool,
    mock_configure,
    mock_load_dotenv,
    mock_check_notifications,
    tmp_path,
):
    install_dir = tmp_path / "openclaw"
    install_dir.mkdir()
    (install_dir / ".env").write_text("")
    app_meta = SimpleNamespace(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        app_config_state={},
        managed_files=[],
        channels_enabled=[],
        skills_enabled=[],
        injected_skills=[],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta
    mock_registry_cls.return_value = registry
    mock_configure.return_value = (
        OpenClawInstanceConfig(access_mode="local", primary_provider="openai", default_model="gpt-4o"),
        {"OPENAI_API_KEY": "sk-test"},
    )
    mock_apply_tool.return_value = {
        "ok": True,
        "warnings": [],
        "channel_verification": {"statuses": {}, "warnings": []},
        "result": {
            "app_config_state": {"access_mode": "local", "primary_provider": "openai", "default_model": "gpt-4o"},
            "managed_files": [".env", ".openclaw/openclaw.json"],
            "channels_enabled": [],
            "skills_enabled": ["filesystem"],
        },
    }

    result = runner.invoke(app, ["app", "configure", "openclaw"])

    assert result.exit_code == 0
    assert "OpenClaw is configured inside Armorer" in result.stdout
    mock_apply_tool.assert_called_once()


@patch("armorer.main._check_notifications")
@patch("armorer.main.load_dotenv")
@patch("armorer.main.apply_openclaw_config")
@patch("armorer.main.AppRegistry")
def test_app_configure_supports_noninteractive_flags(
    mock_registry_cls,
    mock_apply_tool,
    mock_load_dotenv,
    mock_check_notifications,
    tmp_path,
):
    install_dir = tmp_path / "openclaw"
    install_dir.mkdir()
    (install_dir / ".env").write_text("")
    app_meta = SimpleNamespace(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        app_config_state={},
        managed_files=[],
        channels_enabled=[],
        skills_enabled=[],
        injected_skills=[],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta
    mock_registry_cls.return_value = registry
    mock_apply_tool.return_value = {
        "ok": True,
        "warnings": [],
        "channel_verification": {"statuses": {}, "warnings": []},
        "result": {
            "app_config_state": {
                "primary_provider": "gemini",
                "default_model": "gemini-flash-lite-latest",
                "enabled_channels": ["telegram"],
                "enabled_skills": ["github"],
            },
            "managed_files": [".env", ".openclaw/openclaw.json"],
            "channels_enabled": ["telegram"],
            "skills_enabled": ["github"],
        },
    }

    result = runner.invoke(
        app,
        [
            "app",
            "config",
            "openclaw",
            "--provider",
            "gemini",
            "--model",
            "gemini-flash-lite-latest",
            "--channel",
            "telegram",
            "--skill",
            "github",
            "--secret",
            "GEMINI_API_KEY=gm-test",
            "--secret",
            "TELEGRAM_BOT_TOKEN=tg-test",
        ],
    )

    assert result.exit_code == 0
    assert "OpenClaw is configured inside Armorer" in result.stdout
    mock_apply_tool.assert_called_once_with(
        "openclaw",
        config_patch={
            "primary_provider": "gemini",
            "default_model": "gemini-flash-lite-latest",
            "enabled_channels": ["telegram"],
            "enabled_skills": ["github"],
        },
        secrets={
            "GEMINI_API_KEY": "gm-test",
            "TELEGRAM_BOT_TOKEN": "tg-test",
        },
        bind_channels=True,
        pairing_codes={},
    )


@patch("armorer.main._check_notifications")
@patch("armorer.main.load_dotenv")
@patch("armorer.main.apply_openclaw_config")
@patch("armorer.main.AppRegistry")
def test_app_configure_supports_skip_bind(
    mock_registry_cls,
    mock_apply_tool,
    mock_load_dotenv,
    mock_check_notifications,
    tmp_path,
):
    install_dir = tmp_path / "openclaw"
    install_dir.mkdir()
    (install_dir / ".env").write_text("")
    app_meta = SimpleNamespace(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        app_config_state={},
        managed_files=[],
        channels_enabled=[],
        skills_enabled=[],
        injected_skills=[],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta
    mock_registry_cls.return_value = registry
    mock_apply_tool.return_value = {
        "ok": True,
        "warnings": [],
        "channels_bound": False,
        "channel_verification": {"statuses": {}, "warnings": []},
        "result": {
            "app_config_state": {
                "primary_provider": "gemini",
                "default_model": "gemini-flash-lite-latest",
                "enabled_channels": ["telegram"],
                "enabled_skills": [],
            },
            "managed_files": [".env", ".openclaw/openclaw.json"],
            "channels_enabled": ["telegram"],
            "skills_enabled": [],
        },
    }

    result = runner.invoke(
        app,
        [
            "app",
            "config",
            "openclaw",
            "--provider",
            "gemini",
            "--model",
            "gemini-flash-lite-latest",
            "--channel",
            "telegram",
            "--skip-bind",
        ],
    )

    assert result.exit_code == 0
    assert "Channel verification: deferred" in result.stdout
    assert mock_apply_tool.call_args.kwargs["bind_channels"] is False


@patch("armorer.apps.openclaw.config_adapter.ensure_openclaw_instance_ports")
@patch("armorer.tools.app_setup.ConfigManager")
@patch("armorer.tools.app_setup.AppRegistry")
def test_openclaw_chat_tools_context_validate_apply(
    mock_registry_cls,
    mock_cfg_cls,
    mock_ports,
    tmp_path,
):
    install_dir = tmp_path / "openclaw"
    (install_dir / ".openclaw").mkdir(parents=True)
    (install_dir / ".env").write_text("OPENCLAW_CONFIG_DIR=./.openclaw\nOPENCLAW_WORKSPACE_DIR=./workspace\n")
    (install_dir / ".openclaw" / "openclaw.json").write_text("{}\n")

    cfg = MagicMock()
    cfg.find_vault_key.return_value = None
    cfg.get_key.return_value = None
    mock_cfg_cls.return_value = cfg
    mock_ports.return_value = {
        "OPENCLAW_GATEWAY_PORT": "18789",
        "OPENCLAW_BRIDGE_PORT": "18790",
    }

    app_meta = SimpleNamespace(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        app_config_state={},
        managed_files=[],
        channels_enabled=[],
        skills_enabled=[],
        injected_skills=[],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta
    mock_registry_cls.return_value = registry

    context = get_openclaw_setup_context("openclaw")
    assert context["ok"] is True
    assert "current_config" in context
    assert "options" in context
    assert context["state_summary"]["provider"] == "openai"
    assert "setup_kb" in context
    assert context["setup_kb"]["next_questions"]

    validation = validate_openclaw_selection(
        "openclaw",
        config_patch={
            "primary_provider": "openai",
            "default_model": "gpt-4o",
            "enabled_channels": [],
            "enabled_skills": [],
            "web_search_provider": "disabled",
        },
        secrets={"OPENAI_API_KEY": "sk-test"},
    )
    assert validation["ok"] is True

    result = apply_openclaw_config(
        "openclaw",
        config_patch={
            "primary_provider": "openai",
            "default_model": "gpt-4o",
            "enabled_channels": [],
            "enabled_skills": [],
            "web_search_provider": "disabled",
        },
        secrets={"OPENAI_API_KEY": "sk-test"},
    )
    assert result["ok"] is True
    assert "Applied OpenClaw configuration for 'openclaw'." in result["message"]

    env_text = (install_dir / ".env").read_text()
    data = json.loads((install_dir / ".openclaw" / "openclaw.json").read_text())
    assert "OPENCLAW_GATEWAY_BIND=loopback" in env_text
    assert "armorer" not in data
    registry.add_app.assert_called_once()


@patch("armorer.tools.app_setup.approve_openclaw_pairings")
@patch("armorer.tools.app_setup.verify_openclaw_channels")
@patch("armorer.tools.app_setup.bind_openclaw_channels")
@patch("armorer.tools.app_setup.wait_for_compose_service_ready")
@patch("armorer.tools.app_setup.recreate_compose_service")
@patch("armorer.tools.app_setup.load_compose_services")
@patch("armorer.tools.app_setup.apply_openclaw_instance_config")
@patch("armorer.tools.app_setup.validate_openclaw_selection")
@patch("armorer.tools.app_setup.load_openclaw_instance_config")
@patch("armorer.tools.app_setup.ConfigManager")
@patch("armorer.tools.app_setup.AppRegistry")
def test_apply_openclaw_config_reuses_existing_credentials_in_runtime_apply(
    mock_registry_cls,
    mock_cfg_cls,
    mock_load_config,
    mock_validate,
    mock_apply_instance,
    mock_load_compose_services,
    mock_recreate_compose_service,
    mock_wait_ready,
    mock_bind_channels,
    mock_verify_channels,
    mock_approve_pairings,
    tmp_path,
):
    install_dir = tmp_path / "openclaw"
    install_dir.mkdir()
    (install_dir / ".env").write_text("")
    app_meta = SimpleNamespace(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        app_config_state={},
        managed_files=[],
        channels_enabled=[],
        skills_enabled=[],
        injected_skills=[],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta
    mock_registry_cls.return_value = registry

    cfg = MagicMock()
    cfg.find_vault_key.side_effect = lambda key: {
        "GEMINI_API_KEY": "gm-existing",
        "TELEGRAM_BOT_TOKEN": "tg-existing",
    }.get(key)
    cfg.get_scoped_secret.side_effect = lambda agent, key: {
        ("openclaw", "TELEGRAM_BOT_TOKEN"): "tg-existing",
    }.get((agent, key))
    cfg.get_key.return_value = None
    mock_cfg_cls.return_value = cfg

    mock_load_config.return_value = OpenClawInstanceConfig(
        access_mode="local",
        primary_provider="openai",
        default_model="gpt-4o",
        enabled_channels=[],
        enabled_skills=[],
        trusted_proxies=[],
        skill_settings={},
        web_search_provider="disabled",
    )
    mock_validate.return_value = {
        "ok": True,
        "warnings": [],
        "errors": [],
        "missing_credentials": [],
        "prepared_skills": [],
        "skill_settings": {},
    }
    mock_apply_instance.return_value = {
        "app_config_state": {"access_mode": "local", "primary_provider": "gemini", "default_model": "gemini-flash-lite-latest"},
        "managed_files": [".env", ".openclaw/openclaw.json"],
        "channels_enabled": ["telegram"],
        "skills_enabled": [],
        "runtime_env_changed": True,
    }
    mock_load_compose_services.return_value = {"openclaw-gateway": {}}
    mock_recreate_compose_service.return_value = SimpleNamespace(returncode=0, stdout="", stderr="")
    mock_wait_ready.return_value = {"ok": True, "status": "running", "health": "healthy"}
    mock_bind_channels.return_value = {"bound_channels": ["telegram"], "warnings": []}
    mock_verify_channels.return_value = {"statuses": {"telegram": {"verified": True}}, "warnings": []}
    mock_approve_pairings.return_value = {"statuses": {"telegram": {"approved": True}}, "warnings": []}

    result = apply_openclaw_config(
        "openclaw",
        config_patch={
            "primary_provider": "gemini",
            "default_model": "gemini-flash-lite-latest",
            "enabled_channels": ["telegram"],
            "enabled_skills": [],
            "web_search_provider": "disabled",
        },
        secrets={},
        pairing_codes={"telegram": "PAIRME12"},
    )

    assert result["ok"] is True
    effective_secrets = mock_apply_instance.call_args.kwargs["secrets_map"]
    assert effective_secrets["GEMINI_API_KEY"] == "gm-existing"
    assert effective_secrets["TELEGRAM_BOT_TOKEN"] == "tg-existing"
    assert mock_bind_channels.call_args.args[2]["GEMINI_API_KEY"] == "gm-existing"
    assert mock_bind_channels.call_args.args[2]["TELEGRAM_BOT_TOKEN"] == "tg-existing"
    mock_recreate_compose_service.assert_called_once_with(install_dir, "openclaw-gateway")
    mock_wait_ready.assert_called_once_with(install_dir, "openclaw-gateway", timeout_seconds=120)
    mock_approve_pairings.assert_called_once_with(install_dir, {"telegram": "PAIRME12"})
    assert result["pairing_approval"]["statuses"]["telegram"]["approved"] is True
    assert result["runtime_restart"]["restarted"] is True


@patch("armorer.tools.app_setup.approve_openclaw_pairings")
@patch("armorer.tools.app_setup.validate_openclaw_selection")
@patch("armorer.tools.app_setup.load_openclaw_instance_config")
@patch("armorer.tools.app_setup.ConfigManager")
@patch("armorer.tools.app_setup.AppRegistry")
def test_apply_openclaw_config_pairing_only_skips_validation(
    mock_registry_cls,
    mock_cfg_cls,
    mock_load_config,
    mock_validate,
    mock_approve_pairings,
    tmp_path,
):
    install_dir = tmp_path / "openclaw"
    install_dir.mkdir()
    app_meta = SimpleNamespace(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        app_config_state={},
        managed_files=[".env", ".openclaw/openclaw.json"],
        channels_enabled=["telegram"],
        skills_enabled=["blogwatcher"],
        injected_skills=["blogwatcher"],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta
    mock_registry_cls.return_value = registry
    mock_cfg_cls.return_value = MagicMock()
    mock_load_config.return_value = OpenClawInstanceConfig(
        access_mode="local",
        primary_provider="gemini",
        default_model="gemini-flash-lite-latest",
        enabled_channels=["telegram"],
        enabled_skills=["blogwatcher"],
        trusted_proxies=[],
        skill_settings={"blogwatcher": {"enabled": True}},
        web_search_provider="disabled",
    )
    mock_approve_pairings.return_value = {
        "ok": True,
        "statuses": {"telegram": {"approved": True, "detail": "approved"}},
        "warnings": [],
    }

    result = apply_openclaw_config(
        "openclaw",
        config_patch={},
        secrets={},
        pairing_codes={"telegram": "PAIRME12"},
    )

    assert result["ok"] is True
    mock_validate.assert_not_called()
    mock_approve_pairings.assert_called_once_with(install_dir, {"telegram": "PAIRME12"})
    assert result["pairing_approval"]["statuses"]["telegram"]["approved"] is True
