#!/usr/bin/env python3
"""test_ops_api.py — Phase 0 (return-not-print) contract: every high-level op has a
core `*_op` function that RETURNS an envelope dict and prints nothing to stdout.

This is the seam the MCP server wraps (docs/MCP-SERVER-SPEC.md §4): the CLI wrappers
are tested by the other suites; here we call the cores in-process against the mock and
assert (a) the envelope shapes and (b) stdout purity — a stray print() would corrupt
an MCP stdio/HTTP response stream.

Run:  python test_ops_api.py [--port 8850]
"""
from __future__ import annotations

import argparse
import contextlib
import io
import json
import os
import subprocess
import sys
import tempfile
import time
import urllib.request
from pathlib import Path

HERE = Path(__file__).resolve().parent
SCRIPTS = HERE.parent / "echo-memory.plugin.src" / "skills" / "echo-memory" / "scripts"
KEY = "test-key-not-a-real-secret"

ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, default=8850)
a = ap.parse_args()
BASE = f"http://127.0.0.1:{a.port}"

# env must be set BEFORE importing echo (it resolves config at import time)
os.environ.update(ECHO_BASE=BASE, ECHO_KEY=KEY, ECHO_TODAY="2026-06-21",
                  ECHO_NOW="2300", ECHO_STATE_DIR=tempfile.mkdtemp(), ECHO_VERIFY="0")
sys.path.insert(0, str(SCRIPTS))

failures = []


def check(name, cond, detail=""):
    print(f"{'ok  ' if cond else 'FAIL'} {name}" + (f"  -- {detail}" if not cond else ""))
    if not cond:
        failures.append(name)


def http(method, url, body=None):
    data = body.encode() if isinstance(body, str) else body
    req = urllib.request.Request(url, data=data, method=method,
                                 headers={"Authorization": f"Bearer {KEY}"})
    try:
        with urllib.request.urlopen(req, timeout=10) as r:
            return r.status, r.read().decode("utf-8", "replace")
    except Exception as e:  # noqa: BLE001
        return getattr(e, "code", 0), ""


def pure(fn, *args, **kw):
    """Call fn capturing stdout; return (result, captured_stdout)."""
    buf = io.StringIO()
    with contextlib.redirect_stdout(buf):
        out = fn(*args, **kw)
    return out, buf.getvalue()


