#!/usr/bin/env python3
"""L1 smoke tests for scripts/ops/gitea CLI.

Run:
  python3 testing/ops/gitea.test.py

Hits the real Gitea API for read-only operations (requires GITEA_API_TOKEN).
Skips network tests if token absent. Mutation tests are dry-run only.
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[2]
CLI = REPO_ROOT / "scripts" / "ops" / "gitea"

passed = 0
failed = 0


def run(args: list[str], env_extra: dict | None = None,
        stdin: str | None = None) -> tuple[int, str, str]:
    env = dict(os.environ)
    if env_extra:
        env.update({k: v for k, v in env_extra.items() if v is not None})
        for k, v in env_extra.items():
            if v is None:
                env.pop(k, None)
    p = subprocess.run(
        [str(CLI), *args], capture_output=True, text=True, env=env,
        input=stdin, timeout=30,
    )
    return p.returncode, p.stdout, p.stderr


def assert_eq(actual, expected, label: str) -> None:
    global passed, failed
    if actual == expected:
        passed += 1
        print(f"  ✓ {label}")
    else:
        failed += 1
        print(f"  ✗ {label}: expected {expected!r}, got {actual!r}")


def assert_contains(haystack: str, needle: str, label: str) -> None:
    global passed, failed
    if needle in haystack:
        passed += 1
        print(f"  ✓ {label}")
    else:
        failed += 1
        print(f"  ✗ {label}: {needle!r} not in output")


# ---- T1: --help works without token ----
print("[T1] --help works without token, exit=0")
code, out, _ = run(["--help"], env_extra={"GITEA_API_TOKEN": None,
                                           "GITEA_TOKEN": None})
assert_eq(code, 0, "exit code 0")
assert_contains(out, "usage: gitea", "help contains 'usage: gitea'")
assert_contains(out, "issue", "help lists 'issue' subcommand")
assert_contains(out, "label", "help lists 'label' subcommand")

# ---- T2: token missing → exit 1 + structured error in --json ----
print("[T2] no token → exit 1, JSON error structured")
code, out, err = run(["issue", "331", "--json"],
                     env_extra={"GITEA_API_TOKEN": None, "GITEA_TOKEN": None})
assert_eq(code, 1, "exit code 1 (user error)")
try:
    obj = json.loads(err)
    assert_eq(obj.get("code"), "E_TOKEN_MISSING", "JSON code")
    assert_contains(obj.get("hint", ""), "GITEA_API_TOKEN", "JSON hint")
    passed += 1
    print("  ✓ stderr is parseable JSON")
except json.JSONDecodeError:
    failed += 1
    print(f"  ✗ stderr is not JSON: {err[:100]}")

# ---- T3: token missing → human error mode ----
print("[T3] no token → human-readable stderr, exit 1")
code, _, err = run(["issue", "331"],
                   env_extra={"GITEA_API_TOKEN": None, "GITEA_TOKEN": None})
assert_eq(code, 1, "exit code 1")
assert_contains(err, "E_TOKEN_MISSING", "stderr mentions error code")
assert_contains(err, "hint:", "stderr has hint")

# ---- Network tests below — skip if no token ----
if not (os.environ.get("GITEA_API_TOKEN") or os.environ.get("GITEA_TOKEN")):
    print("\n[skip] network tests — no GITEA_API_TOKEN in env")
else:
    # T4: read existing issue
    print("[T4] gitea issue 331 (read) — live API")
    code, out, _ = run(["issue", "331"])
    assert_eq(code, 0, "exit 0")
    assert_contains(out, "#331", "stdout contains issue number")

    # T5: shorthand `gitea issue 331` works same as explicit get
    print("[T5] shorthand vs explicit 'get'")
    c1, o1, _ = run(["issue", "331", "--json"])
    c2, o2, _ = run(["issue", "get", "331", "--json"])
    assert_eq(c1, 0, "shorthand exit 0")
    assert_eq(c2, 0, "explicit get exit 0")
    if c1 == 0 and c2 == 0:
        n1 = json.loads(o1).get("number")
        n2 = json.loads(o2).get("number")
        assert_eq(n1, n2, "shorthand and explicit return same issue")

    # T6: list with label filter
    # Note: we deliberately do NOT assert the array is non-empty — the repo
    # state changes over time. The contract being tested is "the call returns
    # 200 + a JSON array", not "this specific label currently has issues".
    print("[T6] gitea issue list --label Kind/Feature")
    code, out, _ = run(["issue", "list", "--label", "Kind/Feature",
                        "--limit", "5", "--json"])
    assert_eq(code, 0, "exit 0")
    try:
        items = json.loads(out)
        assert_eq(isinstance(items, list), True, "response is JSON array")
    except json.JSONDecodeError:
        failed += 1
        print(f"  ✗ not JSON: {out[:100]}")

    # T7: 404 on bad issue number → exit 1
    print("[T7] gitea issue 99999999 → exit 1")
    code, _, _ = run(["issue", "99999999"])
    assert_eq(code, 1, "exit 1 (404 → user error)")

    # T8: label not found → exit 1
    print("[T8] gitea label add … NonexistentLabel → exit 1")
    code, _, err = run(["label", "add", "331", "NonexistentXyz12345"])
    assert_eq(code, 1, "exit 1")
    assert_contains(err, "E_LABEL_NOT_FOUND", "error code emitted")

    # T9: dry-run does not mutate, exits 0
    print("[T9] dry-run mutation → exit 0 + no side effect")
    code, _, err = run(["label", "add", "331", "Status/PRD 中", "--dry-run"])
    assert_eq(code, 0, "dry-run exit 0")
    assert_contains(err, "DRY_RUN", "dry-run code emitted on stderr")

    # T10: global flag at tail works (argparse global flag hoisting)
    print("[T10] --json at tail (after positional)")
    code, out, _ = run(["pr", "318", "--json"])
    assert_eq(code, 0, "exit 0")
    try:
        json.loads(out)
        passed += 1
        print("  ✓ stdout is JSON despite --json being at tail")
    except json.JSONDecodeError:
        failed += 1
        print("  ✗ stdout not JSON — global flag hoisting failed")

    # T11: scoped-label replacement surfaces in response
    print("[T11] scoped label add reports replaced sibling")
    # Setup: #331 should have Priority/Medium (per project state). Add
    # Priority/Low — Gitea auto-replaces Priority/Medium; CLI must report it.
    # try/finally guarantees Priority/Medium is restored even if assertions
    # raise or the test is interrupted between the two add() calls.
    try:
        code, out, _ = run(["label", "add", "331", "Priority/Low", "--json"])
        assert_eq(code, 0, "add Priority/Low exit 0")
        try:
            obj = json.loads(out)
            replaced = obj.get("replaced", [])
            ok = "Priority/Medium" in replaced
            assert_eq(ok, True,
                      f"replaced lists Priority/Medium (got {replaced})")
        except json.JSONDecodeError:
            failed += 1
            print("  ✗ response not JSON")
    finally:
        # Restore Priority/Medium (which itself replaces Priority/Low via
        # scoped semantics). Best-effort: don't fail the test on cleanup error.
        run(["label", "add", "331", "Priority/Medium"])

    # T12: remove non-existent label on issue → exit 1
    print("[T12] remove label not on issue → E_LABEL_NOT_ON_ISSUE")
    code, _, err = run(["label", "remove", "331", "Status/Blocked"])
    assert_eq(code, 1, "exit 1")
    assert_contains(err, "E_LABEL_NOT_ON_ISSUE", "specific error code")

    # T13: promote --help（无 token 即可）
    print("[T13] gitea promote --help")
    code, out, _ = run(["promote", "--help"])
    assert_eq(code, 0, "exit 0")
    assert_contains(out, "uat", "help mentions uat target")
    assert_contains(out, "prod", "help mentions prod target")
    assert_contains(out, "fast-forward", "help mentions FF-only invariant")

    # T14: promote 非法 target → argparse 错误 exit 2
    print("[T14] promote bad-target → argparse 错误")
    code, _, err = run(["promote", "qa"])
    assert_eq(code, 2, "exit 2 (argparse)")
    assert_contains(err, "invalid choice", "argparse reports invalid choice")

    # T15: promote uat --dry-run 不真合并
    # 状态相关：可能是 UP_TO_DATE（无 diff）也可能是 DRY_RUN_*（有 diff）
    # 不变量：exit 0 + 输出 JSON 含 'promoted' 字段
    print("[T15] gitea promote uat --dry-run --json")
    code, out, _ = run(["promote", "uat", "--dry-run", "--json"])
    assert_eq(code, 0, "dry-run exit 0")
    try:
        obj = json.loads(out)
        assert_eq("promoted" in obj, True, "output JSON has 'promoted' field")
        assert_eq(obj["promoted"], False, "dry-run never reports promoted=true")
    except json.JSONDecodeError:
        failed += 1
        print(f"  ✗ stdout not JSON: {out[:100]}")

    # T16: promote prod --dry-run 同上
    print("[T16] gitea promote prod --dry-run --json")
    code, out, _ = run(["promote", "prod", "--dry-run", "--json"])
    assert_eq(code, 0, "dry-run exit 0")
    try:
        obj = json.loads(out)
        assert_eq(obj.get("promoted"), False, "dry-run promoted=false")
    except json.JSONDecodeError:
        failed += 1
        print(f"  ✗ stdout not JSON: {out[:100]}")

# ---- T17: 调用 gitea_promote_unit.test.py（mock 单测，覆盖 cmd_promote 错误路径） ----
# 不重复实现：unit 测试自己有 12 个 assert + summary + sys.exit。
# 这里只把它接入 gitea.test.py 的执行链，确保 CI 跑 ops 测试时一起触发。
print("[T17] subprocess-run gitea_promote_unit.test.py")
unit_test = Path(__file__).parent / "gitea_promote_unit.test.py"
proc = subprocess.run([sys.executable, str(unit_test)], capture_output=True,
                      text=True, timeout=30)
assert_eq(proc.returncode, 0, "unit test suite exits 0")
if proc.returncode != 0:
    print(f"  unit test stdout (last 30 lines):")
    for line in proc.stdout.splitlines()[-30:]:
        print(f"    {line}")

# ---- summary ----
print(f"\n[summary] {passed} passed, {failed} failed")
sys.exit(0 if failed == 0 else 1)
