from __future__ import annotations

import json
import os
import subprocess

import pytest
from typer.testing import CliRunner

import eightmem.cli.main as cli_main
from eightmem.cli.main import app
from eightmem.core.env import runtime_env_path
from eightmem.llm.ollama import check_backend


runner = CliRunner()


@pytest.fixture(autouse=True)
def restore_runtime_env_after_test():
    keys = [
        "TELEGRAM_BOT_TOKEN",
        "TELEGRAM_WEBHOOK_SECRET",
        "EIGHTMEM_LOCAL_API_KEY",
        "OLLAMA_BASE_URL",
        "DEFAULT_LLM_FALLBACK_MODEL",
        "TELEGRAM_FORWARD_URL",
        "EIGHTMEM_TELEGRAM_APPROVAL_CHAT_ID",
        "TELEGRAM_APPROVAL_CHAT_ID",
        "EIGHTMEM_OPENCLAW_API_URL",
        "EIGHTMEM_OPENCLAW_API_KEY",
        "EIGHTMEM_HERMES_PROJECT_DIR",
        "HERMES_PROJECT_DIR",
    ]
    previous = {key: os.environ.get(key) for key in keys}
    yield
    for key, value in previous.items():
        if value is None:
            os.environ.pop(key, None)
        else:
            os.environ[key] = value


def test_setup_writes_runtime_config_without_live_checks(fake_home, monkeypatch):
    monkeypatch.delenv("TELEGRAM_BOT_TOKEN", raising=False)
    monkeypatch.delenv("TELEGRAM_WEBHOOK_SECRET", raising=False)
    monkeypatch.delenv("EIGHTMEM_LOCAL_API_KEY", raising=False)

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--telegram-token",
            "123456:abcdef",
            "--webhook-secret",
            "secret-1",
            "--telegram-forward-url",
            "https://public.example",
            "--no-register-webhook",
            "--llm-base-url",
            "http://127.0.0.1:11434",
            "--llm-model",
            "qwen2.5:14b",
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
        ],
    )

    assert result.exit_code == 0
    assert "Runtime ready:" in result.stdout
    assert "Config file:" in result.stdout
    assert "LLM check: skipped" in result.stdout

    config = runtime_env_path().read_text(encoding="utf-8")
    assert 'TELEGRAM_BOT_TOKEN="123456:abcdef"' in config
    assert 'TELEGRAM_WEBHOOK_SECRET="secret-1"' in config
    assert 'TELEGRAM_FORWARD_URL="https://public.example"' in config
    assert 'EIGHTMEM_LOCAL_API_KEY="test_runtime_key"' in config
    assert 'DEFAULT_LLM_FALLBACK_MODEL="qwen2.5:14b"' in config


def test_dedupe_previews_and_applies_exact_duplicates(fake_home):
    mem = fake_home / "memory"
    mem.mkdir(parents=True)
    (mem / "BELIEFS.md").write_text("# BELIEFS\n\n## Entries\n\n- Prefers short answers\n", encoding="utf-8")
    (mem / "PREFERENCES.md").write_text("# PREFERENCES\n\n## Entries\n\n- Prefers short answers.\n", encoding="utf-8")
    (mem / "CORRECTIONS.md").write_text("# CORRECTIONS\n\n## Entries\n\n- (empty)\n", encoding="utf-8")

    preview = runner.invoke(app, ["dedupe"])
    assert preview.exit_code == 0
    assert "Exact duplicates removable: 1" in preview.stdout
    assert "Dry run only" in preview.stdout
    assert "Prefers short answers" in (mem / "BELIEFS.md").read_text(encoding="utf-8")

    applied = runner.invoke(app, ["dedupe", "--apply"])
    assert applied.exit_code == 0
    assert "Removed: 1" in applied.stdout
    assert "Prefers short answers" not in (mem / "BELIEFS.md").read_text(encoding="utf-8")
    assert "Prefers short answers." in (mem / "PREFERENCES.md").read_text(encoding="utf-8")


def test_register_cli_creates_connector(fake_home):
    result = runner.invoke(
        app,
        [
            "register",
            "--id",
            "openclaw-viri",
            "--url",
            "http://127.0.0.1:18789/8mem/sync",
            "--description",
            "OpenClaw Viri",
        ],
    )

    assert result.exit_code == 0
    assert "Connector registered." in result.stdout
    assert "ID: openclaw-viri" in result.stdout
    assert "Secret:" in result.stdout

    connectors = json.loads((fake_home / "connectors.json").read_text(encoding="utf-8"))
    assert connectors[0]["id"] == "openclaw-viri"
    assert connectors[0]["url"] == "http://127.0.0.1:18789/8mem/sync"
    assert len(connectors[0]["secret"]) == 64


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

    class _Response:
        def __enter__(self):
            return self

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

        def read(self):
            return b'{"ok": true, "result": true}'

    def _fake_urlopen(req, timeout=10):
        captured["url"] = req.full_url
        captured["body"] = req.data
        captured["headers"] = dict(req.header_items())
        captured["timeout"] = timeout
        return _Response()

    monkeypatch.setattr(cli_main, "urlopen", _fake_urlopen)

    result = cli_main._register_telegram_commands("123456:abcdef")

    payload = json.loads(captured["body"])
    assert result["ok"] is True
    assert captured["url"] == "https://api.telegram.org/bot123456:abcdef/setMyCommands"
    assert payload["commands"][0]["command"] == "passport"
    assert {item["command"] for item in payload["commands"]} == {"passport", "compare", "corrections", "refresh8mem", "brief"}


def test_setup_can_register_telegram_webhook(fake_home, monkeypatch):
    registered: dict[str, object] = {}

    def _fake_set_webhook(config, *, forward_url, timeout_seconds=15):
        registered["token"] = config.bot_token
        registered["secret"] = config.webhook_secret
        registered["forward_url"] = forward_url
        return {"ok": True, "result": True}

    monkeypatch.delenv("TELEGRAM_BOT_TOKEN", raising=False)
    monkeypatch.delenv("TELEGRAM_WEBHOOK_SECRET", raising=False)
    monkeypatch.setattr(cli_main, "set_telegram_webhook", _fake_set_webhook)

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--telegram-token",
            "123456:abcdef",
            "--webhook-secret",
            "secret-1",
            "--telegram-forward-url",
            "https://public.example",
            "--skip-llm-check",
        ],
    )

    assert result.exit_code == 0
    assert "Telegram webhook: registered at https://public.example/channels/telegram/webhook" in result.stdout
    assert registered == {
        "token": "123456:abcdef",
        "secret": "secret-1",
        "forward_url": "https://public.example",
    }


def test_setup_rejects_invalid_telegram_token_without_saving(fake_home, monkeypatch):
    monkeypatch.delenv("TELEGRAM_BOT_TOKEN", raising=False)
    monkeypatch.delenv("TELEGRAM_WEBHOOK_SECRET", raising=False)
    runtime_env_path().parent.mkdir(parents=True, exist_ok=True)
    runtime_env_path().write_text('TELEGRAM_BOT_TOKEN="old-bad-token"\n', encoding="utf-8")

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--telegram-token",
            "not-a-bot-token",
            "--webhook-secret",
            "secret-1",
            "--telegram-forward-url",
            "https://public.example",
            "--skip-llm-check",
        ],
    )

    assert result.exit_code == 0
    assert "Telegram setup skipped because the token was blank or invalid" in result.stdout
    config = runtime_env_path().read_text(encoding="utf-8")
    assert "TELEGRAM_BOT_TOKEN" not in config
    assert "TELEGRAM_WEBHOOK_SECRET" not in config
    assert "TELEGRAM_FORWARD_URL" not in config


def test_setup_interactive_invalid_telegram_token_does_not_prompt_or_save_partial_config(fake_home, monkeypatch):
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(
        app,
        [
            "setup",
            "--runtime-api-key",
            "test_runtime_key",
        ],
        input="2\ny\n\nn\n",
    )

    assert result.exit_code == 0
    assert "Telegram token does not look like a BotFather token" in result.stdout
    assert "Telegram setup: skipped. Add it later with: 8mem setup --mode telegram" in result.stdout
    assert "Telegram webhook secret" not in result.stdout
    config = runtime_env_path().read_text(encoding="utf-8")
    assert "TELEGRAM_BOT_TOKEN" not in config
    assert "TELEGRAM_WEBHOOK_SECRET" not in config
    assert "TELEGRAM_FORWARD_URL" not in config


def test_setup_interactive_invalid_telegram_forward_url_is_not_saved(fake_home, monkeypatch):
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(
        app,
        [
            "setup",
            "--runtime-api-key",
            "test_runtime_key",
        ],
        input="2\n123456:abcdef\n\ny\n\nn\n",
    )

    assert result.exit_code == 0
    assert "Telegram public URL must look like https://your-domain.example" in result.stdout
    config = runtime_env_path().read_text(encoding="utf-8")
    assert 'TELEGRAM_BOT_TOKEN="123456:abcdef"' in config
    assert "TELEGRAM_WEBHOOK_SECRET" in config
    assert "TELEGRAM_FORWARD_URL" not in config


def test_setup_prints_api_key_lookup_not_secret(fake_home):
    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--skip-telegram",
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
        ],
    )

    assert result.exit_code == 0
    assert "Local API key lookup: grep '^EIGHTMEM_LOCAL_API_KEY='" in result.stdout
    assert "test_runtime_key" not in result.stdout


def test_setup_interactive_browser_mode_skips_telegram_and_points_to_chat(fake_home, monkeypatch):
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(
        app,
        [
            "setup",
            "--runtime-api-key",
            "test_runtime_key",
        ],
        input="\nn\n",
    )

    assert result.exit_code == 0
    assert "Browser UI selected. Telegram can be added later" in result.stdout
    assert "Open browser UI: http://127.0.0.1:8787/chat" in result.stdout
    config = runtime_env_path().read_text(encoding="utf-8")
    assert "TELEGRAM_BOT_TOKEN" not in config
    assert 'EIGHTMEM_LOCAL_API_KEY="test_runtime_key"' in config


