#!/usr/bin/env python3
"""
DocuVault Phase 3 integration tests.
Runs against the live stack on localhost:8000 / localhost:9000.
No pytest required — just: python test_integration.py
"""
import io
import sys
import uuid
import httpx

BASE = "http://localhost:8000"
MINIO_PUBLIC = "http://localhost:9000"

PASS = "\033[32m✓\033[0m"
FAIL = "\033[31m✗\033[0m"

results = []

def check(name: str, ok: bool, detail: str = "") -> None:
    results.append(ok)
    icon = PASS if ok else FAIL
    print(f"  {icon}  {name}" + (f"  [{detail}]" if detail else ""))
    if not ok:
        print(f"       DETAIL: {detail}")

def section(title: str) -> None:
    print(f"\n{'─'*60}\n  {title}\n{'─'*60}")


# ── helpers ──────────────────────────────────────────────────────────────────

def login(client: httpx.Client, email: str, password: str):
    r = client.post("/api/auth/login", json={"email": email, "password": password})
    if r.status_code == 200:
        return r.json().get("access_token")
    return None

def auth_headers(token: str) -> dict:
    return {"Authorization": f"Bearer {token}"}


# ── 0. Health ─────────────────────────────────────────────────────────────────

section("0. Health")
with httpx.Client(base_url=BASE, timeout=10) as c:
    r = c.get("/health")
    check("GET /health returns 200", r.status_code == 200)
    body = r.json()
    check("postgres healthy", body.get("checks", {}).get("postgres") == "ok", str(body))
    check("minio healthy",    body.get("checks", {}).get("minio")    == "ok", str(body))


# ── 1. Admin login ────────────────────────────────────────────────────────────

section("1. Admin login")
with httpx.Client(base_url=BASE, timeout=10) as c:
    admin_token = login(c, "admin@docuvault.example", "Admin1234!")
    check("Admin login succeeds", admin_token is not None)

    if admin_token:
        r = c.get("/api/auth/me", headers=auth_headers(admin_token))
        check("GET /api/auth/me role=admin", r.json().get("role") == "admin", str(r.json()))


# ── 2. Register regular user ──────────────────────────────────────────────────

section("2. Register regular user")
user_email = f"testuser_{uuid.uuid4().hex[:6]}@example.com"
_uid = uuid.uuid4().hex[:12]
user_password = f"Dv!{_uid}Z9x#"  # unique per run — won't appear in HIBP

with httpx.Client(base_url=BASE, timeout=10) as c:
    r = c.post("/api/auth/register", json={
        "email": user_email,
        "password": user_password,
        "handle": f"tester_{uuid.uuid4().hex[:4]}",
    })
    check("POST /api/auth/register returns 201",
          r.status_code == 201, f"{r.status_code} {r.text[:120]}")

    user_token = login(c, user_email, user_password)
    check("Regular user login succeeds", user_token is not None)

    if user_token:
        me = c.get("/api/auth/me", headers=auth_headers(user_token)).json()
        check("Role is 'user'", me.get("role") == "user", str(me))
        user_id = me.get("id")


# ── 3. Quota endpoint ─────────────────────────────────────────────────────────

section("3. Quota")
with httpx.Client(base_url=BASE, timeout=10) as c:
    if user_token:
        r = c.get("/api/auth/me/quota", headers=auth_headers(user_token))
        check("GET /api/auth/me/quota returns 200", r.status_code == 200, r.text[:120])
        q = r.json()
        check("used_bytes starts at 0", q.get("used_bytes") == 0, str(q))
        check("limit_bytes is 100 MB", q.get("limit_bytes") == 104857600, str(q))

    # Admin cannot read quota (document endpoints blocked for admin)
    if admin_token:
        r = c.get("/api/auth/me/quota", headers=auth_headers(admin_token))
        # Admin has a quota row too (they're a user in the DB), so 200 is also acceptable
        check("GET /api/auth/me/quota returns 200 or 404", r.status_code in (200, 404), r.text)


