#!/usr/bin/env python3
"""Vigorous live API test for accountants_chatbot SaaS studio.

Writes structured PASS/FAIL lines to stdout; exit 0 only if critical flows pass.
"""
from __future__ import annotations

import json
import os
import sys
import time
import uuid
from pathlib import Path

import requests

BASE = os.environ.get("TEST_BASE", "http://127.0.0.1:8000")
ROOT = Path(__file__).resolve().parents[1]
PDF = ROOT / "samples/balance_sheets/balance_sheet_acme_2023.pdf"
STATEMENT = next(
    ROOT.glob("data/kb_uploads/**/*Paytm*.pdf"),
    PDF,
)
TIMEOUT = float(os.environ.get("TEST_TIMEOUT", "180"))

results: list[dict] = []


def rec(section: str, name: str, ok: bool, detail: str = "", ms: float = 0):
    results.append(
        {
            "section": section,
            "name": name,
            "ok": ok,
            "detail": detail[:800],
            "ms": round(ms),
        }
    )
    mark = "PASS" if ok else "FAIL"
    print(f"[{mark}] {section} :: {name} ({ms:.0f}ms) {detail[:200]}")


def req(method: str, path: str, **kw):
    kw.setdefault("timeout", TIMEOUT)
    t0 = time.time()
    r = requests.request(method, f"{BASE}{path}", **kw)
    return r, (time.time() - t0) * 1000


