"""Tests for the turnkey `mempalace serve` command (#1877).

These exercise the wrapper's security-relevant behavior — token autogeneration
and 0600 persistence, the secure-by-default non-loopback gate, and that the
bearer token is passed via the environment (never argv, so it can't leak via
``ps``) — without binding a real socket. ``cmd_serve`` ends by exec'ing the real
server; we intercept ``os.execve`` to capture the child invocation instead.
"""

import argparse
import os
import stat

import pytest

from mempalace import cli


class _ExecCalled(Exception):
    """Raised by the patched os.execve to stop cmd_serve at the exec boundary."""


@pytest.fixture
def isolated_home(tmp_path, monkeypatch):
    """Point ~ at a temp dir so server token state never touches the real home."""
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setenv("USERPROFILE", str(tmp_path))  # Windows
    # Don't inherit a token from the ambient environment.
    monkeypatch.delenv("MEMPALACE_MCP_HTTP_TOKEN", raising=False)
    return tmp_path


@pytest.fixture
def capture_exec(monkeypatch):
    """Capture the child argv/env cmd_serve would launch, instead of running it.

    cmd_serve takes the os.execve branch on POSIX and the subprocess.run branch
    on Windows. Patch both (rather than forcing os.name, which breaks
    Path.home() on Windows) so the test is platform-agnostic.
    """
    import subprocess

    captured = {}

    def _capture(argv, env):
        captured["argv"] = argv
        captured["env"] = env
        raise _ExecCalled()

    monkeypatch.setattr(cli.os, "execve", lambda path, argv, env: _capture(argv, env))
    monkeypatch.setattr(subprocess, "run", lambda argv, env=None, **kw: _capture(argv, env))
    return captured


def _serve_args(tmp_path, **over):
    base = dict(
        host="127.0.0.1",
        port=8765,
        backend=None,
        global_backend=None,
        palace=str(tmp_path / "palace"),
        token=None,
        tls_cert=None,
        tls_key=None,
        read_only=False,
        allow_insecure=False,
    )
    base.update(over)
    return argparse.Namespace(**base)


def test_token_helper_creates_0600_and_reuses(isolated_home):
    palace = str(isolated_home / "palace")
    token1, created1 = cli._load_or_create_server_token(palace)
    assert created1 is True
    assert token1

    path = cli._server_token_path(palace)
    assert path.exists()
    if os.name == "posix":
        # POSIX permission bits aren't meaningful on Windows (files report 0o666).
        mode = stat.S_IMODE(path.stat().st_mode)
        assert mode == 0o600, oct(mode)
        dir_mode = stat.S_IMODE(path.parent.stat().st_mode)
        assert dir_mode == 0o700, oct(dir_mode)

    token2, created2 = cli._load_or_create_server_token(palace)
    assert created2 is False
    assert token2 == token1  # stable across restarts


def test_loopback_serve_needs_no_token(isolated_home, capture_exec):
    with pytest.raises(_ExecCalled):
        cli.cmd_serve(_serve_args(isolated_home, host="127.0.0.1"))
    env = capture_exec["env"]
    assert "MEMPALACE_MCP_HTTP_TOKEN" not in env
    assert "MEMPALACE_MCP_HTTP_ALLOW_INSECURE_NO_TOKEN" not in env
    # No token persisted for a loopback bind.
    assert not cli._server_token_path(str(isolated_home / "palace")).exists()


def test_non_loopback_autogenerates_token_in_env_not_argv(isolated_home, capture_exec):
    with pytest.raises(_ExecCalled):
        cli.cmd_serve(_serve_args(isolated_home, host="0.0.0.0"))
    env = capture_exec["env"]
    argv = capture_exec["argv"]
    token = env.get("MEMPALACE_MCP_HTTP_TOKEN")
    assert token, "a token must be generated for a network-exposed bind"
    # Security: the token rides in the env, never on the command line.
    assert all(token not in part for part in argv)
    assert "--token" not in argv
    # And it was persisted for reuse on the next start (0600 on POSIX).
    path = cli._server_token_path(str(isolated_home / "palace"))
    assert path.exists()
    if os.name == "posix":
        assert stat.S_IMODE(path.stat().st_mode) == 0o600


def test_allow_insecure_skips_token_and_sets_escape_hatch(isolated_home, capture_exec):
    with pytest.raises(_ExecCalled):
        cli.cmd_serve(_serve_args(isolated_home, host="0.0.0.0", allow_insecure=True))
    env = capture_exec["env"]
    assert env.get("MEMPALACE_MCP_HTTP_ALLOW_INSECURE_NO_TOKEN") == "1"
    assert "MEMPALACE_MCP_HTTP_TOKEN" not in env
    assert not cli._server_token_path(str(isolated_home / "palace")).exists()


def test_read_only_flag_forwarded_to_child(isolated_home, capture_exec):
    with pytest.raises(_ExecCalled):
        cli.cmd_serve(_serve_args(isolated_home, read_only=True))
    assert "--read-only" in capture_exec["argv"]


def test_explicit_token_is_used_and_not_in_argv(isolated_home, capture_exec):
    with pytest.raises(_ExecCalled):
        cli.cmd_serve(_serve_args(isolated_home, host="0.0.0.0", token="my-secret-token"))
    env = capture_exec["env"]
    argv = capture_exec["argv"]
    assert env["MEMPALACE_MCP_HTTP_TOKEN"] == "my-secret-token"
    assert all("my-secret-token" not in part for part in argv)
    # An explicitly-provided token is not persisted to the server token file.
    assert not cli._server_token_path(str(isolated_home / "palace")).exists()


def test_tls_paths_forwarded_and_validated(isolated_home, capture_exec, tmp_path):
    cert = tmp_path / "cert.pem"
    key = tmp_path / "key.pem"
    cert.write_text("x")
    key.write_text("x")
    with pytest.raises(_ExecCalled):
        cli.cmd_serve(_serve_args(isolated_home, tls_cert=str(cert), tls_key=str(key)))
    argv = capture_exec["argv"]
    assert "--tls-cert" in argv and "--tls-key" in argv


def test_tls_requires_both_cert_and_key(isolated_home, capture_exec, tmp_path):
    cert = tmp_path / "cert.pem"
    cert.write_text("x")
    with pytest.raises(SystemExit):
        cli.cmd_serve(_serve_args(isolated_home, tls_cert=str(cert), tls_key=None))
