"""階段 8 E + F 遠端驗證(在 NAS 上跑)。

  E. /api/tasks/{id}/related  → 確認回傳結構
  F. magic_link → /auth-from-line 一次性 + 二次失效

直接 ssh 跑這支:輸出全部 PASS / FAIL。
"""

import asyncio
import json
import sys
import urllib.parse
import urllib.request
import os

# 載 .env
ENV_PATH = "/volume1/homes/robertsu/coba/app/.env"
if os.path.exists(ENV_PATH):
    for line in open(ENV_PATH):
        if "=" in line and not line.startswith("#"):
            k, v = line.strip().split("=", 1)
            os.environ.setdefault(k, v)

sys.path.insert(0, "/volume1/homes/robertsu/coba/app")

from sqlalchemy import select
from app.database import AsyncSessionLocal
from app.models.task import Task
from app.models.user import User
from app.core.security import create_access_token
from app.services.magic_link import generate_magic_link

BASE = "http://localhost:8000"
RESULTS = {"pass": [], "fail": []}


def http(method: str, path: str, headers=None, body=None, allow_redirects=True):
    url = f"{BASE}{path}"
    req = urllib.request.Request(url, method=method)
    for k, v in (headers or {}).items():
        req.add_header(k, v)
    if body is not None:
        req.data = body.encode("utf-8") if isinstance(body, str) else body
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            return resp.status, dict(resp.headers), resp.read().decode("utf-8", "replace")
    except urllib.error.HTTPError as e:
        return e.code, dict(e.headers or {}), (e.read() or b"").decode("utf-8", "replace")
    except Exception as e:
        return -1, {}, f"ERROR: {e}"


def check(label, cond, detail=""):
    if cond:
        RESULTS["pass"].append(f"{label} {detail}")
        print(f"[PASS] {label} {detail}")
    else:
        RESULTS["fail"].append(f"{label} {detail}")
        print(f"[FAIL] {label} {detail}")


async def test_e():
    print("\n" + "="*60)
    print(" E. /api/tasks/{id}/related")
    print("="*60)
    async with AsyncSessionLocal() as db:
        # 找 robert + 一個任意任務
        u = (await db.execute(
            select(User).where(User.email == "ssbb30529@gmail.com").limit(1)
        )).scalar_one_or_none()
        if u is None:
            check("E.0 找到 robert", False, "(找不到 robert)")
            return
        check("E.0 找到 robert", True, f"({u.display_name})")

        t = (await db.execute(
            select(Task).where(Task.deleted_at.is_(None)).order_by(Task.created_at.desc()).limit(1)
        )).scalar_one_or_none()
        if t is None:
            check("E.0 找到任務", False, "(系統沒任務)")
            return
        check("E.0 找到任務", True, f"({t.title[:40]})")

    token = create_access_token(u.id)

    # 1. 沒帶 token → 401
    code, _, body = http("GET", f"/api/tasks/{t.id}/related")
    check("E.1 無 token → 401/403", code in (401, 403), f"got {code}")

    # 2. 帶 token → 200
    code, _, body = http("GET", f"/api/tasks/{t.id}/related",
                         headers={"Authorization": f"Bearer {token}"})
    check("E.2 帶 token → 200", code == 200, f"got {code}")
    if code != 200:
        print(f"  body: {body[:300]}")
        return

    # 3. 結構檢查
    try:
        payload = json.loads(body)
    except Exception:
        check("E.3 JSON 解析", False, body[:200])
        return
    check("E.3 結構含 task_id/related_messages/related_tasks",
          all(k in payload for k in ("task_id", "related_messages", "related_tasks")),
          f"keys={list(payload.keys())}")
    check("E.4 task_id 對得上", str(payload.get("task_id")) == str(t.id))
    check("E.5 related_messages 是 list", isinstance(payload.get("related_messages"), list),
          f"len={len(payload.get('related_messages') or [])}")
    check("E.6 related_tasks 是 list", isinstance(payload.get("related_tasks"), list),
          f"len={len(payload.get('related_tasks') or [])}")

    # 4. 訊息 / 任務 結構抽樣
    if payload.get("related_messages"):
        m = payload["related_messages"][0]
        check("E.7 message item 結構",
              all(k in m for k in ("message_id", "content", "score")),
              f"keys={list(m.keys())}")

    if payload.get("related_tasks"):
        rt = payload["related_tasks"][0]
        check("E.8 task item 結構",
              all(k in rt for k in ("id", "title", "status")),
              f"keys={list(rt.keys())}")


async def test_f():
    print("\n" + "="*60)
    print(" F. magic_link + /auth-from-line")
    print("="*60)
    async with AsyncSessionLocal() as db:
        u = (await db.execute(
            select(User).where(User.email == "ssbb30529@gmail.com").limit(1)
        )).scalar_one_or_none()
    if u is None:
        check("F.0 找到 robert", False)
        return
    check("F.0 找到 robert", True, f"({u.display_name})")

    # 1. 產 magic link
    link = generate_magic_link(u.id, base_url="http://localhost:8000")
    check("F.1 link 含 /auth-from-line?t=", "/auth-from-line?t=" in link)
    token = link.split("?t=", 1)[1]
    print(f"  token len={len(token)}")

    # 2. 第一次 hit → 200,HTML 回應,含正確 localStorage key
    code, headers, body = http("GET", f"/auth-from-line?t={token}")
    check("F.2 第一次 hit → 200", code == 200, f"got {code}")
    check("F.3 是 text/html", "text/html" in (headers.get("content-type", "") or ""),
          f"ct={headers.get('content-type')}")
    check("F.4 HTML 含 localStorage.setItem", "localStorage.setItem" in body)
    check("F.5 key 名 coba_access_token 正確", "'coba_access_token'" in body or '"coba_access_token"' in body,
          "(舊 bug 是寫 'coba_access' 漏了 _token)")
    check("F.6 key 名 coba_refresh_token 正確", "'coba_refresh_token'" in body or '"coba_refresh_token"' in body)
    check("F.7 HTML 含跳轉 location.replace", "location.replace" in body)

    # 3. 第二次 hit 同 token → 應該失效 (一次性)
    code2, _, body2 = http("GET", f"/auth-from-line?t={token}")
    check("F.8 第二次 hit 同 token → 403 (一次性)",
          code2 == 403,
          f"got {code2}")
    check("F.9 失敗 HTML 顯示「連結失效」訊息",
          "連結失效" in body2 or "已經用過" in body2 or "失效" in body2,
          f"body[:200]={body2[:200]}")

    # 4. 亂塞 token → 也應該失效
    code3, _, body3 = http("GET", "/auth-from-line?t=abc.def.ghi")
    check("F.10 亂 token → 403", code3 == 403, f"got {code3}")

    # 5. 過期 token (用過期 jti 之外的方式不好造,跳過)
    print("  (skipping expired-token test — covered by 60s TTL design)")


async def main():
    await test_e()
    await test_f()
    print("\n" + "="*60)
    print(f" 總結:PASS={len(RESULTS['pass'])}  FAIL={len(RESULTS['fail'])}")
    print("="*60)
    if RESULTS["fail"]:
        sys.exit(1)


if __name__ == "__main__":
    asyncio.run(main())