def test_setup_browser_mode_preserves_existing_telegram_config(fake_home, monkeypatch):
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))
    runtime_env_path().parent.mkdir(parents=True, exist_ok=True)
    runtime_env_path().write_text(
        '\n'.join(
            [
                'TELEGRAM_BOT_TOKEN="123456:existing"',
                'TELEGRAM_WEBHOOK_SECRET="existing-secret"',
                'TELEGRAM_FORWARD_URL="https://existing.example"',
                'TELEGRAM_APPROVAL_CHAT_ID="5989413668"',
            ]
        )
        + "\n",
        encoding="utf-8",
    )

    result = runner.invoke(
        app,
        [
            "setup",
            "--mode",
            "browser",
            "--runtime-api-key",
            "test_runtime_key",
        ],
        input="n\n",
    )

    assert result.exit_code == 0
    config = runtime_env_path().read_text(encoding="utf-8")
    assert 'TELEGRAM_BOT_TOKEN="123456:existing"' in config
    assert 'TELEGRAM_WEBHOOK_SECRET="existing-secret"' in config
    assert 'TELEGRAM_FORWARD_URL="https://existing.example"' in config
    assert 'TELEGRAM_APPROVAL_CHAT_ID="5989413668"' in config


def test_setup_interactive_both_mode_saves_telegram_and_registers_webhook(fake_home, monkeypatch):
    registered: dict[str, object] = {}

    def _fake_set_webhook(config, *, forward_url, timeout_seconds=15):
        registered["token"] = config.bot_token
        registered["secret"] = config.webhook_secret
        registered["forward_url"] = forward_url
        return {"ok": True, "result": True}

    monkeypatch.setattr(cli_main, "set_telegram_webhook", _fake_set_webhook)
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(
        app,
        [
            "setup",
            "--runtime-api-key",
            "test_runtime_key",
        ],
        input="3\n123456:abcdef\nsecret-1\nhttps://public.example\nn\n",
    )

    assert result.exit_code == 0
    assert "Browser UI + Telegram selected" in result.stdout
    assert "Telegram webhook: registered at https://public.example/channels/telegram/webhook" in result.stdout
    config = runtime_env_path().read_text(encoding="utf-8")
    assert 'TELEGRAM_BOT_TOKEN="123456:abcdef"' in config
    assert 'TELEGRAM_WEBHOOK_SECRET="secret-1"' in config
    assert 'TELEGRAM_FORWARD_URL="https://public.example"' in config
    assert registered == {
        "token": "123456:abcdef",
        "secret": "secret-1",
        "forward_url": "https://public.example",
    }


def test_setup_skip_llm_check_does_not_prompt_for_ollama(fake_home, monkeypatch):
    def _fail_check(**_kwargs):
        raise AssertionError("LLM check should not run")

    monkeypatch.setattr(cli_main, "check_ollama_backend", _fail_check)

    result = runner.invoke(
        app,
        [
            "setup",
            "--mode",
            "both",
            "--skip-llm-check",
            "--runtime-api-key",
            "test_runtime_key",
        ],
        input="123456:abcdef\n\n\n",
    )

    assert result.exit_code == 0
    assert "Set up local Ollama now" not in result.stdout
    assert "LLM check: skipped" in result.stdout


def test_setup_no_status_and_no_next_steps_suppresses_operator_copy(fake_home, monkeypatch):
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--no-status",
            "--no-next-steps",
            "--runtime-api-key",
            "test_runtime_key",
        ],
    )

    assert result.exit_code == 0
    assert "Runtime ready:" not in result.stdout
    assert "Next:" not in result.stdout
    assert 'EIGHTMEM_LOCAL_API_KEY="test_runtime_key"' in runtime_env_path().read_text(encoding="utf-8")


def test_setup_mode_telegram_enters_telegram_path_directly(fake_home, monkeypatch):
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(
        app,
        [
            "setup",
            "--mode",
            "telegram",
            "--runtime-api-key",
            "test_runtime_key",
            "--no-register-webhook",
        ],
        input="123456:abcdef\n\n\nn\n",
    )

    assert result.exit_code == 0
    assert "How do you want to use 8mem first?" not in result.stdout
    assert "Telegram selected. Required: BotFather token" in result.stdout
    assert "Telegram token saved. Webhook registration skipped because no public URL was provided." in result.stdout
    config = runtime_env_path().read_text(encoding="utf-8")
    assert 'TELEGRAM_BOT_TOKEN="123456:abcdef"' in config


