"""Tenant Admin registration settings (Users page → "Registration
settings"): which built-in fields are required, validation rules + messages
on built-in and extra fields, and the verification every self-registered
account must complete afterwards (email / SMS / both)."""

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 = "register-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 _register(tenant_id, **overrides):
    return {
        "tenant_id": str(tenant_id),
        "organization_name": "Acme Supplies",
        "name": "Rita Registrant",
        "email": "rita@example.com",
        "password": PASSWORD,
        "phone": "+91 98470 12345",
        **overrides,
    }


def _extra_fields():
    return [
        {
            "key": "gst_number",
            "label": "GST Number",
            "type": "text",
            "required": True,
            "required_message": "Please give your {label}.",
            "validation_rules": [{"type": "pattern", "value": "\\d{2}[A-Z]{5}\\d{4}[A-Z]\\d[A-Z\\d]{2}", "message": "{label} must be a 15-character GSTIN"}],
        },
        {"key": "employees", "label": "Employees", "type": "number", "validation_rules": [{"type": "min_value", "value": 1}]},
    ]


async def test_settings_round_trip_and_are_validated(client, auth_headers):
    settings = {
        "phone": {"required": False, "hidden": False},
        "name": {"validation_rules": [{"type": "min_length", "value": 3}]},
        "verification": "none",
    }
    resp = await client.patch("/tenants/me", headers=auth_headers, json={"registration_field_schema": _extra_fields(), "registration_settings": settings})
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert body["registration_settings"]["phone"]["required"] is False
    assert body["registration_settings"]["name"]["validation_rules"][0]["type"] == "min_length"
    assert body["registration_field_schema"][0]["required_message"] == "Please give your {label}."

    # Saving only the field list leaves the settings untouched.
    again = await client.patch("/tenants/me", headers=auth_headers, json={"registration_field_schema": []})
    assert again.json()["registration_settings"]["phone"]["required"] is False

    def bad(payload):
        return client.patch("/tenants/me", headers=auth_headers, json={"registration_field_schema": [], **payload})

    # A rule that doesn't fit the field type, an optional org name, SMS without a required phone, SMS without a connector.
    assert (await bad({"registration_field_schema": [{"key": "b", "label": "B", "type": "boolean", "validation_rules": [{"type": "min_length", "value": 2}]}]})).status_code == 422
    assert (await bad({"registration_settings": {"organization_name": {"required": False}}})).status_code == 422
    assert (await bad({"registration_settings": {"phone": {"hidden": True}, "verification": "sms"}})).status_code == 422
    no_conn = await bad({"registration_settings": {"verification": "sms"}})
    assert no_conn.status_code == 422 and "connector" in no_conn.json()["detail"]


async def test_registration_enforces_required_fields_and_rules(client, auth_headers, test_tenant):
    tenant_id = test_tenant["tenant_id"]
    settings = {"phone": {"required": False}, "name": {"validation_rules": [{"type": "min_length", "value": 3, "message": "Use your full name"}]}}
    await client.patch("/tenants/me", headers=auth_headers, json={"registration_field_schema": _extra_fields(), "registration_settings": settings})

    public = (await client.get(f"/auth/register/fields?tenant_id={tenant_id}")).json()
    assert public["settings"]["phone"]["required"] is False and public["fields"][0]["validation_rules"]

    async def detail(**overrides):
        resp = await client.post("/auth/register", json=_register(tenant_id, **overrides))
        return resp.status_code, resp.json().get("detail")

    assert await detail(name="Al", extra_fields={"gst_number": "27AAPFU0939F1ZV"}) == (400, "Your name: Use your full name")
    assert await detail(extra_fields={}) == (400, "Please give your GST Number.")
    assert await detail(extra_fields={"gst_number": "nope"}) == (400, "GST Number: GST Number must be a 15-character GSTIN")
    assert await detail(extra_fields={"gst_number": "27AAPFU0939F1ZV", "employees": 0}) == (400, "Employees: Must be at least 1.")
    # Phone optional now — registering without one works.
    status, _ = await detail(phone=None, extra_fields={"gst_number": "27AAPFU0939F1ZV", "employees": 12})
    assert status == 201


async def test_both_verification_email_then_sms(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"})
    await _connector(tenant_id, "sms", {"api_url": "https://sms.example.com/send"})
    saved = await client.patch("/tenants/me", headers=auth_headers, json={"registration_field_schema": [], "registration_settings": {"verification": "both"}})
    assert saved.status_code == 200, saved.text

    emails: list[str] = []
    texts: list[str] = []

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

    with patch("app.crud.user_verification._send_email_sync", side_effect=lambda cfg, to, subject, body: emails.append(body)):
        reg = await client.post("/auth/register", headers={"Origin": "https://portal.example.com"}, json=_register(tenant_id))
    assert reg.status_code == 201, reg.text
    assert reg.json()["verification"] == "both" and reg.json()["verification_email_sent"] is True
    assert "text a code" in reg.json()["detail"]

    # Admin activates the registration (the existing approval step).
    users = (await client.get("/users", headers=auth_headers)).json()
    rita = next(u for u in users if u["email"] == "rita@example.com")
    assert rita["verification_method"] == "both" and rita["verification_pending"] is True
    await client.patch(f"/users/{rita['id']}", headers=auth_headers, json={"status": "active"})

    login = {"tenant_id": str(tenant_id), "email": "rita@example.com", "password": PASSWORD}
    blocked = await client.post("/auth/login", json=login)
    assert blocked.status_code == 403 and blocked.json()["detail"]["code"] == "email_verification_required"

    token = parse_qs(urlparse(re.search(r"https://portal\.example\.com/verify-email\?\S+", emails[0]).group(0)).query)["token"][0]
    code = re.search(r"^\s+(\d{6})$", emails[0], re.M).group(1)
    ok = await client.post("/auth/verify-email", json={"tenant_id": str(tenant_id), "token": token, "otp": code})
    assert ok.status_code == 200, ok.text

    # Email done — SMS still pending, so sign-in asks for the texted code.
    with patch("app.crud.user_verification.send_sms", side_effect=fake_sms):
        step = (await client.post("/auth/login", json=login)).json()
    assert step["verification_required"] == "sms" and step["access_token"] is None
    sms_code = re.match(r"(\d{6})", texts[0]).group(1)
    done = await client.post("/auth/login/verify-otp", json={"tenant_id": str(tenant_id), "challenge_token": step["challenge_token"], "otp": sms_code})
    assert done.status_code == 200 and done.json()["access_token"]
    user = done.json()["user"]
    assert user["email_verified_at"] and user["phone_verified_at"] and user["verified_at"] and user["verification_pending"] is False