def main():
    srv = subprocess.Popen([sys.executable, str(HERE / "mock_olrapi.py"), "--port", str(a.port)],
                           stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    try:
        for _ in range(50):
            try:
                urllib.request.urlopen(f"{BASE}/__debug__reset", data=b"", timeout=1)
                break
            except Exception:
                time.sleep(0.1)
        http("PUT", f"{BASE}/vault/_agent/echo-vault.md", "---\nschema_version: 4\n---\n# marker\n")
        http("PUT", f"{BASE}/vault/_agent/context/current-context.md",
             "---\ntype: context-bundle\nscope_updated: \"2026-06-20\"\ncreated: 2026-06-01\n---\n"
             "# Current Context\n\n## Scope\ntesting phase 0\n\n## Scope History\n")

        import echo
        import echo_doctor
        import echo_ops
        import echo_recall
        import echo_reflect
        import echo_session
        import echo_triage

        # capture_op: create / duplicate-gate / dry-run — envelopes, stdout pure
        env, out = pure(echo_ops.capture_op, "person", "Zara Quix", body_text="Met at the expo.")
        check("capture_op create envelope", env.get("ok") is True and env.get("action") == "created"
              and env.get("path") == "resources/people/zara-quix.md", json.dumps(env))
        check("capture_op prints nothing to stdout", out == "", out[:200])
        env, out = pure(echo_ops.capture_op, "person", "Zara Quix Junior", body_text="")
        check("capture_op duplicate-gate is data, not an exit code",
              env.get("ok") is False and env.get("action") == "duplicate-gate"
              and env.get("candidates"), json.dumps(env))
        check("capture_op gate prints nothing", out == "", out[:200])
        env, out = pure(echo_ops.capture_op, "company", "Plan Co", body_text="", dry_run=True)
        check("capture_op dry-run envelope", env.get("action") == "dry-run:create"
              and env.get("path") == "resources/companies/plan-co.md", json.dumps(env))

        # resolve_op / link_op
        env, out = pure(echo_ops.resolve_op, "zara quix")
        check("resolve_op returns the match dict", env.get("match") is True
              and env.get("path") == "resources/people/zara-quix.md", json.dumps(env))
        pure(echo_ops.capture_op, "concept", "Gizmo", body_text="")
        env, out = pure(echo_ops.link_op, "resources/people/zara-quix.md", "resources/concepts/gizmo.md")
        check("link_op envelope", env.get("ok") is True and env.get("a_changed") in (True, False),
              json.dumps(env))
        check("link_op prints nothing", out == "", out[:200])

        # recall_op
        env, out = pure(echo_recall.recall_op, "expo")
        check("recall_op envelope shape", env.get("action") == "recall"
              and isinstance(env.get("primary"), list) and isinstance(env.get("linked"), list),
              json.dumps(env)[:200])
        check("recall_op prints nothing", out == "", out[:200])

        # triage list_op / route_op (dry-run)
        http("PUT", f"{BASE}/vault/inbox/captures/inbox.md", "- 2026-06-10: try the quorlab tool\n")
        env, out = pure(echo_triage.list_op)
        check("triage list_op envelope", env.get("count") == 1
              and env["items"][0]["age_days"] == 11, json.dumps(env))
        props = [{"title": "Quorlab Tool", "kind": "reference", "confidence": 0.9,
                  "line": "- 2026-06-10: try the quorlab tool"}]
        env, out = pure(echo_triage.route_op, props, False)
        check("triage route_op dry-run rows", env.get("dry_run") is True
              and env["rows"][0]["action"] == "create", json.dumps(env))
        check("triage route_op prints nothing", out == "", out[:200])

        # reflect apply_op (applied path, using capture_op internally)
        env, out = pure(echo_reflect.apply_op,
                        [{"title": "Blorp Pattern", "kind": "semantic",
                          "body": "The operator prefers blorp.", "confidence": 0.9}], True)
        check("reflect apply_op applies via capture_op", env.get("applied") == 1
              and env["results"][0]["action"] == "created", json.dumps(env))
        check("reflect apply_op prints nothing", out == "", out[:200])

        # scope ops
        env, out = pure(echo.scope_show_op)
        check("scope_show_op returns scope + freshness", env.get("scope") == "testing phase 0"
              and env.get("scope_updated") == "2026-06-20", json.dumps(env))
        env, out = pure(echo.scope_set_op, "phase zero refactor")
        check("scope_set_op envelope", env.get("action") == "scope-set"
              and env.get("scope_updated") == "2026-06-21", json.dumps(env))
        check("scope_set_op prints nothing", out == "", out[:200])

        # load_op (brief)
        env, out = pure(echo.load_op, True)
        check("load_op returns sections + brief", "marker" in env.get("sections", {})
              and "ECHO load (brief)" in env.get("brief", ""), json.dumps(env)[:200])
        check("load_op prints nothing", out == "", out[:200])

        # doctor run_op
        env, out = pure(echo_doctor.run_op)
        check("doctor run_op checks list", isinstance(env.get("checks"), list)
              and env.get("fatal") is None and env.get("ok") is True, json.dumps(env))
        check("doctor run_op prints nothing", out == "", out[:200])

        # session_end_op: dry-run envelope; bad bundle raises EchoError(2) pre-write
        bundle = {"slug": "phase-zero", "log_body": "---\ntype: session-log\n---\n# S\n\n## Goal\nx\n"}
        env, out = pure(echo_session.session_end_op, bundle, False)
        check("session_end_op dry-run envelope", env.get("dry_run") is True
              and env.get("path") == "_agent/sessions/2026-06-21-2300-phase-zero.md", json.dumps(env))
        check("session_end_op prints nothing", out == "", out[:200])
        try:
            echo_session.session_end_op({"slug": "Bad Slug!", "log_body": "x"}, True)
            check("session_end_op rejects a bad slug", False, "no exception raised")
        except RuntimeError as exc:
            check("session_end_op rejects a bad slug", getattr(exc, "code", 0) == 2, str(exc))

        print(f"\n{len(failures)} failure(s)" if failures else "\nall ops-api tests passed")
        return 1 if failures else 0
    finally:
        srv.terminate()


if __name__ == "__main__":
    raise SystemExit(main())
