"""End-to-end tests for the WhatsApp 2FA OTP layer (Meta Cloud API).

Runs in STUB mode by default — i.e., no META_WA_* env vars are required. The
backend exposes the OTP code in the JSON response (`code_preview`) when
EXPOSE_WA_OTP_CODE=1 and creds are missing. These tests assert the full
challenge → send → verify → invalidation cycle works without breaking the
existing /api/auth/login behaviour for users who have WA 2FA disabled.
"""
import os
import pytest
import requests

BASE = os.environ.get("REACT_APP_BACKEND_URL", "https://offline-billing-pro-2.preview.emergentagent.com").rstrip("/")
ADMIN_EMAIL = os.environ.get("TEST_ADMIN_EMAIL", "regalmarketing2024@gmail.com")
ADMIN_PASSWORD = os.environ.get("TEST_ADMIN_PASSWORD", "Rvasa@#9955")


def _new_session():
    s = requests.Session()
    s.headers.update({"Content-Type": "application/json"})
    return s


def _login(session, email, password):
    return session.post(f"{BASE}/api/auth/login", json={"email": email, "password": password}, timeout=30)


# ---------------------------------------------------------------------------
# Fresh admin session that uses standard login (no WA OTP gate).
# Needed because admin_session in conftest might be polluted by other tests.
# ---------------------------------------------------------------------------
@pytest.fixture(scope="module")
def admin():
    s = _new_session()
    r = _login(s, ADMIN_EMAIL, ADMIN_PASSWORD)
    # If admin has wa_otp_enabled=True from a previous run, the response will
    # contain `requires_wa_otp` — clean it by setting enabled=False via the
    # admin policy endpoint after a one-step OTP verify.
    if r.status_code == 200 and r.json().get("requires_wa_otp"):
        # Recover: send + verify + then disable per-user
        challenge = r.json()["challenge_token"]
        send = s.post(f"{BASE}/api/auth/wa-otp/send", json={"challenge_token": challenge}, timeout=20)
        assert send.status_code == 200, send.text
        body = send.json()
        code = body.get("code_preview")
        assert code, "Stub mode required for tests — set EXPOSE_WA_OTP_CODE=1"
        v = s.post(f"{BASE}/api/auth/wa-otp/verify",
                   json={"otp_token": body["otp_token"], "code": code}, timeout=20)
        assert v.status_code == 200, v.text
        # Disable WA for self so the rest of the test suite stays clean
        off = s.post(f"{BASE}/api/wa-auth/me/toggle", json={"enabled": False}, timeout=20)
        assert off.status_code == 200, off.text
        # Re-login to refresh state
        r = _login(s, ADMIN_EMAIL, ADMIN_PASSWORD)
    assert r.status_code == 200, f"Admin login failed: {r.status_code} {r.text}"
    assert "requires_wa_otp" not in r.json(), "Fixture polluted — wa_otp still on"
    return s


# ---------------------------------------------------------------------------
# Endpoint surface — should be 401 without auth
# ---------------------------------------------------------------------------
def test_endpoints_require_auth():
    s = _new_session()
    assert s.get(f"{BASE}/api/wa-auth/me").status_code == 401
    assert s.get(f"{BASE}/api/admin/wa-auth/policy").status_code == 401
    assert s.get(f"{BASE}/api/admin/wa-auth/users").status_code == 401
    assert s.put(f"{BASE}/api/admin/wa-auth/users/abc", json={"enabled": True}).status_code == 401


# ---------------------------------------------------------------------------
# Admin policy CRUD
# ---------------------------------------------------------------------------
def test_admin_get_policy_defaults(admin):
    r = admin.get(f"{BASE}/api/admin/wa-auth/policy", timeout=20)
    assert r.status_code == 200, r.text
    data = r.json()
    assert data["_id"] == "global"
    assert data["enabled"] is True
    assert "expiry_sec" in data
    assert "provider_configured" in data