def test_setup_mode_openclaw_wires_workspace_without_hardcoded_secret(fake_home, tmp_path, monkeypatch):
    calls: list[tuple[str, ...]] = []
    home = tmp_path / "home"
    monkeypatch.setenv("HOME", str(home))

    def _fake_run(command, **kwargs):
        calls.append(tuple(command))

        class Result:
            returncode = 0
            stdout = ""
            stderr = ""

        return Result()

    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"
    workspace.mkdir()
    (workspace / "AGENTS.md").write_text("# Existing Agent Rules\n\nKeep this.\n", encoding="utf-8")
    (workspace / "HEARTBEAT.md").write_text("# Existing Heartbeat\n", encoding="utf-8")
    config_path.write_text(
        json.dumps(
            {
                "model": "gpt-4o-mini",
                "channels": {"telegram": {"enabled": True}},
            }
        ),
        encoding="utf-8",
    )
    monkeypatch.setattr(cli_main.subprocess, "run", _fake_run)
    monkeypatch.setattr(cli_main, "has_command", lambda command: command == "systemctl")

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "openclaw",
            "--openclaw-workspace",
            str(workspace),
            "--openclaw-config",
            str(config_path),
            "--eightmem-api-url",
            "http://8mem.local:8787",
            "--eightmem-api-key",
            "remote_memory_key",
            "--runtime-api-key",
            "secret_runtime_key",
            "--skip-llm-check",
        ],
    )

    assert result.exit_code == 0
    assert "OpenClaw wired:" in result.stdout
    assert "approval test -> skipped: OpenClaw Telegram runtimes may not forward inline approval callbacks" in result.stdout
    assert "secret_runtime_key" not in result.stdout
    assert "remote_memory_key" not in result.stdout
    assert ("systemctl", "--user", "restart", "openclaw-gateway") in calls
    assert ("systemctl", "--user", "enable", "--now", "8mem-agent-sync-receiver.service") in calls
    agents = (workspace / "AGENTS.md").read_text(encoding="utf-8")
    commands = (workspace / "8MEM-COMMANDS.md").read_text(encoding="utf-8")
    heartbeat = (workspace / "HEARTBEAT.md").read_text(encoding="utf-8")
    memory_api = (workspace / "MEMORY-API.md").read_text(encoding="utf-8")
    remember_flow = (workspace / "memory" / "8mem-api-remember.md").read_text(encoding="utf-8")
    propose_flow = (workspace / "memory" / "8mem-api-propose.md").read_text(encoding="utf-8")
    correct_flow = (workspace / "memory" / "8mem-api-correct.md").read_text(encoding="utf-8")
    forget_flow = (workspace / "memory" / "8mem-api-forget.md").read_text(encoding="utf-8")
    approval_flow = (workspace / "memory" / "8mem-api-approval.md").read_text(encoding="utf-8")
    assert "# Existing Agent Rules" in agents
    assert len(agents) < 6500
    assert "Read `MEMORY-API.md` at startup." in agents
    assert "Read `8MEM-COMMANDS.md` at startup." in agents
    assert "Read `8mem-card.md` at startup." in agents
    assert "source of truth for 8mem memory commands" in agents
    assert "follow the `MEMORY-API.md` status contract exactly" in agents
    assert "Never hardcode `Saved.` after a memory API call" in agents
    assert "Say `Saved.` only for exact `status: written`" in agents
    assert "say `Already in memory.` for exact `status: duplicate`" in agents
    assert 'Save then reply "Saved."' not in agents
    assert "If `memory/8mem-context.md` is missing or empty at startup" not in agents
    assert "| `/corrections` | Run exact command from `MEMORY-API.md` section `/corrections`." in commands
    assert "If cached context is missing or empty" in commands
    assert "do not invent" in commands
    assert "run `8mem-card.md` function `SEND_PASSPORT_CARD` before ending the turn" in commands
    assert "run `8mem-card.md` function `SEND_COMPARE_CARD` before ending the turn" in commands
    assert "Do not skip the helper just because the text answer was sent successfully." in commands
    assert "Apply 8mem intent routing before email, calendar, file, shell, or generic assistant routing." in agents
    assert "Negotiation/commercial quote detection runs before email drafting and before approval flow." in commands
    assert "| `/brief [person]` | Use the Brief format below." in commands
    assert "bare `/compare` with no topic" in commands
    assert "Do not send a compare card" in commands
    assert "| `/compare <topic>` | Use the exact Compare format below." in commands
    assert "inferred durable preference or operating principle" in commands
    assert "Bullets only; no inline labels like `Name:` or `Timezone:`." in commands
    assert "Extract the timezone from there. Never use the Pi system clock or infer UTC." in commands
    assert "8mem context is not refreshed yet. Send /refresh8mem and retry." in commands
    assert "<Name> - 8mem passport" in commands
    assert "Currently helping with" in commands
    assert "Recent corrections" in commands
    assert 'Ask "forget <memory>" anytime.' in commands
    assert "Never use a `/passport` section heading named `Hard rules`" in commands
    assert "With <Name>'s memory" in commands
    assert "Used\n- <specific memory/correction used>" in commands
    assert "No file names, file paths, ports, hostnames, API names, bearer keys, JSON, or implementation notes." in commands
    card = (workspace / "8mem-card.md").read_text(encoding="utf-8")
    assert "http://8mem.local:8787/v1/context" in heartbeat
    assert "## 8mem Memory API" in memory_api
    assert "http://8mem.local:8787/v1/context" in memory_api
    assert "### /passport" in memory_api
    assert "Run this exact command for `/passport`" in memory_api
    assert "Do not use headings named `Hard rules` or `Corrections`" in memory_api
    assert "Currently helping with" in memory_api
    assert "Trust check" in memory_api
    assert "### passport card" in memory_api
    assert "### compare card" in memory_api
    assert "SEND_COMPARE_CARD` before ending the turn" in memory_api
    assert "Do not skip the helper just because the text answer was sent successfully." in memory_api
    assert "### recent correction details" in memory_api
    assert "Read `memory/8mem-api-remember.md` and follow it exactly." in memory_api
    assert "Read `memory/8mem-api-propose.md` and follow it exactly." in memory_api
    assert "Read `memory/8mem-api-correct.md` and follow it exactly." in memory_api
    assert "Read `memory/8mem-api-forget.md` and follow it exactly." in memory_api
    assert "Read `memory/8mem-api-approval.md` and follow it exactly." in memory_api
    assert "Do not run this for bare `/compare` with no topic" in memory_api
    assert "### /corrections\n\nRun this. Do not infer from cache.\n\n```bash\nE=" in memory_api
    assert "`refinement_pending`: show existing vs new and ask whether to update." in remember_flow
    assert "`refinement_pending`: follow the refinement branch from `8mem-api-remember.md`." in propose_flow
    assert "Never use a `/passport` section heading named `Corrections`" in commands
    assert str(runtime_env_path()) in memory_api
    assert "gateway.systemd.env" in card
    assert "HOST=${HOST:-8mem.local:8787}" in card
    assert "SEND_PASSPORT_CARD()" in card
    assert "SEND_COMPARE_CARD()" in card
    assert "/v1/passport/card" in card
    assert "/v1/compare/card" in card
    assert "sendPhoto" in card
    assert "/v1/memory" in remember_flow
    assert "/v1/memory/propose" in propose_flow
    assert "/v1/memory/commit" in propose_flow
    assert "/v1/correction" in correct_flow
    assert "/v1/forget" in forget_flow
    assert "FACT='<fact>'" in remember_flow
    assert "json.dumps({\"text\": os.environ.get(\"FACT\", \"\")" in remember_flow
    assert '-d \'{"text":"<fact>"' not in remember_flow
    assert "`duplicate`: reply exactly `Already in memory.` Stop." in remember_flow
    assert "`skipped`: reply exactly `Already tracked.` Stop." in remember_flow
    assert "If user keeps existing: reply exactly `Kept existing.` Stop." in remember_flow
    assert "Reply `Updated.` only after `status: corrected`" in remember_flow
    assert "`not_found`: reply exactly `No matching memory found.` Stop." in forget_flow
    assert "`not_found`: reply exactly `Nothing was deleted.` Stop." in forget_flow
    assert "Never use this for memory remember/correct/forget." in approval_flow
    assert "### 8mem Approval Flow" in approval_flow
    assert "http://127.0.0.1:8787/v1/approval" in approval_flow
    assert "TELEGRAM_APPROVAL_CHAT_ID" in approval_flow
    assert "TELEGRAM_ALLOWED_USER_ID" in approval_flow
    assert "Never execute without confirmed resolution." in approval_flow
    assert "manual fallback" in approval_flow
    assert "approval:approved:<id>" in approval_flow
    assert "approval:denied:<id>" in approval_flow
    assert "EIGHTMEM_OPENCLAW_API_KEY" in memory_api
    assert "secret_runtime_key" not in memory_api
    assert "remote_memory_key" not in memory_api
    assert "secret_runtime_key" not in remember_flow + propose_flow + correct_flow + forget_flow + approval_flow
    assert "remote_memory_key" not in remember_flow + propose_flow + correct_flow + forget_flow + approval_flow
    assert "secret_runtime_key" not in agents
    assert "secret_runtime_key" not in heartbeat
    assert "remote_memory_key" not in heartbeat
    assert "memory/8mem-context.md.tmp && mv memory/8mem-context.md.tmp memory/8mem-context.md" in heartbeat
    openclaw_config = json.loads(config_path.read_text(encoding="utf-8"))
    assert openclaw_config["model"] == "gpt-4o-mini"
    assert openclaw_config["channels"]["telegram"]["capabilities"]["inlineButtons"] == "dm"
    assert {item["command"] for item in openclaw_config["channels"]["telegram"]["customCommands"]} == {
        "passport",
        "compare",
        "corrections",
        "refresh8mem",
        "brief",
    }
    config = runtime_env_path().read_text(encoding="utf-8")
    assert 'EIGHTMEM_OPENCLAW_API_URL="http://8mem.local:8787"' in config
    assert 'EIGHTMEM_OPENCLAW_API_KEY="remote_memory_key"' in config
    receiver = fake_home / "webhook-receiver.py"
    receiver_env = fake_home / "agents" / "receiver.env"
    service = home / ".config" / "systemd" / "user" / "8mem-agent-sync-receiver.service"
    assert receiver.exists()
    assert receiver.stat().st_mode & 0o111
    receiver_text = receiver.read_text(encoding="utf-8")
    receiver_env_text = receiver_env.read_text(encoding="utf-8")
    compile(receiver_text, str(receiver), "exec")
    assert "PORT = int(os.environ.get(\"EIGHTMEM_AGENT_SYNC_PORT\", \"18792\"))" in receiver_text
    assert "system_prompt_injection" in receiver_text
    assert str(workspace / "memory" / "8mem-context.md") in receiver_text
    assert "EIGHTMEM_OPENCLAW_API_URL=http://8mem.local:8787" in receiver_env_text
    assert f"EIGHTMEM_OPENCLAW_CONTEXT_FILE={workspace / 'memory' / '8mem-context.md'}" in receiver_env_text
    assert (fake_home / "webhook-secret.txt").exists()
    assert "ExecStart=" in service.read_text(encoding="utf-8")


def test_setup_mode_openclaw_rejects_remote_primary_without_key(fake_home, tmp_path):
    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "openclaw",
            "--openclaw-workspace",
            str(workspace),
            "--openclaw-config",
            str(config_path),
            "--eightmem-api-url",
            "http://remote.example:8787",
            "--runtime-api-key",
            "local_runtime_key",
            "--skip-llm-check",
            "--no-restart-openclaw",
        ],
    )

    assert result.exit_code == 2
    assert "remote primary memory needs --eightmem-api-key" in result.stdout
    assert not (workspace / "AGENTS.md").exists()
    assert not (workspace / "8MEM-COMMANDS.md").exists()
    assert not (workspace / "MEMORY-API.md").exists()


def test_setup_mode_openclaw_rejects_oversized_agents_file_without_partial_patch(fake_home, tmp_path):
    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"
    workspace.mkdir()
    original_agents = "A" * 10_000
    original_heartbeat = "# Existing Heartbeat\n"
    original_config = {"channels": {"telegram": {"enabled": True}}}
    (workspace / "AGENTS.md").write_text(original_agents, encoding="utf-8")
    (workspace / "HEARTBEAT.md").write_text(original_heartbeat, encoding="utf-8")
    config_path.write_text(json.dumps(original_config), encoding="utf-8")

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "openclaw",
            "--openclaw-workspace",
            str(workspace),
            "--openclaw-config",
            str(config_path),
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
            "--no-restart-openclaw",
        ],
    )

    assert result.exit_code == 2
    assert "OpenClaw setup failed" in result.stdout
    assert "OpenClaw's hard bootstrap-file limit is 12000 chars" in result.stdout
    assert "silently truncates oversized bootstrap files" in result.stdout
    assert (workspace / "AGENTS.md").read_text(encoding="utf-8") == original_agents
    assert (workspace / "HEARTBEAT.md").read_text(encoding="utf-8") == original_heartbeat
    assert json.loads(config_path.read_text(encoding="utf-8")) == original_config


def test_setup_mode_openclaw_rejects_oversized_heartbeat_file_without_partial_patch(fake_home, tmp_path):
    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"
    workspace.mkdir()
    original_agents = "# Existing Agent Rules\n"
    original_heartbeat = "H" * 11_500
    original_config = {"channels": {"telegram": {"enabled": True}}}
    (workspace / "AGENTS.md").write_text(original_agents, encoding="utf-8")
    (workspace / "HEARTBEAT.md").write_text(original_heartbeat, encoding="utf-8")
    config_path.write_text(json.dumps(original_config), encoding="utf-8")

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "openclaw",
            "--openclaw-workspace",
            str(workspace),
            "--openclaw-config",
            str(config_path),
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
            "--no-restart-openclaw",
        ],
    )

    assert result.exit_code == 2
    assert "OpenClaw setup failed" in result.stdout
    assert "HEARTBEAT.md would be" in result.stdout
    assert (workspace / "AGENTS.md").read_text(encoding="utf-8") == original_agents
    assert (workspace / "HEARTBEAT.md").read_text(encoding="utf-8") == original_heartbeat
    assert json.loads(config_path.read_text(encoding="utf-8")) == original_config


