"""New-user verification chosen at creation (Users page): "email" sends a
link + code and blocks sign-in until verified; "sms" texts a code at first
sign-in and only then issues the session. Delivery is mocked — the email
body / SMS text are captured so the tests can read the code out of them."""

import re
import uuid
from unittest.mock import patch
from urllib.parse import parse_qs, urlparse

from tests.conftest import AsyncSessionLocal
from app.models.reporting import Connector

PASSWORD = "verify-me-123"


async def _connector(tenant_id: uuid.UUID, type_: str, config: dict) -> None:
    async with AsyncSessionLocal() as db:
        async with db.begin():
            db.add(Connector(tenant_id=tenant_id, name=f"Pytest {type_}", type=type_, configuration=config, status="enabled"))


def _new_user(email: str, **extra) -> dict:
    return {"name": "Verify Me", "email": email, "password": PASSWORD, **extra}


def _login(tenant_id, email):
    return {"tenant_id": str(tenant_id), "email": email, "password": PASSWORD}


async def test_verification_needs_a_connector(client, auth_headers):
    resp = await client.post("/users", headers=auth_headers, json=_new_user("noconn@example.com", verification_method="email"))
    assert resp.status_code == 422
    assert "connector" in resp.json()["detail"]
    users = (await client.get("/users", headers=auth_headers)).json()
    assert not any(u["email"] == "noconn@example.com" for u in users)

    no_phone = await client.post("/users", headers=auth_headers, json=_new_user("nophone@example.com", verification_method="sms"))
    assert no_phone.status_code == 422


async def test_email_verification_flow(client, auth_headers, test_tenant):
    tenant_id = test_tenant["tenant_id"]
    await _connector(tenant_id, "email_smtp", {"host": "smtp.example.com", "from": "noreply@example.com"})
    sent: list[str] = []

    with patch("app.crud.user_verification._send_email_sync", side_effect=lambda cfg, to, subject, body: sent.append(body)):
        created = await client.post(
            "/users", headers={**auth_headers, "Origin": "https://app.example.com"}, json=_new_user("ella@example.com", verification_method="email")
        )
    assert created.status_code == 201, created.text
    body = created.json()
    assert body["verification_delivery"] == "sent"
    assert body["verification_pending"] is True and body["verification_method"] == "email"

    link = re.search(r"https://app\.example\.com/verify-email\?\S+", sent[0]).group(0)
    token = parse_qs(urlparse(link).query)["token"][0]
    code = re.search(r"^\s+(\d{6})$", sent[0], re.M).group(1)

    # Blocked until verified — with a machine-readable reason for the login page.
    blocked = await client.post("/auth/login", json=_login(tenant_id, "ella@example.com"))
    assert blocked.status_code == 403
    assert blocked.json()["detail"]["code"] == "email_verification_required"

    wrong = await client.post("/auth/verify-email", json={"tenant_id": str(tenant_id), "token": token, "otp": "000000" if code != "000000" else "111111"})
    assert wrong.status_code == 400 and "4 attempts left" in wrong.json()["detail"]

    ok = await client.post("/auth/verify-email", json={"tenant_id": str(tenant_id), "token": token, "otp": code})
    assert ok.status_code == 200, ok.text
    # The link is single-use.
    again = await client.post("/auth/verify-email", json={"tenant_id": str(tenant_id), "token": token, "otp": code})
    assert again.status_code == 400

    login = await client.post("/auth/login", json=_login(tenant_id, "ella@example.com"))
    assert login.status_code == 200 and login.json()["access_token"]
    assert login.json()["user"]["verification_pending"] is False


async def test_email_resend_needs_password_and_cooldown(client, auth_headers, test_tenant):
    tenant_id = test_tenant["tenant_id"]
    await _connector(tenant_id, "email_smtp", {"host": "smtp.example.com", "from": "noreply@example.com"})
    with patch("app.crud.user_verification._send_email_sync"):
        await client.post("/users", headers=auth_headers, json=_new_user("rae@example.com", verification_method="email"))
        bad_pw = await client.post(
            "/auth/resend-email-verification", json={"tenant_id": str(tenant_id), "email": "rae@example.com", "password": "wrong-password"}
        )
        assert bad_pw.status_code == 401
        too_soon = await client.post("/auth/resend-email-verification", json=_login(tenant_id, "rae@example.com"))
        assert too_soon.status_code == 429


async def test_sms_first_login_flow(client, auth_headers, test_tenant):
    tenant_id = test_tenant["tenant_id"]
    await _connector(tenant_id, "sms", {"api_url": "https://sms.example.com/send"})
    texts: list[tuple[str, str]] = []

    async def fake_send(config, to, message):
        texts.append((to, message))
        return 200

    created = await client.post("/users", headers=auth_headers, json=_new_user("sam@example.com", verification_method="sms", phone="+91 98470 12345"))
    assert created.status_code == 201, created.text
    assert created.json()["verification_delivery"] is None  # nothing sent until first sign-in

    with patch("app.crud.user_verification.send_sms", side_effect=fake_send):
        first = await client.post("/auth/login", json=_login(tenant_id, "sam@example.com"))
    assert first.status_code == 200
    step = first.json()
    assert step["access_token"] is None and step["verification_required"] == "sms"
    assert step["phone_hint"] == "•••• 2345"
    assert texts[0][0] == "+91 98470 12345"
    code = re.match(r"(\d{6}) is your", texts[0][1]).group(1)

    # The challenge token is not a session.
    as_session = await client.get("/notifications", headers={"Authorization": f"Bearer {step['challenge_token']}"})
    assert as_session.status_code == 401

    challenge = {"tenant_id": str(tenant_id), "challenge_token": step["challenge_token"]}
    wrong = await client.post("/auth/login/verify-otp", json={**challenge, "otp": "000000" if code != "000000" else "111111"})
    assert wrong.status_code == 400
    ok = await client.post("/auth/login/verify-otp", json={**challenge, "otp": code})
    assert ok.status_code == 200, ok.text
    assert ok.json()["access_token"] and ok.json()["user"]["verification_pending"] is False

    # Later sign-ins are normal — no second text.
    with patch("app.crud.user_verification.send_sms", side_effect=fake_send):
        later = await client.post("/auth/login", json=_login(tenant_id, "sam@example.com"))
    assert later.json()["access_token"] and len(texts) == 1


async def test_sms_gateway_failure_blocks_with_clear_error(client, auth_headers, test_tenant):
    tenant_id = test_tenant["tenant_id"]
    await _connector(tenant_id, "sms", {"api_url": "https://sms.example.com/send"})
    await client.post("/users", headers=auth_headers, json=_new_user("gus@example.com", verification_method="sms", phone="+14155550100"))

    async def refused(config, to, message):
        return 500

    with patch("app.crud.user_verification.send_sms", side_effect=refused):
        resp = await client.post("/auth/login", json=_login(tenant_id, "gus@example.com"))
    assert resp.status_code == 503
    assert "HTTP 500" in resp.json()["detail"]