# ── 4. Presigned upload flow ──────────────────────────────────────────────────

section("4. Three-step presigned upload")
doc_id = None
with httpx.Client(base_url=BASE, timeout=15) as c:
    if not user_token:
        print("  SKIP — no user token")
    else:
        # Step 1: get upload URL
        r = c.post("/api/documents/upload-url",
                   headers=auth_headers(user_token),
                   json={"filename": "test.txt", "content_type": "text/plain"})
        check("POST /api/documents/upload-url returns 200",
              r.status_code == 200, f"{r.status_code} {r.text[:120]}")

        if r.status_code == 200:
            body = r.json()
            upload_url = body.get("upload_url", "")
            doc_id = body.get("document_id")
            check("upload_url present", bool(upload_url), upload_url[:60] if upload_url else "missing")
            check("document_id present", bool(doc_id), str(doc_id))

            # Step 2: PUT file bytes directly to MinIO via presigned URL.
            # The URL may contain the Docker-internal host (minio:9000); rewrite
            # to localhost:9000 for the TCP connection but keep minio:9000 in the
            # Host header so the HMAC signature remains valid.
            file_content = b"Hello DocuVault integration test! " * 100  # ~3.4 KB
            try:
                put_url = upload_url
                extra_headers = {"Content-Type": "text/plain"}
                if "minio:9000" in put_url:
                    extra_headers["Host"] = "minio:9000"
                    put_url = put_url.replace("http://minio:9000", "http://localhost:9000", 1)
                put_r = httpx.put(put_url, content=file_content,
                                  headers=extra_headers, timeout=15)
                check("PUT to MinIO presigned URL succeeds",
                      put_r.status_code in (200, 204),
                      f"{put_r.status_code} {put_r.text[:60]}")
            except Exception as e:
                check("PUT to MinIO presigned URL succeeds", False, str(e))
                doc_id = None

            # Step 3: confirm
            if doc_id:
                r2 = c.post(f"/api/documents/{doc_id}/confirm",
                            headers=auth_headers(user_token))
                check("POST /api/documents/{id}/confirm returns 200",
                      r2.status_code == 200, f"{r2.status_code} {r2.text[:120]}")
                if r2.status_code == 200:
                    conf = r2.json()
                    check("confirm returns size_bytes > 0",
                          conf.get("size_bytes", 0) > 0, str(conf))
                    check("confirm status=uploaded",
                          conf.get("status") == "uploaded", str(conf))


# ── 5. Quota updated after upload ─────────────────────────────────────────────

section("5. Quota updated after upload")
with httpx.Client(base_url=BASE, timeout=10) as c:
    if user_token and doc_id:
        q = c.get("/api/auth/me/quota", headers=auth_headers(user_token)).json()
        check("used_bytes > 0 after upload", q.get("used_bytes", 0) > 0, str(q))


# ── 6. Document list + ownership ─────────────────────────────────────────────

section("6. Document list and ownership isolation")
with httpx.Client(base_url=BASE, timeout=10) as c:
    if user_token:
        r = c.get("/api/documents", headers=auth_headers(user_token))
        check("GET /api/documents returns 200", r.status_code == 200, r.text[:60])
        items = r.json() if r.status_code == 200 else []
        if isinstance(items, dict):
            items = items.get("items", items.get("documents", []))
        check("Document list contains uploaded doc", len(items) >= 1, f"count={len(items)}")

    # Second user cannot see first user's document
    if doc_id:
        user2_email = f"user2_{uuid.uuid4().hex[:6]}@example.com"
        c.post("/api/auth/register", json={
            "email": user2_email,
            "password": user_password,
            "handle": f"u2_{uuid.uuid4().hex[:4]}",
        })
        tok2 = login(c, user2_email, user_password)
        if tok2:
            r = c.get(f"/api/documents/{doc_id}", headers=auth_headers(tok2))
            check("Cross-user GET returns 404 (SEC-04)", r.status_code == 404,
                  f"got {r.status_code}")