def test_setup_mode_openclaw_rejects_oversized_memory_api_file_without_partial_patch(fake_home, tmp_path):
    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"
    workspace.mkdir()
    original_agents = "# Existing Agent Rules\n"
    original_heartbeat = "# Existing Heartbeat\n"
    original_memory_api = "M" * 11_950
    original_config = {"channels": {"telegram": {"enabled": True}}}
    (workspace / "AGENTS.md").write_text(original_agents, encoding="utf-8")
    (workspace / "HEARTBEAT.md").write_text(original_heartbeat, encoding="utf-8")
    (workspace / "MEMORY-API.md").write_text(original_memory_api, encoding="utf-8")
    config_path.write_text(json.dumps(original_config), encoding="utf-8")

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "openclaw",
            "--openclaw-workspace",
            str(workspace),
            "--openclaw-config",
            str(config_path),
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
            "--no-restart-openclaw",
        ],
    )

    assert result.exit_code == 2
    assert "OpenClaw setup failed" in result.stdout
    assert "MEMORY-API.md would be" in result.stdout
    assert (workspace / "AGENTS.md").read_text(encoding="utf-8") == original_agents
    assert (workspace / "HEARTBEAT.md").read_text(encoding="utf-8") == original_heartbeat
    assert (workspace / "MEMORY-API.md").read_text(encoding="utf-8") == original_memory_api
    assert json.loads(config_path.read_text(encoding="utf-8")) == original_config


def test_setup_mode_openclaw_uses_8mem_commands_when_agents_commands_is_large(fake_home, tmp_path):
    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"
    workspace.mkdir()
    existing_commands = "# Existing OpenClaw Commands\n\n" + ("C" * 10_246)
    (workspace / "AGENTS-COMMANDS.md").write_text(existing_commands, encoding="utf-8")

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "openclaw",
            "--openclaw-workspace",
            str(workspace),
            "--openclaw-config",
            str(config_path),
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
            "--no-restart-openclaw",
        ],
    )

    assert result.exit_code == 0
    assert (workspace / "AGENTS-COMMANDS.md").read_text(encoding="utf-8") == existing_commands
    assert "## 8mem Command Rules" in (workspace / "8MEM-COMMANDS.md").read_text(encoding="utf-8")
    assert "Read `8MEM-COMMANDS.md` at startup" in (workspace / "AGENTS.md").read_text(encoding="utf-8")


def test_setup_mode_openclaw_migrates_legacy_commands_block_to_8mem_commands(fake_home, tmp_path):
    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"
    workspace.mkdir()
    legacy_commands = "# Existing OpenClaw Commands\n\n" + cli_main._openclaw_agents_commands_block()
    (workspace / "AGENTS-COMMANDS.md").write_text(legacy_commands, encoding="utf-8")

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "openclaw",
            "--openclaw-workspace",
            str(workspace),
            "--openclaw-config",
            str(config_path),
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
            "--no-restart-openclaw",
        ],
    )

    assert result.exit_code == 0
    migrated = (workspace / "AGENTS-COMMANDS.md").read_text(encoding="utf-8")
    assert "# Existing OpenClaw Commands" in migrated
    assert "<!-- 8mem managed block: 8mem-openclaw-integration -->" not in migrated
    assert "## 8mem Command Rules" in (workspace / "8MEM-COMMANDS.md").read_text(encoding="utf-8")
    assert "AGENTS-COMMANDS.md -> removed migrated block" in result.stdout


def test_setup_mode_openclaw_is_idempotent(fake_home, tmp_path, monkeypatch):
    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"
    monkeypatch.setattr(cli_main, "_restart_openclaw_gateway", lambda: (True, "restarted"))

    args = [
        "setup",
        "--non-interactive",
        "--mode",
        "openclaw",
        "--openclaw-workspace",
        str(workspace),
        "--openclaw-config",
        str(config_path),
        "--eightmem-api-url",
        "http://127.0.0.1:8787",
        "--runtime-api-key",
        "test_runtime_key",
        "--skip-llm-check",
        "--no-restart-openclaw",
    ]

    first = runner.invoke(app, args)
    second = runner.invoke(app, args)

    assert first.exit_code == 0
    assert second.exit_code == 0
    agents = (workspace / "AGENTS.md").read_text(encoding="utf-8")
    commands = (workspace / "8MEM-COMMANDS.md").read_text(encoding="utf-8")
    heartbeat = (workspace / "HEARTBEAT.md").read_text(encoding="utf-8")
    memory_api = (workspace / "MEMORY-API.md").read_text(encoding="utf-8")
    flow_files = [workspace / "memory" / name for name in cli_main.OPENCLAW_MEMORY_FLOW_FILES]
    assert agents.count("<!-- 8mem managed block: 8mem-openclaw-integration -->") == 1
    assert commands.count("<!-- 8mem managed block: 8mem-openclaw-integration -->") == 1
    assert heartbeat.count("<!-- 8mem managed block: 8mem-openclaw-integration -->") == 1
    assert memory_api.count("<!-- 8mem managed block: 8mem-openclaw-integration -->") == 1
    assert all(path.read_text(encoding="utf-8").count("<!-- 8mem managed block: 8mem-openclaw-integration -->") == 1 for path in flow_files)
    assert "AGENTS.md     -> 8mem integration block unchanged" in second.stdout
    assert "8MEM-COMMANDS.md -> 8mem command rules unchanged" in second.stdout
    assert "HEARTBEAT.md  -> 8mem context refresh unchanged" in second.stdout
    assert "MEMORY-API.md -> 8mem command reference unchanged" in second.stdout
    assert "memory API flows -> 8mem-api-remember.md unchanged" in second.stdout
    assert "openclaw.json -> inline buttons unchanged" in second.stdout


def test_setup_mode_openclaw_preserves_existing_memory_api_content(fake_home, tmp_path):
    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"
    workspace.mkdir()
    (workspace / "MEMORY-API.md").write_text("# Existing API Notes\n\nKeep this.\n", encoding="utf-8")

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "openclaw",
            "--openclaw-workspace",
            str(workspace),
            "--openclaw-config",
            str(config_path),
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
            "--no-restart-openclaw",
        ],
    )

    assert result.exit_code == 0
    memory_api = (workspace / "MEMORY-API.md").read_text(encoding="utf-8")
    assert "# Existing API Notes" in memory_api
    assert "## 8mem Memory API" in memory_api
    assert memory_api.count("<!-- 8mem managed block: 8mem-openclaw-integration -->") == 1


def test_setup_mode_openclaw_reads_workspace_from_openclaw_json(fake_home, tmp_path):
    configured_workspace = tmp_path / "configured-workspace"
    config_path = tmp_path / "openclaw.json"
    config_path.write_text(
        json.dumps(
            {
                "agents": {"defaults": {"workspace": str(configured_workspace)}},
                "channels": {"telegram": {"enabled": True}},
            }
        ),
        encoding="utf-8",
    )

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "openclaw",
            "--openclaw-config",
            str(config_path),
            "--eightmem-api-url",
            "http://127.0.0.1:8787",
            "--runtime-api-key",
            "test_runtime_key",
            "--no-restart-openclaw",
        ],
    )

    assert result.exit_code == 0
    assert "OpenClaw wired:" in result.stdout
    assert (configured_workspace / "AGENTS.md").exists()
    assert (configured_workspace / "HEARTBEAT.md").exists()
    assert (configured_workspace / "MEMORY-API.md").exists()


def test_setup_interactive_auto_wires_when_standard_openclaw_detected(fake_home, tmp_path, monkeypatch):
    home = tmp_path / "home"
    workspace = home / ".openclaw" / "workspace"
    config_path = home / ".openclaw" / "openclaw.json"
    workspace.mkdir(parents=True)
    config_path.write_text(
        json.dumps(
            {
                "agents": {"defaults": {"workspace": str(workspace)}},
                "channels": {"telegram": {"enabled": True}},
            }
        ),
        encoding="utf-8",
    )
    monkeypatch.setenv("HOME", str(home))
    monkeypatch.setattr(cli_main, "_restart_openclaw_gateway", lambda: (True, "restarted"))

    result = runner.invoke(
        app,
        [
            "setup",
            "--runtime-api-key",
            "test_runtime_key",
        ],
    )

    assert result.exit_code == 0
    assert "How do you want to use 8mem first?" not in result.stdout
    assert "OpenClaw detected - wired automatically:" in result.stdout
    assert "Your agent now remembers you." in result.stdout
    assert (workspace / "AGENTS.md").exists()
    assert (workspace / "HEARTBEAT.md").exists()


def test_setup_non_interactive_does_not_auto_wire_standard_openclaw(fake_home, tmp_path, monkeypatch):
    home = tmp_path / "home"
    workspace = home / ".openclaw" / "workspace"
    config_path = home / ".openclaw" / "openclaw.json"
    workspace.mkdir(parents=True)
    config_path.write_text(
        json.dumps(
            {
                "agents": {"defaults": {"workspace": str(workspace)}},
                "channels": {"telegram": {"enabled": True}},
            }
        ),
        encoding="utf-8",
    )
    monkeypatch.setenv("HOME", str(home))

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
        ],
    )

    assert result.exit_code == 0
    assert not (workspace / "AGENTS.md").exists()
    assert not (workspace / "HEARTBEAT.md").exists()
    openclaw_config = json.loads(config_path.read_text(encoding="utf-8"))
    assert "capabilities" not in openclaw_config["channels"]["telegram"]


def test_setup_mode_openclaw_agents_block_includes_edit_approval_rules(fake_home, tmp_path):
    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "openclaw",
            "--openclaw-workspace",
            str(workspace),
            "--openclaw-config",
            str(config_path),
            "--runtime-api-key",
            "test_runtime_key",
            "--no-restart-openclaw",
        ],
    )

    assert result.exit_code == 0
    commands = (workspace / "8MEM-COMMANDS.md").read_text(encoding="utf-8")
    assert "Buttons must include Approve, Deny, and Edit." in commands
    assert "approval:edit:<approval_id>" in commands
    assert "User requested edit" in commands
    assert "Use `MEMORY-API.md` section `Approval Flow`" in commands
    assert "do not also send a text fallback" in commands
    assert "Note: I did not schedule a reminder" in commands
    assert "this will not trigger automatically" in commands