def test_admin_policy_update_bounds(admin):
    # Invalid bounds rejected
    r = admin.put(f"{BASE}/api/admin/wa-auth/policy", json={"expiry_sec": 10}, timeout=20)
    assert r.status_code == 400
    r = admin.put(f"{BASE}/api/admin/wa-auth/policy", json={"max_retries": 99}, timeout=20)
    assert r.status_code == 400
    r = admin.put(f"{BASE}/api/admin/wa-auth/policy", json={"resend_cooldown_sec": 5}, timeout=20)
    assert r.status_code == 400
    # Valid update accepted + persisted
    r = admin.put(f"{BASE}/api/admin/wa-auth/policy",
                  json={"expiry_sec": 120, "max_retries": 4, "resend_cooldown_sec": 45},
                  timeout=20)
    assert r.status_code == 200
    data = r.json()
    assert data["expiry_sec"] == 120
    assert data["max_retries"] == 4
    assert data["resend_cooldown_sec"] == 45
    # Reset to defaults — restore ALL fields including booleans the
    # admin fixture relies on (any "force_*" toggle left as True
    # would break every subsequent login).
    admin.put(f"{BASE}/api/admin/wa-auth/policy", json={
        "expiry_sec": 90, "max_retries": 5, "resend_cooldown_sec": 30,
        "force_enable_for_all": False, "force_enable_for_admins_only": False,
        "enabled": True,
    }, timeout=20)


# ---------------------------------------------------------------------------
# Admin users list + per-user toggle
# ---------------------------------------------------------------------------
def test_admin_users_listing(admin):
    r = admin.get(f"{BASE}/api/admin/wa-auth/users", timeout=20)
    assert r.status_code == 200
    users = r.json()
    assert isinstance(users, list)
    assert len(users) >= 1
    first = users[0]
    assert "id" in first and "email" in first and "wa_otp_enabled" in first


def test_admin_toggle_user_without_phone_rejects(admin):
    """User without a phone number cannot be enabled."""
    # Find ANY user that has no phone — if all have phones, just verify the
    # endpoint exists with a synthetic uid.
    r = admin.get(f"{BASE}/api/admin/wa-auth/users", timeout=20)
    users = r.json()
    target = next((u for u in users if not (u.get("phone") or "").strip()), None)
    if target is None:
        # Use bogus id to exercise 404 branch instead
        r = admin.put(f"{BASE}/api/admin/wa-auth/users/000000000000000000000000",
                      json={"enabled": True}, timeout=20)
        assert r.status_code in (400, 404)
        return
    r = admin.put(f"{BASE}/api/admin/wa-auth/users/{target['id']}",
                  json={"enabled": True}, timeout=20)
    assert r.status_code == 400
    assert "phone" in r.json().get("detail", "").lower()


# ---------------------------------------------------------------------------
# Full E2E: enable WA OTP for admin → login → send → verify
# ---------------------------------------------------------------------------
def test_full_wa_otp_flow_end_to_end(admin):
    # Get admin's own user record
    me = admin.get(f"{BASE}/api/auth/me", timeout=20).json()
    uid = me["id"]
    # Ensure phone is set
    admin.get(f"{BASE}/api/wa-auth/me", timeout=20)  # touch endpoint

    # Enable for self via /me/toggle (the user already has phone seeded)
    on = admin.post(f"{BASE}/api/wa-auth/me/toggle", json={"enabled": True}, timeout=20)
    if on.status_code == 400 and "phone" in on.json().get("detail", "").lower():
        pytest.skip("Admin user has no phone — cannot run E2E WA flow")
    assert on.status_code == 200, on.text

    try:
        # Fresh session — login should now return requires_wa_otp
        s2 = _new_session()
        r = _login(s2, ADMIN_EMAIL, ADMIN_PASSWORD)
        assert r.status_code == 200
        data = r.json()
        assert data.get("requires_wa_otp") is True, f"Expected wa_otp gate, got {data}"
        assert data.get("challenge_token")
        assert data.get("masked_phone", "").startswith("+") or "XXXX" in data.get("masked_phone", "")

        # Send OTP
        send = s2.post(f"{BASE}/api/auth/wa-otp/send",
                       json={"challenge_token": data["challenge_token"]}, timeout=20)
        assert send.status_code == 200, send.text
        body = send.json()
        assert body["ok"] is True
        assert body["delivered_via"] in ("whatsapp", "stub", "failed")
        otp_token = body["otp_token"]

        # Stub mode → code visible
        code = body.get("code_preview")
        assert code, "STUB mode should expose code_preview when META not configured"
        assert len(code) == 6 and code.isdigit()

        # Wrong code → 401
        wrong = s2.post(f"{BASE}/api/auth/wa-otp/verify",
                        json={"otp_token": otp_token, "code": "000000"}, timeout=20)
        assert wrong.status_code == 401

        # Correct code → cookies issued, user payload returned
        ok = s2.post(f"{BASE}/api/auth/wa-otp/verify",
                     json={"otp_token": otp_token, "code": code}, timeout=20)
        assert ok.status_code == 200, ok.text
        payload = ok.json()
        assert payload["id"] == uid
        assert payload["email"] == ADMIN_EMAIL

        # OTP is single-use — second verify fails
        again = s2.post(f"{BASE}/api/auth/wa-otp/verify",
                        json={"otp_token": otp_token, "code": code}, timeout=20)
        assert again.status_code == 401

        # /auth/me works with the new cookies
        me2 = s2.get(f"{BASE}/api/auth/me", timeout=20)
        assert me2.status_code == 200
        assert me2.json()["email"] == ADMIN_EMAIL
    finally:
        # Cleanup — turn WA 2FA off so other tests aren't affected
        admin.post(f"{BASE}/api/wa-auth/me/toggle", json={"enabled": False}, timeout=20)