# ── 7. Admin blocked from documents ──────────────────────────────────────────

section("7. Admin 403 on document endpoints (SEC-04 / SC4)")
with httpx.Client(base_url=BASE, timeout=10) as c:
    if admin_token:
        r = c.get("/api/documents", headers=auth_headers(admin_token))
        check("Admin GET /api/documents returns 403",
              r.status_code == 403, f"got {r.status_code}")

        r = c.post("/api/documents/upload-url",
                   headers=auth_headers(admin_token),
                   json={"filename": "x.txt", "content_type": "text/plain"})
        check("Admin POST /api/documents/upload-url returns 403",
              r.status_code == 403, f"got {r.status_code}")


# ── 8. Topics namespace ───────────────────────────────────────────────────────

section("8. Topics namespace isolation (DOC-04)")
with httpx.Client(base_url=BASE, timeout=10) as c:
    if admin_token:
        # Admin creates a system topic
        r = c.post("/api/admin/topics",
                   headers=auth_headers(admin_token),
                   json={"name": "System Topic", "description": "visible to all"})
        check("Admin POST /api/admin/topics returns 201",
              r.status_code == 201, f"{r.status_code} {r.text[:80]}")

    if user_token:
        # User can see system topic
        r = c.get("/api/topics", headers=auth_headers(user_token))
        check("GET /api/topics returns 200", r.status_code == 200, r.text[:60])
        topics = r.json() if r.status_code == 200 else []
        if isinstance(topics, dict):
            topics = topics.get("items", topics.get("topics", []))
        system_visible = any(t.get("name") == "System Topic" for t in topics)
        check("System topic visible to regular user", system_visible, str([t.get("name") for t in topics]))

        # User creates own topic
        r2 = c.post("/api/topics",
                    headers=auth_headers(user_token),
                    json={"name": "My Topic", "color": "#ff0000"})
        check("User POST /api/topics returns 200 or 201",
              r2.status_code in (200, 201), f"{r2.status_code} {r2.text[:80]}")


# ── 9. Unauthenticated blocked ────────────────────────────────────────────────

section("9. Unauthenticated requests blocked")
with httpx.Client(base_url=BASE, timeout=10) as c:
    r = c.get("/api/documents")
    check("GET /api/documents without token returns 401 or 403",
          r.status_code in (401, 403), f"got {r.status_code}")

    r = c.get("/api/topics")
    check("GET /api/topics without token returns 401 or 403",
          r.status_code in (401, 403), f"got {r.status_code}")


# ── 10. Settings endpoint removed ─────────────────────────────────────────────

section("10. /api/settings removed (D-12)")
with httpx.Client(base_url=BASE, timeout=10) as c:
    r = c.get("/api/settings")
    check("GET /api/settings returns 404", r.status_code == 404, f"got {r.status_code}")


# ── 11. Quota delete decrement ────────────────────────────────────────────────

section("11. Quota decrements on document delete (STORE-06)")
with httpx.Client(base_url=BASE, timeout=10) as c:
    if user_token and doc_id:
        q_before = c.get("/api/auth/me/quota", headers=auth_headers(user_token)).json()
        used_before = q_before.get("used_bytes", 0)

        r = c.delete(f"/api/documents/{doc_id}", headers=auth_headers(user_token))
        check("DELETE /api/documents/{id} returns 200 or 204",
              r.status_code in (200, 204), f"{r.status_code} {r.text[:60]}")

        q_after = c.get("/api/auth/me/quota", headers=auth_headers(user_token)).json()
        used_after = q_after.get("used_bytes", 0)
        check("used_bytes decreased after delete",
              used_after < used_before, f"{used_before} → {used_after}")


# ── Summary ───────────────────────────────────────────────────────────────────

passed = sum(results)
total  = len(results)
print(f"\n{'═'*60}")
print(f"  {'PASS' if passed == total else 'FAIL'}  {passed}/{total} checks passed")
print(f"{'═'*60}\n")
sys.exit(0 if passed == total else 1)