def test_setup_mode_openclaw_non_interactive_skips_llm_probe(fake_home, tmp_path, monkeypatch):
    def _fail_if_called(**kwargs):
        raise AssertionError("OpenClaw setup should not probe Ollama")

    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"
    monkeypatch.setattr(cli_main, "check_ollama_backend", _fail_if_called)

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "openclaw",
            "--openclaw-workspace",
            str(workspace),
            "--openclaw-config",
            str(config_path),
            "--runtime-api-key",
            "test_runtime_key",
            "--no-restart-openclaw",
        ],
    )

    assert result.exit_code == 0
    assert "LLM check: skipped" in result.stdout


def test_setup_mode_hermes_writes_hooks_plugin_config_and_connector(fake_home, tmp_path, monkeypatch):
    real_subprocess_run = subprocess.run
    hermes_home = tmp_path / "home"
    config_path = hermes_home / ".hermes" / "config.yaml"
    project_dir = tmp_path / "hermes-project"
    config_path.parent.mkdir(parents=True)
    project_dir.mkdir()
    (hermes_home / ".openclaw").mkdir(parents=True)
    (hermes_home / ".openclaw" / "gateway.systemd.env").write_text(
        'GOG_KEYRING_PASSWORD="test-gog-password"\nGOG_ACCOUNT="openclaw.test.2025@gmail.com"\n',
        encoding="utf-8",
    )
    (hermes_home / ".hermes" / "skills" / "email" / "himalaya").mkdir(parents=True)
    config_path.write_text(
        """
model:
  default: gpt-5-mini-2025-08-07
auxiliary:
  compression:
    provider: openai-codex
  title_generation:
    provider: openai-codex
  session_search:
    provider: openai-codex
""",
        encoding="utf-8",
    )
    monkeypatch.setenv("HOME", str(hermes_home))
    monkeypatch.setenv("HERMES_TELEGRAM_BOT_TOKEN", "999:hermes")
    monkeypatch.setattr(cli_main, "has_command", lambda command: command == "systemctl")
    monkeypatch.setattr(cli_main, "_run_systemctl_user", lambda args: (True, "ok"))
    monkeypatch.setattr(cli_main, "_register_telegram_commands_from_env", lambda path: "skipped")
    monkeypatch.setattr(cli_main.subprocess, "run", lambda *args, **kwargs: type("Result", (), {"returncode": 0, "stdout": "", "stderr": ""})())

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "hermes",
            "--hermes-config",
            str(config_path),
            "--hermes-project-dir",
            str(project_dir),
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
            "--no-restart-hermes",
        ],
    )

    assert result.exit_code == 0
    assert "Hermes wired:" in result.stdout
    assert (hermes_home / ".hermes" / "agent-hooks" / "8mem-inject.sh").exists()
    candidate_hook_yaml = (hermes_home / ".hermes" / "hooks" / "8mem-candidate" / "HOOK.yaml").read_text(encoding="utf-8")
    candidate_hook = (hermes_home / ".hermes" / "hooks" / "8mem-candidate" / "handler.py").read_text(encoding="utf-8")
    sync = (hermes_home / ".hermes" / "agent-hooks" / "8mem-sync.sh").read_text(encoding="utf-8")
    write = (hermes_home / ".hermes" / "agent-hooks" / "8mem-write.sh").read_text(encoding="utf-8")
    guard = (hermes_home / ".hermes" / "agent-hooks" / "8mem-guard.sh").read_text(encoding="utf-8")
    approval = (hermes_home / ".hermes" / "agent-hooks" / "8mem-approval.sh").read_text(encoding="utf-8")
    resolve = (hermes_home / ".hermes" / "agent-hooks" / "8mem-resolve.sh").read_text(encoding="utf-8")
    plugin = (hermes_home / ".hermes" / "plugins" / "8mem" / "__init__.py").read_text(encoding="utf-8")
    plugin_yaml = (hermes_home / ".hermes" / "plugins" / "8mem" / "plugin.yaml").read_text(encoding="utf-8")
    hermes_rules = (project_dir / "HERMES.md").read_text(encoding="utf-8")
    hermes_env = (hermes_home / ".hermes" / ".env").read_text(encoding="utf-8")
    gog_skill = (hermes_home / ".hermes" / "skills" / "email" / "gog" / "SKILL.md").read_text(encoding="utf-8")
    config = config_path.read_text(encoding="utf-8")
    connectors = json.loads((fake_home / "connectors.json").read_text(encoding="utf-8"))
    assert "EIGHTMEM_LOCAL_API_KEY" in sync
    assert "EIGHTMEM_LOCAL_API_KEY" in write
    assert "/v1/memory" in write
    assert "explicit_user_statement" in write
    assert (hermes_home / ".hermes" / "agent-hooks" / "8mem-write.sh").stat().st_mode & 0o111
    assert "agent:start" in candidate_hook_yaml
    assert "/v1/memory/candidate" in candidate_hook
    assert "hermes_gateway_candidate" in candidate_hook
    assert 'context.get("platform")' in candidate_hook
    assert 'payload["telegram_chat_id"]' in candidate_hook
    assert "human_description" in guard
    assert "/v1/memory/propose" in guard
    assert "Blocked bulk memory re-save" in guard
    assert "Blocked unsafe 8mem maintenance write path" in guard
    assert "8mem dedupe" in guard
    assert "8mem apply-import-facts" in guard
    assert "Save explicit memory to 8mem" in guard
    assert "Propose inferred memory to 8mem" in guard
    assert (hermes_home / ".hermes" / "agent-hooks" / "8mem-approval.sh").stat().st_mode & 0o111
    assert (hermes_home / ".hermes" / "agent-hooks" / "8mem-resolve.sh").stat().st_mode & 0o111
    assert "BOT_TOKEN_ENV" in approval
    assert "TELEGRAM_APPROVAL_CHAT_ID" in approval
    assert "telegram_bot_token" in approval
    assert "telegram_chat_id" in approval
    assert 'curl -s -X POST "http://127.0.0.1:8787/v1/approval"' in approval
    assert 'curl -s -X POST "http://127.0.0.1:8787/v1/approval/$APPROVAL_ID"' in resolve
    assert "approved|denied|edit" in resolve
    assert "User requested edit" in resolve
    assert "OPENAI_API_KEY" in plugin
    assert "max_completion_tokens" in plugin
    assert "1500" in plugin
    assert "def _format_passport" in plugin
    assert "def _active_beliefs" in plugin
    assert "What topic should I compare?" in plugin
    assert "should I add one more launch feature tonight" in plugin
    passport_impl = plugin.split("def _passport", 1)[1].split("def _corrections", 1)[0]
    assert "system_prompt_injection" not in passport_impl
    assert "Currently helping with" in plugin
    assert "Recent corrections" in plugin
    assert "Trust check" in plugin
    assert "Hard rules" not in plugin
    assert "No old -> new corrections recorded yet." in plugin
    assert "Can't reach 8mem right now." not in plugin
    assert "8mem is not reachable right now." in plugin
    assert "/v1/passport/card" in plugin
    assert "/v1/compare/card" in plugin
    assert "sendPhoto" in plugin
    assert "TELEGRAM_ALLOWED_USER_ID" in plugin
    assert "channel_directory.json" in plugin
    assert "8mem_hermes_card_error.log" in plugin
    assert "version: 0.2.0" in plugin_yaml
    for command in ("refresh8mem", "passport", "corrections", "compare", "remember", "correct", "forget", "brief", "status", "help"):
        assert f"  - {command}" in plugin_yaml
    assert 'GOG_KEYRING_PASSWORD="test-gog-password"' in hermes_env
    assert 'GOG_ACCOUNT="openclaw.test.2025@gmail.com"' in hermes_env
    assert 'TELEGRAM_BOT_TOKEN="999:hermes"' in hermes_env
    assert "TELEGRAM_APPROVAL_CHAT_ID" not in hermes_env
    assert "approval test  -> skipped: missing agent Telegram approval chat id" in result.stdout
    assert "gog gmail send" in gog_skill
    assert "Himalaya" in gog_skill
    assert not (hermes_home / ".hermes" / "skills" / "email" / "himalaya").exists()
    assert (hermes_home / ".hermes" / "skills" / "email" / "himalaya.disabled").exists()
    assert "8mem managed block: 8mem-hermes-integration" in hermes_rules
    assert "`/passport`, `/compare`, `/corrections`, and `/refresh8mem` are read-only commands" in hermes_rules
    assert "Do not offer to save memory during unrelated action requests" in hermes_rules
    assert "Do not echo raw injected context" in hermes_rules
    assert "Do not save anything from the passport output" in hermes_rules
    assert "MANUAL SAVE fallback" in hermes_rules
    assert "$HOME/.hermes/agent-hooks/8mem-write.sh save" in hermes_rules
    assert 'reply exactly `Saved.`' in hermes_rules
    assert "`duplicate` -> reply exactly `Already in memory.`" in hermes_rules
    assert "`skipped` -> reply exactly `Already tracked.`" in hermes_rules
    assert "reply exactly `Kept existing.`" in hermes_rules
    assert "must never use the external-action approval flow" in hermes_rules
    assert "unless the exact 8mem API status confirms it" in hermes_rules
    assert "8mem Approval Flow" in hermes_rules
    assert "$HOME/.hermes/agent-hooks/8mem-approval.sh" in hermes_rules
    assert "$HOME/.hermes/agent-hooks/8mem-resolve.sh" in hermes_rules
    assert "never write inline curl for approvals" in hermes_rules
    assert "never read TELEGRAM_APPROVAL_CHAT_ID or TELEGRAM_BOT_TOKEN yourself" in hermes_rules
    assert "curl -s -X POST \"http://127.0.0.1:8787/v1/approval\"" not in hermes_rules
    assert "TELEGRAM_CHAT_ID=$(grep" not in hermes_rules
    assert "gog gmail send --to" in hermes_rules
    assert "Do not use sendmail, msmtp, mail, or Himalaya." in hermes_rules
    assert "/home/ishaan" not in hermes_rules
    assert "For bare `/compare` with no topic" in hermes_rules
    assert "Do not save anything from the compare output" in hermes_rules
    assert "it came from 8mem shared memory available to this agent" in hermes_rules
    assert 'Do not call injected 8mem context "Hermes internal memory"' in hermes_rules
    assert "Inferred durable memories must be proposed first" in hermes_rules
    assert "`POST /v1/memory/propose` creates a pending candidate only" in hermes_rules
    assert "Do NOT use the native memory tool for inferred preferences" in hermes_rules
    assert "Route it to one proposal before any maintenance, import, or diagnostic command" in hermes_rules
    assert "Do NOT run `8mem dedupe`, `8mem apply-import-facts`" in hermes_rules
    assert "Do NOT ask a free-form confirmation, clarification, or native approval before the propose call." in hermes_rules
    assert "If the memory tool is blocked for bulk/passport/context re-save, do not retry" in hermes_rules
    assert "Only after propose returns `status: proposed`" in hermes_rules
    assert "Propose exactly one new user-stated fact" in hermes_rules
    assert "Pending candidates are not saved active memory" in hermes_rules
    assert "provider: custom" in config
    assert "memory_enabled: false" in config
    assert connectors[0]["id"] == "hermes"
    assert connectors[0]["url"] == "http://127.0.0.1:8766/8mem/sync"
    receiver = fake_home / "webhook-receiver.py"
    receiver_env = fake_home / "agents" / "receiver.env"
    service = hermes_home / ".config" / "systemd" / "user" / "8mem-agent-sync-receiver.service"
    assert receiver.exists()
    receiver_text = receiver.read_text(encoding="utf-8")
    compile(receiver_text, str(receiver), "exec")
    assert "$HOME/.hermes/8mem-context.md" in receiver_text
    assert "system_prompt_injection" in receiver_text
    assert "EIGHTMEM_HERMES_SYNC_SCRIPT" in receiver_env.read_text(encoding="utf-8")
    assert "webhook-receiver.py" in service.read_text(encoding="utf-8")

    blocked = real_subprocess_run(
        [str(hermes_home / ".hermes" / "agent-hooks" / "8mem-guard.sh")],
        input=json.dumps(
            {
                "tool_name": "terminal_tool",
                "tool_input": {"command": "/home/user/.local/bin/8mem dedupe --user-id 5989413668"},
            }
        ),
        capture_output=True,
        text=True,
        check=False,
    )
    assert blocked.returncode == 0
    directive = json.loads(blocked.stdout)
    assert directive["action"] == "block"
    assert directive["human_description"] == "Blocked unsafe 8mem maintenance write path"
    assert "/v1/memory/propose" in directive["message"]