def test_resend_cooldown_returns_same_token(admin):
    """Re-issuing /send within cooldown should return the same otp_token
    (no duplicate WhatsApp send)."""
    # Touch /auth/me so the admin session is warmed before we toggle WA OTP.
    admin.get(f"{BASE}/api/auth/me", timeout=20)
    on = admin.post(f"{BASE}/api/wa-auth/me/toggle", json={"enabled": True}, timeout=20)
    if on.status_code != 200:
        pytest.skip("Cannot enable WA OTP")
    try:
        s2 = _new_session()
        r = _login(s2, ADMIN_EMAIL, ADMIN_PASSWORD)
        if not r.json().get("requires_wa_otp"):
            pytest.skip("WA OTP gate not hit — check phone field")
        challenge = r.json()["challenge_token"]
        first = s2.post(f"{BASE}/api/auth/wa-otp/send", json={"challenge_token": challenge}, timeout=20).json()
        second = s2.post(f"{BASE}/api/auth/wa-otp/send", json={"challenge_token": challenge}, timeout=20).json()
        assert first["otp_token"] == second["otp_token"], "cooldown should reuse same OTP token"
        assert second.get("cooldown") is True
    finally:
        admin.post(f"{BASE}/api/wa-auth/me/toggle", json={"enabled": False}, timeout=20)


def test_bad_challenge_token_rejected(admin):
    s = _new_session()
    r = s.post(f"{BASE}/api/auth/wa-otp/send",
               json={"challenge_token": "garbage.token.xxx"}, timeout=20)
    assert r.status_code == 401


def test_bad_otp_token_rejected(admin):
    s = _new_session()
    r = s.post(f"{BASE}/api/auth/wa-otp/verify",
               json={"otp_token": "not-a-real-token", "code": "123456"}, timeout=20)
    assert r.status_code == 401


# ---------------------------------------------------------------------------
# /wa-auth/me — self-status & toggle
# ---------------------------------------------------------------------------
def test_self_status_returns_policy(admin):
    r = admin.get(f"{BASE}/api/wa-auth/me", timeout=20)
    assert r.status_code == 200
    data = r.json()
    assert "enabled" in data
    assert "phone" in data
    assert "policy" in data
    assert "expiry_sec" in data["policy"]


def test_webhook_verify_handshake():
    """Meta sends a GET handshake — must respond with the challenge token."""
    s = _new_session()
    r = s.get(f"{BASE}/api/webhooks/whatsapp", params={
        "hub.mode": "subscribe",
        "hub.verify_token": "wrong-token",
        "hub.challenge": "1234567",
    }, timeout=20)
    assert r.status_code == 403  # wrong verify token