def main() -> int:
    # ── System ────────────────────────────────────────────────────────────
    r, ms = req("GET", "/health")
    rec("system", "GET /health", r.status_code == 200, r.text[:300], ms)
    health = r.json() if r.ok else {}

    r, ms = req("GET", "/")
    rec("system", "GET / (studio UI)", r.status_code == 200 and "html" in r.headers.get("content-type", "").lower(), f"ct={r.headers.get('content-type')}", ms)

    r, ms = req("GET", "/app")
    rec("system", "GET /app", r.status_code == 200, f"ct={r.headers.get('content-type')}", ms)

    # ── Auth guest ────────────────────────────────────────────────────────
    r, ms = req("POST", "/saas/auth/guest", json={})
    ok = r.status_code == 200 and "access_token" in (r.json() if r.ok else {})
    rec("auth", "POST /saas/auth/guest", ok, r.text[:300], ms)
    if not ok:
        return _finish(1)
    token = r.json()["access_token"]
    H = {"Authorization": f"Bearer {token}"}

    # ── Me / agents ───────────────────────────────────────────────────────
    r, ms = req("GET", "/saas/me", headers=H)
    me = r.json() if r.ok else {}
    agents = me.get("agents") or []
    alex = next((a for a in agents if a.get("is_main") or a.get("name") == "Alex"), agents[0] if agents else None)
    rec(
        "auth",
        "GET /saas/me",
        r.status_code == 200 and alex is not None,
        f"agents={len(agents)} names={[a.get('name') for a in agents]} ready={alex and alex.get('ready_docs')}",
        ms,
    )
    if not alex:
        return _finish(1)
    aid = alex["id"]

    # Only Alex policy
    extras = [a for a in agents if not (a.get("is_main") or a.get("name") == "Alex")]
    rec("policy", "only Alex agent present", len(extras) == 0, f"extras={[a.get('name') for a in extras]}", 0)

    r, ms = req("GET", "/saas/agents", headers=H)
    rec("agents", "GET /saas/agents", r.status_code == 200 and isinstance(r.json(), list), f"n={len(r.json()) if r.ok else 0}", ms)

    r, ms = req("GET", f"/saas/agents/{aid}", headers=H)
    rec("agents", "GET /saas/agents/{id}", r.status_code == 200 and r.json().get("id") == aid, r.text[:200], ms)

    # Create agent should be blocked (403)
    r, ms = req("POST", "/saas/agents", headers=H, json={"name": f"Extra-{uuid.uuid4().hex[:6]}", "description": "should fail"})
    rec("policy", "POST /saas/agents blocked", r.status_code in (403, 400, 405), f"status={r.status_code} body={r.text[:200]}", ms)

    # Patch Alex settings
    r, ms = req(
        "PATCH",
        f"/saas/agents/{aid}",
        headers=H,
        json={"description": "Your main accounting and finance assistant.", "kb_mode": "tenant"},
    )
    rec("agents", "PATCH /saas/agents/{id}", r.status_code == 200, r.text[:200], ms)

    # Delete Alex should be blocked
    r, ms = req("DELETE", f"/saas/agents/{aid}", headers=H)
    rec("policy", "DELETE Alex blocked", r.status_code in (403, 400, 405), f"status={r.status_code} body={r.text[:200]}", ms)

    # ── Documents list (before) ───────────────────────────────────────────
    r, ms = req("GET", f"/saas/agents/{aid}/documents", headers=H)
    docs_before = r.json() if r.ok else []
    rec("kb", "GET documents (before)", r.status_code == 200, f"count={len(docs_before)}", ms)

    # Reject unsupported type
    r, ms = req(
        "POST",
        f"/saas/agents/{aid}/documents",
        headers=H,
        files={"file": ("bad.exe", b"MZ fake", "application/octet-stream")},
    )
    rec("kb", "reject unsupported filetype", r.status_code == 400, r.text[:200], ms)

    # Reject empty
    r, ms = req(
        "POST",
        f"/saas/agents/{aid}/documents",
        headers=H,
        files={"file": ("empty.txt", b"", "text/plain")},
    )
    rec("kb", "reject empty file", r.status_code == 400, r.text[:200], ms)

    # Upload policy-style text (enough content to chunk)
    policy = (
        "Acme Refund Policy\n\n"
        "Customers may request a full refund within 14 days of purchase.\n"
        "Late fees are 2 percent per month on overdue invoices.\n"
        "Contact support@acme.test for billing questions.\n"
        "Payment terms are Net 30 for approved corporate accounts.\n"
        "Disputed charges must be raised within 7 business days.\n"
    ) * 3
    r, ms = req(
        "POST",
        f"/saas/agents/{aid}/documents",
        headers=H,
        files={"file": ("acme_refund_policy.txt", policy.encode(), "text/plain")},
    )
    up = r.json() if r.content else {}
    doc = (up.get("document") or {}) if r.ok else {}
    rec(
        "kb",
        "upload TXT policy",
        r.status_code == 200 and doc.get("status") == "ready" and (up.get("chunks_indexed") or 0) > 0,
        f"status={doc.get('status')} chunks={up.get('chunks_indexed')} err={up.get('detail') or doc.get('error')}",
        ms,
    )
    txt_doc_id = doc.get("id")

    # Upload PDF (sample balance sheet — small)
    if PDF.exists():
        with PDF.open("rb") as f:
            r, ms = req(
                "POST",
                f"/saas/agents/{aid}/documents",
                headers=H,
                files={"file": ("balance_sheet_acme_2023.pdf", f, "application/pdf")},
            )
        up = r.json() if r.content else {}
        doc = (up.get("document") or {}) if r.ok else {}
        rec(
            "kb",
            "upload PDF balance sheet",
            r.status_code == 200 and doc.get("status") == "ready" and (up.get("chunks_indexed") or 0) > 0,
            f"status={doc.get('status')} chunks={up.get('chunks_indexed')} detail={up.get('detail')}",
            ms,
        )
        pdf_doc_id = doc.get("id")
    else:
        rec("kb", "upload PDF balance sheet", False, "fixture missing", 0)
        pdf_doc_id = None

    # Optional statement PDF if different
    if STATEMENT.exists() and STATEMENT.resolve() != PDF.resolve():
        with STATEMENT.open("rb") as f:
            r, ms = req(
                "POST",
                f"/saas/agents/{aid}/documents",
                headers=H,
                files={"file": ("upi_statement.pdf", f, "application/pdf")},
            )
        up = r.json() if r.content else {}
        doc = (up.get("document") or {}) if r.ok else {}
        rec(
            "kb",
            "upload PDF UPI/statement",
            r.status_code == 200 and doc.get("status") == "ready",
            f"status={doc.get('status')} chunks={up.get('chunks_indexed')} detail={up.get('detail')}",
            ms,
        )
        stmt_doc_id = doc.get("id")
    else:
        stmt_doc_id = None
        rec("kb", "upload PDF UPI/statement", True, "skipped (no separate statement fixture)", 0)

    r, ms = req("GET", f"/saas/agents/{aid}/documents", headers=H)
    docs = r.json() if r.ok else []
    ready = [d for d in docs if d.get("status") == "ready"]
    rec(
        "kb",
        "GET documents (after uploads)",
        r.status_code == 200 and len(ready) >= 1,
        f"total={len(docs)} ready={len(ready)} files={[d.get('filename') for d in docs[:8]]}",
        ms,
    )

    # /me should reflect ready_docs > 0
    r, ms = req("GET", "/saas/me", headers=H)
    me2 = r.json() if r.ok else {}
    alex2 = next((a for a in (me2.get("agents") or []) if a.get("id") == aid), None)
    rec(
        "kb",
        "/me ready_docs after upload",
        bool(alex2 and (alex2.get("ready_docs") or 0) >= 1),
        f"ready_docs={alex2 and alex2.get('ready_docs')} chunks={alex2 and alex2.get('chunk_count')}",
        ms,
    )

    # ── Chat flows ────────────────────────────────────────────────────────
    session = f"vigorous-{uuid.uuid4().hex[:8]}"

    def chat(q: str, label: str, expect_ks: str | None = None, expect_upload: bool | None = None):
        r, ms = req(
            "POST",
            f"/saas/agents/{aid}/chat",
            headers={**H, "Content-Type": "application/json"},
            json={"question": q, "session_id": session},
        )
        data = r.json() if r.content else {}
        answer = (data.get("answer") or "")[:280].replace("\n", " | ")
        ks = data.get("knowledge_state")
        cta = data.get("show_upload_cta")
        cites = data.get("citations") or []
        ok = r.status_code == 200 and bool(data.get("answer"))
        if expect_ks is not None and ks != expect_ks:
            ok = False
        if expect_upload is not None and bool(cta) != expect_upload:
            ok = False
        # Leak check: shared platform dataset should not appear for tenant KB questions
        leak = any(
            (c.get("source_dataset") or "").startswith("financial_")
            or (c.get("source_dataset") or "").startswith("archive2_")
            for c in cites
        )
        if leak:
            ok = False
        rec(
            "chat",
            label,
            ok,
            f"ks={ks} cta={cta} cites={len(cites)} leak={leak} ans={answer}",
            ms,
        )
        return data

    chat("hi", "greeting / hi", expect_ks=None)
    chat("Tell me about the knowledge base", "KB overview")
    chat("What is the Acme refund window?", "policy retrieval (14 days)")
    chat("How much did I spend?", "spend question")
    chat("How many payments were made?", "payment count")
    chat("What is 2+2?", "math / no KB needed")

    # Legacy agent chat alias
    r, ms = req(
        "POST",
        "/saas/agent/chat",
        headers={**H, "Content-Type": "application/json"},
        json={"question": "hello", "session_id": session, "agent_id": aid},
    )
    rec("chat", "POST /saas/agent/chat alias", r.status_code == 200, (r.text[:200] if r.content else ""), ms)

    # Feedback
    r, ms = req(
        "POST",
        f"/saas/agents/{aid}/feedback",
        headers={**H, "Content-Type": "application/json"},
        json={
            "session_id": session,
            "question": "What is the Acme refund window?",
            "answer": "14 days",
            "rating": "up",
        },
    )
    rec("feedback", "POST feedback", r.status_code == 200, r.text[:200], ms)

    r, ms = req("GET", f"/saas/agents/{aid}/feedback", headers=H)
    rec("feedback", "GET feedback", r.status_code == 200, f"n={len(r.json()) if r.ok else 0}", ms)

    # API keys
    r, ms = req("GET", "/saas/api-keys", headers=H)
    rec("api-keys", "GET api-keys", r.status_code == 200, r.text[:200], ms)

    r, ms = req("POST", "/saas/api-keys", headers=H, json={"name": "vigorous-test", "scopes": ["chat"]})
    key_body = r.json() if r.ok else {}
    key_id = key_body.get("id")
    rec("api-keys", "POST api-keys", r.status_code == 200 and bool(key_id or key_body.get("key")), r.text[:200], ms)
    if key_id:
        r, ms = req("DELETE", f"/saas/api-keys/{key_id}", headers=H)
        rec("api-keys", "DELETE api-keys", r.status_code == 200, r.text[:200], ms)

    # Delete one uploaded doc
    del_id = txt_doc_id or pdf_doc_id or stmt_doc_id
    if del_id:
        r, ms = req("DELETE", f"/saas/agents/{aid}/documents/{del_id}", headers=H)
        rec("kb", "DELETE document", r.status_code == 200, r.text[:200], ms)
        r, ms = req("GET", f"/saas/agents/{aid}/documents", headers=H)
        ids = [d.get("id") for d in (r.json() if r.ok else [])]
        rec("kb", "deleted doc gone from list", del_id not in ids, f"remaining={len(ids)}", ms)
    else:
        rec("kb", "DELETE document", False, "no doc id", 0)

    # Auth negative
    r, ms = req("GET", "/saas/me")
    rec("auth", "GET /me without token rejected", r.status_code in (401, 403), f"status={r.status_code}", ms)

    r, ms = req("GET", "/saas/me", headers={"Authorization": "Bearer bogus"})
    rec("auth", "GET /me bad token rejected", r.status_code in (401, 403), f"status={r.status_code}", ms)

    # Register/login smoke (unique email)
    email = f"vigorous_{uuid.uuid4().hex[:10]}@example.com"
    password = "TestPass123!"
    r, ms = req(
        "POST",
        "/saas/auth/register",
        json={"email": email, "password": password, "full_name": "Vigorous Tester", "org_name": "Vig Org"},
    )
    reg_ok = r.status_code == 200 and "access_token" in (r.json() if r.ok else {})
    rec("auth", "POST /saas/auth/register", reg_ok, r.text[:250], ms)
    if reg_ok:
        rtok = r.json()["access_token"]
        r, ms = req("GET", "/saas/me", headers={"Authorization": f"Bearer {rtok}"})
        reg_agents = (r.json() if r.ok else {}).get("agents") or []
        rec(
            "auth",
            "registered user has Alex only",
            r.status_code == 200 and len(reg_agents) == 1 and reg_agents[0].get("name") == "Alex",
            f"agents={[a.get('name') for a in reg_agents]}",
            ms,
        )
        # Empty KB chat for fresh user
        raid = reg_agents[0]["id"]
        r, ms = req(
            "POST",
            f"/saas/agents/{raid}/chat",
            headers={"Authorization": f"Bearer {rtok}", "Content-Type": "application/json"},
            json={"question": "How much did I spend?", "session_id": f"empty-{uuid.uuid4().hex[:6]}"},
        )
        data = r.json() if r.content else {}
        rec(
            "chat",
            "fresh user empty KB spend → upload CTA",
            r.status_code == 200
            and data.get("knowledge_state") == "empty"
            and bool(data.get("show_upload_cta")),
            f"ks={data.get('knowledge_state')} cta={data.get('show_upload_cta')} ans={(data.get('answer') or '')[:180]}",
            ms,
        )
        r, ms = req(
            "POST",
            "/saas/auth/login",
            json={"email": email, "password": password},
        )
        rec("auth", "POST /saas/auth/login", r.status_code == 200 and "access_token" in (r.json() if r.ok else {}), r.text[:200], ms)

    # Frontend build assets present
    studio = ROOT / "static/studio/index.html"
    rec("frontend", "static/studio/index.html exists", studio.exists(), str(studio), 0)
    if studio.exists():
        html = studio.read_text()
        rec("frontend", "studio HTML references hashed assets", "assets/" in html, html[:120].replace("\n", " "), 0)

    # UI source: upload only in Knowledge panel
    chat_src = (ROOT / "frontend/src/components/ChatPanel.jsx").read_text()
    know_src = (ROOT / "frontend/src/components/KnowledgePanel.jsx").read_text()
    rec(
        "frontend",
        "ChatPanel has no file input / paperclip upload",
        'type="file"' not in chat_src and "Paperclip" not in chat_src and "UploadCloud" not in chat_src,
        "",
        0,
    )
    rec(
        "frontend",
        "KnowledgePanel has PDF upload zone",
        'type="file"' in know_src and "UploadCloud" in know_src,
        "",
        0,
    )

    # Health extras
    rec(
        "system",
        "Ollama reachable + model set",
        bool(health.get("ollama_reachable")) and bool(health.get("ollama_model")),
        f"model={health.get('ollama_model')} vs_ready={health.get('vector_store_ready')}",
        0,
    )

    return _finish(0 if all(x["ok"] for x in results if x["section"] in ("system", "auth", "kb", "chat", "policy")) else 1)


def _finish(code_hint: int) -> int:
    passed = sum(1 for x in results if x["ok"])
    failed = sum(1 for x in results if not x["ok"])
    out = {
        "base": BASE,
        "passed": passed,
        "failed": failed,
        "total": len(results),
        "results": results,
    }
    out_path = ROOT / "TEST_RESULTS.json"
    out_path.write_text(json.dumps(out, indent=2))
    print(f"\nSUMMARY passed={passed} failed={failed} total={len(results)} → {out_path}")
    return 1 if failed else 0


if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception as exc:
        print(f"[FAIL] harness crashed: {exc}")
        sys.exit(2)