def test_hermes_approval_test_values_require_agent_chat_id(fake_home):
    runtime_env_path().parent.mkdir(parents=True, exist_ok=True)
    runtime_env_path().write_text(
        'EIGHTMEM_LOCAL_API_KEY="local-runtime-key"\n'
        'TELEGRAM_APPROVAL_CHAT_ID="viri-chat"\n',
        encoding="utf-8",
    )
    hermes_env = fake_home / ".hermes" / ".env"
    hermes_env.parent.mkdir(parents=True, exist_ok=True)
    hermes_env.write_text(
        'TELEGRAM_BOT_TOKEN="999:hermes"\n',
        encoding="utf-8",
    )

    local_api_key, chat_id, bot_token = cli_main._approval_test_values(hermes_env, require_agent_chat_id=True)

    assert local_api_key == "local-runtime-key"
    assert chat_id == ""
    assert bot_token == "999:hermes"


def test_hermes_approval_test_values_uses_hermes_chat_id(fake_home):
    runtime_env_path().parent.mkdir(parents=True, exist_ok=True)
    runtime_env_path().write_text(
        'EIGHTMEM_LOCAL_API_KEY="local-runtime-key"\n'
        'TELEGRAM_APPROVAL_CHAT_ID="viri-chat"\n',
        encoding="utf-8",
    )
    hermes_env = fake_home / ".hermes" / ".env"
    hermes_env.parent.mkdir(parents=True, exist_ok=True)
    hermes_env.write_text(
        'TELEGRAM_BOT_TOKEN="999:hermes"\n'
        'TELEGRAM_APPROVAL_CHAT_ID="govi-chat"\n',
        encoding="utf-8",
    )

    local_api_key, chat_id, bot_token = cli_main._approval_test_values(hermes_env, require_agent_chat_id=True)

    assert local_api_key == "local-runtime-key"
    assert chat_id == "govi-chat"
    assert bot_token == "999:hermes"


def test_setup_mode_hermes_remote_primary_requires_key(fake_home, tmp_path, monkeypatch):
    hermes_home = tmp_path / "home"
    config_path = hermes_home / ".hermes" / "config.yaml"
    config_path.parent.mkdir(parents=True)
    config_path.write_text("model:\n  default: gpt-5-mini-2025-08-07\n", encoding="utf-8")
    monkeypatch.setenv("HOME", str(hermes_home))
    monkeypatch.delenv("EIGHTMEM_OPENCLAW_API_KEY", raising=False)

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "hermes",
            "--hermes-config",
            str(config_path),
            "--eightmem-api-url",
            "http://mac.tailnet.example:8787",
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
        ],
    )

    assert result.exit_code == 2
    assert "Hermes setup failed: remote primary memory needs" in result.stdout


def test_setup_mode_openclaw_rejects_invalid_openclaw_json(fake_home, tmp_path):
    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"
    workspace.mkdir()
    config_path.write_text("{not valid json", encoding="utf-8")

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "openclaw",
            "--openclaw-workspace",
            str(workspace),
            "--openclaw-config",
            str(config_path),
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
            "--no-restart-openclaw",
        ],
    )

    assert result.exit_code == 2
    assert "OpenClaw setup failed" in result.stdout
    assert "not valid JSON" in result.stdout


def test_uninstall_mode_openclaw_removes_blocks_and_restores_inline_buttons(fake_home, tmp_path):
    workspace = tmp_path / "workspace"
    config_path = tmp_path / "openclaw.json"
    args = [
        "setup",
        "--non-interactive",
        "--mode",
        "openclaw",
        "--openclaw-workspace",
        str(workspace),
        "--openclaw-config",
        str(config_path),
        "--runtime-api-key",
        "test_runtime_key",
        "--no-restart-openclaw",
    ]
    setup_result = runner.invoke(app, args)
    (workspace / "memory").mkdir(exist_ok=True)
    (workspace / "memory" / "8mem-context.md").write_text("context", encoding="utf-8")

    uninstall_result = runner.invoke(
        app,
        [
            "uninstall",
            "--mode",
            "openclaw",
            "--no-restart-openclaw",
        ],
    )

    assert setup_result.exit_code == 0
    assert uninstall_result.exit_code == 0
    assert "8mem OpenClaw integration removed:" in uninstall_result.stdout
    assert not (workspace / "AGENTS.md").exists()
    assert not (workspace / "HEARTBEAT.md").exists()
    assert not (workspace / "MEMORY-API.md").exists()
    for name in cli_main.OPENCLAW_MEMORY_FLOW_FILES:
        assert not (workspace / "memory" / name).exists()
    assert not (workspace / "memory" / "8mem-context.md").exists()
    openclaw_config = json.loads(config_path.read_text(encoding="utf-8"))
    assert "capabilities" not in openclaw_config["channels"]["telegram"]


def test_setup_interactive_rejects_yes_as_ollama_url(fake_home, monkeypatch):
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(
        app,
        [
            "setup",
            "--runtime-api-key",
            "test_runtime_key",
        ],
        input="1\ny\ny\n\n\n",
    )

    assert result.exit_code == 0
    assert "Ollama URL must be like http://localhost:11434" in result.stdout
    assert "LLM check: passed" in result.stdout
    config = runtime_env_path().read_text(encoding="utf-8")
    assert 'OLLAMA_BASE_URL="http://localhost:11434"' in config
    assert 'OLLAMA_BASE_URL="y"' not in config


def test_setup_normalizes_ollama_url_without_scheme(fake_home, monkeypatch):
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--skip-telegram",
            "--llm-base-url",
            "localhost:11434",
            "--runtime-api-key",
            "test_runtime_key",
        ],
    )

    assert result.exit_code == 0
    config = runtime_env_path().read_text(encoding="utf-8")
    assert 'OLLAMA_BASE_URL="http://localhost:11434"' in config


def test_setup_sanitizes_bad_ollama_env_defaults(fake_home, monkeypatch):
    monkeypatch.setenv("OLLAMA_BASE_URL", "n")
    monkeypatch.setenv("DEFAULT_LLM_FALLBACK_MODEL", "y")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(
        app,
        [
            "setup",
            "--runtime-api-key",
            "test_runtime_key",
        ],
        input="1\ny\n\n\n",
    )

    assert result.exit_code == 0
    assert "Ollama base URL [http://localhost:11434]" in result.stdout
    assert "Default local model [llama3.2:latest]" in result.stdout
    config = runtime_env_path().read_text(encoding="utf-8")
    assert 'OLLAMA_BASE_URL="http://localhost:11434"' in config
    assert 'DEFAULT_LLM_FALLBACK_MODEL="llama3.2:latest"' in config


def test_ollama_check_reports_invalid_url_without_traceback():
    ok, error = check_backend(base_url="y")

    assert ok is False
    assert error is not None
    assert "Invalid Ollama base URL" in error


