#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Daneshyar - Login Test for All Demo Users
==========================================
1. Connects directly to DB via .env credentials
2. Re-hashes ALL demo user passwords to Demo@1234  (Laravel-compatible bcrypt, cost 12)
3. Hits the live HTTPS login endpoint for every user and shows PASS/FAIL

Requires:  pip install requests pymysql bcrypt

Usage:
    python scripts/test_login_all_users.py
    python scripts/test_login_all_users.py --no-rehash   # skip DB step, just test HTTP
    python scripts/test_login_all_users.py --rehash-only  # fix passwords, skip HTTP
"""
from __future__ import annotations

import argparse
import io
import sys
import time
from pathlib import Path

# Force UTF-8 stdout on Windows so Persian/arrow chars don't crash
if hasattr(sys.stdout, "reconfigure"):
    try:
        sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    except Exception:
        pass
if hasattr(sys.stderr, "reconfigure"):
    try:
        sys.stderr.reconfigure(encoding="utf-8", errors="replace")
    except Exception:
        pass

# ── Config ────────────────────────────────────────────────────────────────────
BASE_URL = "https://mydaneshyar.ir"
DEMO_PASSWORD = "Demo@1234"

DEMO_USERS: list[dict] = [
    # username                role                  school  expected_redirect_prefix
    {"u": "admin",          "role": "super_admin",  "s": None, "to": "/super-admin"},
    {"u": "modir1",         "role": "owner",        "s": 1,    "to": "/school"},
    {"u": "naeb1",          "role": "moaven",       "s": 1,    "to": "/school"},
    {"u": "teacher1",       "role": "teacher",      "s": 1,    "to": "/teacher"},
    {"u": "teacher2",       "role": "teacher",      "s": 1,    "to": "/teacher"},
    {"u": "teacher3",       "role": "teacher",      "s": 1,    "to": "/teacher"},
    {"u": "modir2",         "role": "owner",        "s": 2,    "to": "/school"},
    {"u": "teacher4",       "role": "teacher",      "s": 2,    "to": "/teacher"},
    {"u": "teacher5",       "role": "teacher",      "s": 2,    "to": "/teacher"},
    {"u": "ali.rezaei",     "role": "student",      "s": 1,    "to": "/student"},
    {"u": "m.karimi",       "role": "student",      "s": 1,    "to": "/student"},
    {"u": "s.ahmadi",       "role": "student",      "s": 1,    "to": "/student"},
    {"u": "z.mousavi",      "role": "student",      "s": 1,    "to": "/student"},
    {"u": "a.najafi",       "role": "student",      "s": 1,    "to": "/student"},
    {"u": "f.hosseini",     "role": "student",      "s": 1,    "to": "/student"},
    {"u": "h.ghasemi",      "role": "student",      "s": 1,    "to": "/student"},
    {"u": "n.rezaei",       "role": "student",      "s": 1,    "to": "/student"},
    {"u": "m.taheri",       "role": "student",      "s": 1,    "to": "/student"},
    {"u": "a.tehrani",      "role": "student",      "s": 1,    "to": "/student"},
    {"u": "p.nouri",        "role": "student",      "s": 2,    "to": "/student"},
    {"u": "k.mousavi",      "role": "student",      "s": 2,    "to": "/student"},
    {"u": "d.ahmadi",       "role": "student",      "s": 2,    "to": "/student"},
    {"u": "ar.ghasemi",     "role": "student",      "s": 2,    "to": "/student"},
    {"u": "st.rezaei",      "role": "student",      "s": 2,    "to": "/student"},
    {"u": "r.karimi",       "role": "student",      "s": 2,    "to": "/student"},
    {"u": "y.sadeghi",      "role": "student",      "s": 2,    "to": "/student"},
    {"u": "ar.hosseini",    "role": "student",      "s": 2,    "to": "/student"},
]

ROOT = Path(__file__).resolve().parents[1]

# ── Colours (ANSI) ────────────────────────────────────────────────────────────
G = "\033[92m"; R = "\033[91m"; Y = "\033[93m"; B = "\033[94m"; RST = "\033[0m"

def ok(msg):  print(f"{G}[PASS]{RST} {msg}", flush=True)
def err(msg): print(f"{R}[FAIL]{RST} {msg}", flush=True)
def warn(msg):print(f"{Y}[WARN]{RST} {msg}", flush=True)
def info(msg):print(f"{B}[INFO]{RST} {msg}", flush=True)

# ── .env reader ───────────────────────────────────────────────────────────────
def read_env() -> dict[str, str]:
    values: dict[str, str] = {}
    for line in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, v = line.split("=", 1)
        values[k] = v.strip().strip('"').strip("'")
    return values

# ── DB: re-hash passwords ─────────────────────────────────────────────────────
def laravel_bcrypt(password: str) -> str:
    """Return a bcrypt hash that Laravel's BcryptHasher accepts.

    Python's bcrypt package emits the equivalent ``$2b$`` prefix, but
    Laravel/PHP's strict algorithm verification expects ``$2y$``. Keeping a
    ``$2b$`` value in the database makes Hash::check throw before verifying it.
    """
    import bcrypt

    password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
    if password_hash.startswith(b"$2b$"):
        password_hash = b"$2y$" + password_hash[4:]
    return password_hash.decode()


def rehash_passwords() -> bool:
    try:
        import pymysql
        import bcrypt
    except ImportError:
        err("pip install pymysql bcrypt  — cannot re-hash the database")
        return False

    env = read_env()
    info(f"Connecting to DB at {env['DB_HOST']}:{env.get('DB_PORT', '3306')} ...")

    for attempt in range(1, 5):
        try:
            conn = pymysql.connect(
                host=env["DB_HOST"],
                port=int(env.get("DB_PORT", "3306")),
                user=env["DB_USERNAME"],
                password=env["DB_PASSWORD"],
                database=env["DB_DATABASE"],
                charset="utf8mb4",
                autocommit=False,
                connect_timeout=15,
                cursorclass=pymysql.cursors.DictCursor,
            )
            break
        except Exception as exc:
            warn(f"DB connect attempt {attempt} failed: {exc}")
            if attempt == 4:
                err("Could not connect to DB — password re-hash aborted")
                return False
            time.sleep(attempt * 2)

    try:
        cur = conn.cursor()

        # Demo users -> Demo@1234
        demo_usernames = [u["u"] for u in DEMO_USERS]
        demo_hash = laravel_bcrypt(DEMO_PASSWORD)

        placeholders = ", ".join(["%s"] * len(demo_usernames))
        cur.execute(
            f"UPDATE users SET password=%s, updated_at=NOW() WHERE username IN ({placeholders})",
            [demo_hash] + demo_usernames,
        )
        updated = cur.rowcount
        if updated != len(demo_usernames):
            raise RuntimeError(
                f"Expected {len(demo_usernames)} demo users, but updated {updated}. "
                "The database is missing seeded accounts."
            )
        info(f"Re-hashed {updated} demo users -> '{DEMO_PASSWORD}' (Laravel $2y$ bcrypt)")

        conn.commit()
        info("DB commit done.")
        return True
    except Exception as exc:
        conn.rollback()
        err(f"DB re-hash failed: {exc}")
        return False
    finally:
        conn.close()

# ── HTTP: test login ──────────────────────────────────────────────────────────
def get_csrf(session) -> str:
    """Fetch login page and extract CSRF token from meta tag."""
    import re
    r = session.get(f"{BASE_URL}/login", timeout=20)
    r.raise_for_status()
    m = re.search(r'<meta name="csrf-token" content="([^"]+)"', r.text)
    if not m:
        # Inertia puts it as a JSON prop sometimes — try X-XSRF-TOKEN cookie
        xsrf = session.cookies.get("XSRF-TOKEN", "")
        if xsrf:
            return xsrf.replace("%3D", "=")  # URL-decode
        raise ValueError("CSRF token not found on login page")
    return m.group(1)

def test_login(username: str, password: str, expected_prefix: str, debug: bool = False) -> tuple[bool, str]:
    """
    Returns (success, detail).
    POSTs to /login as a regular form (no Inertia headers — keeps it simple).
    """
    try:
        import requests
    except ImportError:
        return False, "pip install requests"

    import requests
    session = requests.Session()
    session.headers.update({
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "fa,en;q=0.9",
        "User-Agent": "Mozilla/5.0 (DaneshyarLoginTest/2.0)",
    })

    try:
        csrf = get_csrf(session)
    except Exception as exc:
        return False, f"CSRF fetch failed: {exc}"

    payload = {
        "_token": csrf,
        "username": username,
        "password": password,
        "remember": "0",
    }

    try:
        r = session.post(
            f"{BASE_URL}/login",
            data=payload,
            headers={"Referer": f"{BASE_URL}/login"},
            timeout=25,
            allow_redirects=True,
        )
    except Exception as exc:
        return False, f"POST failed: {exc}"

    if r.status_code == 429:
        return False, "Rate limited (HTTP 429)"

    if debug:
        print(f"    [DBG] status={r.status_code} final={r.url}")
        print(f"    [DBG] body[:300]={r.text[:300]!r}")

    final_url = r.url.rstrip("/")
    final_path = final_url.replace(BASE_URL, "")

    if final_path.startswith("/login"):
        # Detect why: look for common error indicators
        body = r.text
        if "نام کاربری یا رمز عبور" in body or "invalid credentials" in body.lower():
            return False, f"Bad credentials (http {r.status_code})"
        if r.history and any(h.status_code == 419 for h in r.history):
            return False, "CSRF mismatch (419)"
        return False, f"Still on login page (http {r.status_code})"

    if expected_prefix and not final_path.startswith(expected_prefix):
        if "/profile" in final_path or "/complete" in final_path:
            return True, f"-> profile-complete flow ({final_path})"
        return False, f"Wrong redirect: expected '{expected_prefix}*', got '{final_path}'"

    return True, f"-> {final_path}"

# ── Main ──────────────────────────────────────────────────────────────────────
def main() -> None:
    parser = argparse.ArgumentParser(description="Test login for all Daneshyar demo users")
    parser.add_argument("--no-rehash",    action="store_true", help="Skip DB password re-hash step")
    parser.add_argument("--rehash-only",  action="store_true", help="Only re-hash passwords, skip HTTP tests")
    parser.add_argument("--debug",        action="store_true", help="Print HTTP status and body snippet per user")
    args = parser.parse_args()

    print()
    print("=" * 65)
    print("  Daneshyar - Login Test  /  mydaneshyar.ir")
    print("=" * 65)
    print()

    # Step 1: re-hash passwords in DB
    if not args.no_rehash:
        info("Step 1/2 — Re-hashing demo passwords in DB ...")
        if not rehash_passwords():
            sys.exit(1)
        print()
        if args.rehash_only:
            info("--rehash-only: done.")
            return
    else:
        info("Skipping DB re-hash (--no-rehash)")

    # Step 2: HTTP login tests
    info(f"Step 2/2 — Testing HTTP login at {BASE_URL} ...")
    print()

    passed = 0
    failed = 0
    results: list[tuple[str, str, bool, str]] = []

    for user in DEMO_USERS:
        username = user["u"]
        password = DEMO_PASSWORD
        role = user["role"]
        expected = user.get("to", "")

        success, detail = test_login(username, password, expected, debug=args.debug)
        results.append((username, role, success, detail))

        label = f"{username:<16} ({role:<11})"
        if success:
            ok(f"{label}  {detail}")
            passed += 1
        else:
            err(f"{label}  {detail}")
            failed += 1

        time.sleep(0.4)  # be gentle on the server

    print()
    print("=" * 65)
    print(f"  Results: {G}{passed} PASSED{RST}  /  {R}{failed} FAILED{RST}  /  {passed+failed} total")
    print("=" * 65)
    print()

    if failed > 0:
        print(f"{Y}Failed users:{RST}")
        for username, role, success, detail in results:
            if not success:
                print(f"  • {username} ({role}): {detail}")
        print()
        sys.exit(1)
    else:
        ok("All users can log in successfully!")
        sys.exit(0)


if __name__ == "__main__":
    main()