def test_doctor_json_reports_ready_with_warnings(fake_home, monkeypatch):
    monkeypatch.delenv("TELEGRAM_BOT_TOKEN", raising=False)
    monkeypatch.delenv("TELEGRAM_WEBHOOK_SECRET", raising=False)
    monkeypatch.delenv("EIGHTMEM_LOCAL_API_KEY", raising=False)
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    setup_result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--skip-telegram",
            "--runtime-api-key",
            "test_runtime_key",
            "--skip-llm-check",
        ],
    )
    assert setup_result.exit_code == 0

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    assert payload["ok"] is True
    assert payload["status"] == "ready"
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["runtime_home"]["status"] == "pass"
    assert checks["memory_files"]["status"] == "pass"
    assert checks["runtime_config"]["status"] == "pass"
    assert checks["runtime_api_auth"]["status"] == "pass"
    assert checks["semantic_retrieval"]["status"] in {"pass", "warn"}
    assert checks["v1_context"]["status"] == "pass"
    assert checks["telegram"]["status"] == "warn"
    assert checks["telegram_webhook"]["status"] == "warn"
    assert "Fix:" in checks["telegram"]["message"]
    assert "Fix:" in checks["telegram_webhook"]["message"]


def test_doctor_checks_registered_telegram_webhook(fake_home, monkeypatch):
    monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "123456:abcdef")
    monkeypatch.setenv("TELEGRAM_WEBHOOK_SECRET", "secret-1")
    monkeypatch.setenv("TELEGRAM_FORWARD_URL", "https://public.example")
    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))
    monkeypatch.setattr(
        cli_main,
        "get_telegram_webhook_info",
        lambda config: {
            "ok": True,
            "result": {"url": "https://public.example/channels/telegram/webhook"},
        },
    )

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["telegram"]["status"] == "pass"
    assert checks["telegram_webhook"]["status"] == "pass"


def test_doctor_reports_sqlite_vec_available(fake_home, monkeypatch):
    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))
    monkeypatch.setattr(cli_main, "sqlite_vec_available", lambda: True)

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["semantic_retrieval"]["status"] == "pass"
    assert "sqlite-vec available" in checks["semantic_retrieval"]["message"]


def test_doctor_reports_sqlite_vec_optional_when_missing(fake_home, monkeypatch):
    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))
    monkeypatch.setattr(cli_main, "sqlite_vec_available", lambda: False)

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["semantic_retrieval"]["status"] == "warn"
    assert "exact SQLite/Markdown memory still works" in checks["semantic_retrieval"]["message"]


def test_doctor_webhook_warning_hides_raw_telegram_payload(fake_home, monkeypatch):
    monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "123456:abcdef")
    monkeypatch.setenv("TELEGRAM_WEBHOOK_SECRET", "secret-1")
    monkeypatch.setenv("TELEGRAM_FORWARD_URL", "https://public.example")
    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))
    monkeypatch.setattr(
        cli_main,
        "get_telegram_webhook_info",
        lambda config: {
            "ok": True,
            "result": {
                "url": "",
                "pending_update_count": 0,
                "allowed_updates": ["message", "callback_query"],
            },
        },
    )

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

    assert result.exit_code == 0
    assert "[WARN] telegram_webhook" in result.stdout
    assert "Fix: run `8mem setup --mode telegram`" in result.stdout
    assert "allowed_updates" not in result.stdout
    assert "pending_update_count" not in result.stdout
    assert "{'ok':" not in result.stdout


def test_setup_webhook_warning_hides_raw_telegram_payload(fake_home, monkeypatch):
    def _fake_set_webhook(config, *, forward_url, timeout_seconds=15):
        return {
            "ok": False,
            "result": {
                "url": "",
                "pending_update_count": 0,
                "allowed_updates": ["message", "callback_query"],
            },
        }

    monkeypatch.setattr(cli_main, "set_telegram_webhook", _fake_set_webhook)
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(
        app,
        [
            "setup",
            "--non-interactive",
            "--mode",
            "telegram",
            "--telegram-token",
            "123456:abcdef",
            "--webhook-secret",
            "secret-1",
            "--telegram-forward-url",
            "https://public.example",
            "--skip-llm-check",
        ],
    )

    assert result.exit_code == 0
    assert "Telegram webhook: warning" in result.stdout
    assert "Fix: run `8mem setup --mode telegram`" in result.stdout
    assert "allowed_updates" not in result.stdout
    assert "pending_update_count" not in result.stdout
    assert "{'ok':" not in result.stdout


def test_doctor_warns_on_invalid_telegram_token(fake_home, monkeypatch):
    monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "not-a-bot-token")
    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["telegram"]["status"] == "warn"
    assert "Invalid TELEGRAM_BOT_TOKEN format" in checks["telegram"]["message"]
    assert "Fix:" in checks["telegram"]["message"]


def test_doctor_warns_when_hermes_callbacks_missing(fake_home, monkeypatch):
    hermes_home = fake_home / ".hermes"
    hooks = hermes_home / "agent-hooks"
    hooks.mkdir(parents=True)
    (hooks / "8mem-approval.sh").write_text("#!/usr/bin/env bash\n", encoding="utf-8")
    project_dir = fake_home / "hermes"
    telegram_py = project_dir / "gateway" / "platforms" / "telegram.py"
    telegram_py.parent.mkdir(parents=True)
    telegram_py.write_text("def handle_callback(data):\n    pass\n", encoding="utf-8")
    monkeypatch.setenv("HOME", str(fake_home))
    monkeypatch.setenv("EIGHTMEM_HERMES_PROJECT_DIR", str(project_dir))
    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["hermes_approval_callbacks"]["status"] == "warn"
    assert "/passport, /compare, and memory flows can work" in checks["hermes_approval_callbacks"]["message"]
    assert "Telegram Approve buttons" in checks["hermes_approval_callbacks"]["message"]
    assert "update Hermes" in checks["hermes_approval_callbacks"]["message"]


def test_doctor_passes_when_hermes_callbacks_present(fake_home, monkeypatch):
    hermes_home = fake_home / ".hermes"
    hooks = hermes_home / "agent-hooks"
    hooks.mkdir(parents=True)
    (hooks / "8mem-approval.sh").write_text("#!/usr/bin/env bash\n", encoding="utf-8")
    project_dir = fake_home / "hermes"
    telegram_py = project_dir / "gateway" / "platforms" / "telegram.py"
    telegram_py.parent.mkdir(parents=True)
    telegram_py.write_text(
        """
async def _handle_callback_query(data):
    if data.startswith("approval:"):
        url = f"http://127.0.0.1:8787/v1/approval/{approval_id}"
        await self.handle_message(_wake_event)
        logger.info("Woke session after 8mem approval")
""",
        encoding="utf-8",
    )
    monkeypatch.setenv("HOME", str(fake_home))
    monkeypatch.setenv("EIGHTMEM_HERMES_PROJECT_DIR", str(project_dir))
    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["hermes_approval_callbacks"]["status"] == "pass"
    assert "wakes the agent session" in checks["hermes_approval_callbacks"]["message"]


def test_doctor_warns_openclaw_approval_callbacks_may_need_manual_fallback(fake_home, monkeypatch):
    manifest = fake_home / "openclaw-install-manifest.json"
    manifest.parent.mkdir(parents=True)
    manifest.write_text(
        json.dumps({"mode": "openclaw", "workspace": str(fake_home / ".openclaw" / "workspace")}),
        encoding="utf-8",
    )
    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["openclaw_approval_callbacks"]["status"] == "warn"
    assert "manual fallback" in checks["openclaw_approval_callbacks"]["message"]
    assert "approval:approved:<id>" in checks["openclaw_approval_callbacks"]["message"]
    assert "Core 8mem memory features still work" in checks["openclaw_approval_callbacks"]["message"]


def test_doctor_warns_when_openclaw_context_file_is_missing(fake_home, monkeypatch):
    workspace = fake_home / ".openclaw" / "workspace"
    manifest = fake_home / "openclaw-install-manifest.json"
    manifest.parent.mkdir(parents=True, exist_ok=True)
    manifest.write_text(json.dumps({"mode": "openclaw", "workspace": str(workspace)}), encoding="utf-8")
    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["openclaw_context_freshness"]["status"] == "warn"
    assert "Agent context file is missing" in checks["openclaw_context_freshness"]["message"]
    assert "/refresh8mem" in checks["openclaw_context_freshness"]["message"]


def test_doctor_passes_when_openclaw_context_file_is_fresh(fake_home, monkeypatch):
    workspace = fake_home / ".openclaw" / "workspace"
    context_file = workspace / "memory" / "8mem-context.md"
    context_file.parent.mkdir(parents=True)
    context_file.write_text("User is Test (UTC).", encoding="utf-8")
    manifest = fake_home / "openclaw-install-manifest.json"
    manifest.write_text(json.dumps({"mode": "openclaw", "workspace": str(workspace)}), encoding="utf-8")
    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["openclaw_context_freshness"]["status"] == "pass"
    assert "Agent context file is fresh" in checks["openclaw_context_freshness"]["message"]


def test_doctor_warns_when_agent_sync_connector_missing(fake_home, monkeypatch):
    workspace = fake_home / ".openclaw" / "workspace"
    manifest = fake_home / "openclaw-install-manifest.json"
    manifest.parent.mkdir(parents=True, exist_ok=True)
    manifest.write_text(json.dumps({"mode": "openclaw", "workspace": str(workspace)}), encoding="utf-8")
    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["agent_sync_connector"]["status"] == "warn"
    assert "Real-time memory sharing may lag" in checks["agent_sync_connector"]["message"]
    assert "rerun `8mem setup --mode hermes` or `8mem setup --mode openclaw`" in checks["agent_sync_connector"]["message"]


def test_doctor_passes_when_agent_sync_receiver_accepts_signed_ping(fake_home, monkeypatch):
    workspace = fake_home / ".openclaw" / "workspace"
    manifest = fake_home / "openclaw-install-manifest.json"
    manifest.parent.mkdir(parents=True, exist_ok=True)
    manifest.write_text(json.dumps({"mode": "openclaw", "workspace": str(workspace)}), encoding="utf-8")
    connector, error = cli_main.register_connector(
        connector_id=cli_main.AGENT_SYNC_CONNECTOR_ID,
        url="http://127.0.0.1:18792/8mem/sync",
        secret="test-secret",
    )
    assert connector is not None
    assert error is None

    class _Response:
        status = 200

        def __enter__(self):
            return self

        def __exit__(self, *_args):
            return False

        def getcode(self):
            return self.status

    seen = {}

    def _fake_urlopen(request, timeout):
        seen["url"] = request.full_url
        seen["signature"] = request.headers.get("X-8mem-signature")
        seen["timeout"] = timeout
        return _Response()

    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))
    monkeypatch.setattr(cli_main, "urlopen", _fake_urlopen)

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["agent_sync_connector"]["status"] == "pass"
    assert checks["agent_sync_receiver"]["status"] == "pass"
    assert seen["url"] == "http://127.0.0.1:18792/8mem/sync"
    assert seen["signature"]
    assert seen["timeout"] == 2


def test_doctor_warns_when_memory_health_needs_review(fake_home, monkeypatch):
    monkeypatch.setenv("EIGHTMEM_LOCAL_API_KEY", "test_runtime_key")
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))
    monkeypatch.setattr(
        cli_main,
        "preview_memory_dedup",
        lambda: {"exact_duplicate_count": 2, "near_duplicate_count": 1},
    )
    monkeypatch.setattr(cli_main, "get_structured_tensions", lambda: ["Preference guidance conflicts."])

    result = runner.invoke(app, ["doctor", "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    checks = {item["name"]: item for item in payload["checks"]}
    assert checks["memory_health"]["status"] == "warn"
    assert "2 exact duplicate memory entries" in checks["memory_health"]["message"]
    assert "1 near-duplicate review item" in checks["memory_health"]["message"]
    assert "1 possible conflict" in checks["memory_health"]["message"]
    assert "No automatic cleanup was applied" in checks["memory_health"]["message"]


def test_doctor_text_uses_nonzero_exit_for_context_failure(fake_home, monkeypatch):
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (True, None))
    monkeypatch.setattr(cli_main, "build_engram_context", lambda: {"system_prompt_injection": ""})

    result = runner.invoke(app, ["doctor", "--skip-llm-check"])

    assert result.exit_code == 1
    assert "[FAIL] v1_context" in result.stdout
    assert "Fix:" in result.stdout


def test_doctor_text_gives_plain_english_recovery_steps(fake_home, monkeypatch):
    monkeypatch.delenv("TELEGRAM_BOT_TOKEN", raising=False)
    monkeypatch.delenv("TELEGRAM_FORWARD_URL", raising=False)
    monkeypatch.delenv("EIGHTMEM_LOCAL_API_KEY", raising=False)
    monkeypatch.setattr(cli_main, "check_ollama_backend", lambda **_: (False, "Ollama connection refused"))

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

    assert result.exit_code == 0
    assert "Fix: run `8mem setup`" in result.stdout
    assert "Fix: run `8mem setup --mode telegram`" in result.stdout
    assert "Fix: start Ollama" in result.stdout
    assert "stack trace" not in result.stdout.lower()


def test_forget_cli_deletes_memory_with_audit(fake_home):
    init_result = runner.invoke(app, ["init"])
    assert init_result.exit_code == 0
    preferences = fake_home / "memory" / "PREFERENCES.md"
    preferences.write_text(
        "# PREFERENCES\n\nStable response and formatting preferences.\n\n## Entries\n\n- Keep answers concise.\n",
        encoding="utf-8",
    )

    result = runner.invoke(app, ["forget", "concise", "--yes"])

    assert result.exit_code == 0
    assert "Deleted 1 memory entry." in result.stdout
    assert "Keep answers concise." not in preferences.read_text(encoding="utf-8")
    evolution = (fake_home / "memory" / "EVOLUTION.md").read_text(encoding="utf-8")
    assert 'Deleted memory from Preferences: "Keep answers concise."' in evolution
    events = (fake_home / "memory" / ".memory_events.jsonl").read_text(encoding="utf-8")
    assert '"action": "forget_memory"' in events


def test_start_background_writes_pid_and_log_paths(fake_home, monkeypatch):
    started: dict[str, object] = {}

    class _FakeProcess:
        pid = 12345

    def _fake_popen(command, **kwargs):
        started["command"] = command
        started["kwargs"] = kwargs
        return _FakeProcess()

    monkeypatch.setattr(cli_main, "_pid_is_running", lambda pid: False)
    monkeypatch.setattr(cli_main, "_port_is_in_use", lambda host, port: False)
    monkeypatch.setattr(cli_main, "_systemd_user_service", lambda: None)
    monkeypatch.setattr(cli_main, "_wait_for_readyz", lambda host, port: (True, f"http://{host}:{port}/readyz"))
    monkeypatch.setattr(cli_main.subprocess, "Popen", _fake_popen)

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

    assert result.exit_code == 0
    assert "8mem running at http://0.0.0.0:8787" in result.stdout
    assert (fake_home / "8mem.pid").read_text(encoding="utf-8") == "12345\n"
    assert (fake_home / "8mem.log").exists()
    assert started["command"][0]
    assert started["command"][-5:] == ["serve", "--host", "0.0.0.0", "--port", "8787"]


def test_start_refuses_unmanaged_process_on_port(fake_home, monkeypatch):
    monkeypatch.setattr(cli_main, "_pid_is_running", lambda pid: False)
    monkeypatch.setattr(cli_main, "_port_is_in_use", lambda host, port: True)
    monkeypatch.setattr(cli_main, "_systemd_user_service", lambda: None)

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

    assert result.exit_code == 1
    assert "Port 8787 is already in use on 0.0.0.0" in result.stdout
    assert "lsof -nP -iTCP:8787 -sTCP:LISTEN" in result.stdout
    assert not (fake_home / "8mem.pid").exists()


def test_status_reports_running_and_ready(fake_home, monkeypatch):
    fake_home.mkdir(parents=True, exist_ok=True)
    (fake_home / "8mem.pid").write_text("12345\n", encoding="utf-8")
    monkeypatch.setattr(cli_main, "_pid_is_running", lambda pid: pid == 12345)
    monkeypatch.setattr(cli_main, "_systemd_user_service", lambda: None)
    monkeypatch.setattr(cli_main, "_probe_readyz", lambda host, port: (True, f"http://{host}:{port}/readyz"))

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

    assert result.exit_code == 0
    assert "Status: running" in result.stdout
    assert "PID: 12345" in result.stdout
    assert "URL: http://0.0.0.0:8787" in result.stdout
    assert "readyz: pass - http://0.0.0.0:8787/readyz" in result.stdout


def test_stop_terminates_running_background_process(fake_home, monkeypatch):
    fake_home.mkdir(parents=True, exist_ok=True)
    (fake_home / "8mem.pid").write_text("12345\n", encoding="utf-8")
    calls: list[int] = []
    running_checks = iter([True, False])

    def _fake_pid_is_running(pid):
        return next(running_checks)

    monkeypatch.setattr(cli_main, "_pid_is_running", _fake_pid_is_running)
    monkeypatch.setattr(cli_main, "_systemd_user_service", lambda: None)
    monkeypatch.setattr(cli_main.os, "kill", lambda pid, sig: calls.append(pid))

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

    assert result.exit_code == 0
    assert calls == [12345]
    assert "8mem stopped (pid 12345)." in result.stdout
    assert not (fake_home / "8mem.pid").exists()


def test_start_detects_active_systemd_service(fake_home, monkeypatch):
    monkeypatch.setattr(cli_main, "_systemd_user_service", lambda: {"ActiveState": "active", "MainPID": "222"})

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

    assert result.exit_code == 0
    assert "8mem already running at http://0.0.0.0:8787 (systemd, pid 222)" in result.stdout


def test_start_uses_systemd_when_service_is_installed_but_stopped(fake_home, monkeypatch):
    calls: list[str] = []
    monkeypatch.setattr(cli_main, "_systemd_user_service", lambda: {"ActiveState": "inactive", "MainPID": "0"})
    monkeypatch.setattr(cli_main, "_run_systemd_user_action", lambda action: (calls.append(action) or True, ""))
    monkeypatch.setattr(cli_main, "_wait_for_readyz", lambda host, port: (True, f"http://{host}:{port}/readyz"))

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

    assert result.exit_code == 0
    assert calls == ["start"]
    assert "8mem running at http://0.0.0.0:8787 (systemd)" in result.stdout


def test_status_reports_systemd_state(fake_home, monkeypatch):
    monkeypatch.setattr(cli_main, "_systemd_user_service", lambda: {"ActiveState": "active", "MainPID": "333"})
    monkeypatch.setattr(cli_main, "_probe_readyz", lambda host, port: (True, f"http://{host}:{port}/readyz"))

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

    assert result.exit_code == 0
    assert "Status: running (systemd)" in result.stdout
    assert "PID: 333" in result.stdout
    assert "URL: http://0.0.0.0:8787" in result.stdout


def test_stop_uses_systemd_when_service_is_active(fake_home, monkeypatch):
    calls: list[str] = []
    monkeypatch.setattr(cli_main, "_systemd_user_service", lambda: {"ActiveState": "active", "MainPID": "444"})
    monkeypatch.setattr(cli_main, "_run_systemd_user_action", lambda action: (calls.append(action) or True, ""))

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

    assert result.exit_code == 0
    assert calls == ["stop"]
    assert "8mem stopped (systemd)." in result.stdout
