"""Public forms (app/routers/public_forms.py) — filled in and submitted
without signing in — and verification codes for email / phone fields
(app/crud/form_verification.py), signed in or not. Delivery is mocked; the
code is read out of the captured message."""

import re
import uuid
from unittest.mock import patch

import pytest

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


@pytest.fixture(autouse=True)
def _fresh_rate_limits():
    throttle.reset()
    yield
    throttle.reset()


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


async def _form(client, headers, fields: list[dict], **settings) -> dict:
    resp = await client.post("/forms", headers=headers, json={"name": f"Pytest Public {uuid.uuid4().hex[:6]}", "settings": settings, "field_definitions": fields})
    assert resp.status_code == 201, resp.text
    published = await client.post(f"/forms/{resp.json()['form_id']}/publish", headers=headers)
    assert published.status_code == 200, published.text
    return published.json()


CONTACT = [
    {"name": "ref", "type": "autonumber", "label": "Ticket number", "autonumber_format": "T-{SEQ:3}"},
    {"name": "name", "type": "text", "label": "Name", "required": True},
    {"name": "topic", "type": "dropdown", "label": "Topic", "options": ["Fares", "Routes"]},
    {"name": "message", "type": "textarea", "label": "Message", "required": True},
]


async def test_public_form_round_trip(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    form = await _form(client, auth_headers, CONTACT, public_access=True, public_message="Thanks — we'll reply within two days.")
    base = f"/public/forms/{tid}/{form['form_id']}"

    got = await client.get(base)
    assert got.status_code == 200, got.text
    body = got.json()
    assert body["name"] == form["name"] and [f["name"] for f in body["field_definitions"]] == ["ref", "name", "topic", "message"]
    assert body["recaptcha_site_key"] is None

    # Checked like any submission: required fields, options...
    bad = await client.post(f"{base}/submissions", json={"field_values": {"name": "Asha", "topic": "Parking"}})
    assert bad.status_code == 422
    ok = await client.post(f"{base}/submissions", json={"field_values": {"name": "Asha", "topic": "Fares", "message": "The 7:10 bus was late."}})
    assert ok.status_code == 201, ok.text
    assert ok.json()["message"] == "Thanks — we'll reply within two days."
    assert ok.json()["references"] == {"Ticket number": "T-001"}

    # ...and lands with the form's other submissions, sent by no one.
    listed = await client.get(f"/forms/{form['form_id']}/submissions", headers=auth_headers)
    assert listed.status_code == 200, listed.text
    rows = listed.json()
    rows = rows.get("items", rows) if isinstance(rows, dict) else rows
    assert any(r["field_values"].get("message") == "The 7:10 bus was late." and r["submitted_by"] is None for r in rows)


async def test_only_public_forms_are_served(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    private = await _form(client, auth_headers, CONTACT)
    assert (await client.get(f"/public/forms/{tid}/{private['form_id']}")).status_code == 404
    assert (await client.post(f"/public/forms/{tid}/{private['form_id']}/submissions", json={"field_values": {}})).status_code == 404
    # The right form under another workspace's id isn't found either.
    public = await _form(client, auth_headers, CONTACT, public_access=True)
    assert (await client.get(f"/public/forms/{uuid.uuid4()}/{public['form_id']}")).status_code == 404


async def test_signed_in_only_fields_are_refused(client, auth_headers):
    resp = await client.post(
        "/forms",
        headers=auth_headers,
        json={
            "name": f"Pytest Public bad {uuid.uuid4().hex[:6]}",
            "settings": {"public_access": True},
            "field_definitions": [
                {"name": "who", "type": "user", "label": "Assignee", "user_type": "internal"},
                {"name": "doc", "type": "file_upload", "label": "Document"},  # fine: public upload endpoint
                {"name": "office", "type": "dropdown", "label": "Office", "options": ["A"], "option_filters": [{"field": "", "operator": "equals", "value": "{{session.office}}"}]},
            ],
        },
    )
    assert resp.status_code == 422
    detail = resp.json()["detail"]
    assert detail["code"] == "not_public_safe" and len(detail["problems"]) == 2


async def test_public_datasource_options_show_only_what_the_form_needs(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    master = await client.post(
        "/masters",
        headers=auth_headers,
        json={"name": f"Pytest Depots {uuid.uuid4().hex[:6]}", "field_schema": [{"name": "depot", "type": "varchar"}, {"name": "secret_note", "type": "varchar"}]},
    )
    mid = master.json()["id"]
    rec = await client.post(f"/masters/{mid}/records", headers=auth_headers, json={"field_values": {"depot": "Ernakulam", "secret_note": "internal"}})
    form = await _form(
        client, auth_headers, [{"name": "depot", "type": "dropdown", "label": "Depot", "master_id": mid, "master_display_field": "depot"}], public_access=True
    )
    opts = await client.get(f"/public/forms/{tid}/{form['form_id']}/options/depot")
    assert opts.status_code == 200, opts.text
    assert opts.json() == [{"id": rec.json()["id"], "field_values": {"depot": "Ernakulam"}}]
    sent = await client.post(f"/public/forms/{tid}/{form['form_id']}/submissions", json={"field_values": {"depot": rec.json()["id"]}})
    assert sent.status_code == 201, sent.text


async def test_captcha_is_required_when_asked(client, auth_headers, test_tenant, monkeypatch):
    from app.core.config import settings

    monkeypatch.setattr(settings, "recaptcha_site_key", "site-key")
    monkeypatch.setattr(settings, "recaptcha_secret_key", "secret-key")
    tid = test_tenant["tenant_id"]
    form = await _form(client, auth_headers, CONTACT, public_access=True, public_captcha=True)
    base = f"/public/forms/{tid}/{form['form_id']}"
    assert (await client.get(base)).json()["recaptcha_site_key"] == "site-key"
    values = {"name": "Asha", "message": "Hello"}
    no_token = await client.post(f"{base}/submissions", json={"field_values": values})
    assert no_token.status_code == 400
    with patch("app.core.recaptcha._sync_verify", return_value={"success": True}):
        ok = await client.post(f"{base}/submissions", json={"field_values": values, "captcha_token": "tok"})
    assert ok.status_code == 201, ok.text


async def test_submissions_are_rate_limited(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    form = await _form(client, auth_headers, [{"name": "n", "type": "text", "label": "N"}], public_access=True)
    url = f"/public/forms/{tid}/{form['form_id']}/submissions"
    codes = [(await client.post(url, json={"field_values": {"n": str(i)}})).status_code for i in range(11)]
    assert codes[:10] == [201] * 10 and codes[10] == 429


async def test_public_email_verification(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    await _connector(tid, "email_smtp", {"host": "smtp.example.com", "from": "noreply@example.com"})
    form = await _form(
        client, auth_headers, [{"name": "email", "type": "email", "label": "Email", "required": True, "require_verification": True}], public_access=True
    )
    base = f"/public/forms/{tid}/{form['form_id']}"

    unverified = await client.post(f"{base}/submissions", json={"field_values": {"email": "asha@example.com"}})
    assert unverified.status_code == 422
    assert "Confirm this email address" in unverified.json()["detail"][0]["msg"]

    sent: list[tuple[str, str]] = []
    with patch("app.crud.form_verification._send_email_sync", side_effect=lambda cfg, to, subject, body: sent.append((to, body))):
        started = await client.post(f"{base}/verify/send", json={"field": "email", "value": " Asha@Example.com "})
        assert started.status_code == 200, started.text
        assert started.json()["sent_to"] == "a•••@example.com"
        # One code at a time.
        again = await client.post(f"{base}/verify/send", json={"field": "email", "value": "asha@example.com"})
        assert again.status_code == 429
    assert sent[0][0] == "asha@example.com"
    code = re.search(r"\b(\d{6})\b", sent[0][1]).group(1)
    vid = started.json()["verification_id"]

    wrong = await client.post(f"{base}/verify/confirm", json={"verification_id": vid, "code": "000000" if code != "000000" else "111111"})
    assert wrong.status_code == 400 and "4 attempts left" in wrong.json()["detail"]
    right = await client.post(f"{base}/verify/confirm", json={"verification_id": vid, "code": code})
    assert right.status_code == 200, right.text
    token = right.json()["token"]
    # A code works once.
    assert (await client.post(f"{base}/verify/confirm", json={"verification_id": vid, "code": code})).status_code == 409

    # The token is for that address only.
    other = await client.post(f"{base}/submissions", json={"field_values": {"email": "someone@example.com"}, "verifications": {"email": token}})
    assert other.status_code == 422
    done = await client.post(f"{base}/submissions", json={"field_values": {"email": "ASHA@example.com"}, "verifications": {"email": token}})
    assert done.status_code == 201, done.text


async def test_signed_in_phone_verification(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    await _connector(tid, "sms", {"api_url": "https://sms.example.com/send"})
    form = await _form(client, auth_headers, [{"name": "mobile", "type": "phone", "label": "Mobile", "default_country": "+91", "require_verification": True}])
    texts: list[tuple[str, str]] = []

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

    with patch("app.crud.form_verification.send_sms", side_effect=fake_send):
        started = await client.post("/form-submissions/verify/send", headers=auth_headers, json={"form_id": form["form_id"], "field": "mobile", "value": "98470 12345"})
    assert started.status_code == 200, started.text
    assert texts[0][0] == "+919847012345" and started.json()["sent_to"] == "•••• 2345"
    code = re.search(r"\b(\d{6})\b", texts[0][1]).group(1)
    confirmed = await client.post(
        "/form-submissions/verify/confirm", headers=auth_headers, json={"form_id": form["form_id"], "verification_id": started.json()["verification_id"], "code": code}
    )
    assert confirmed.status_code == 200, confirmed.text

    payload = {"form_id": form["form_id"], "form_version": form["version"], "field_values": {"mobile": "+91 98470-12345"}}
    assert (await client.post("/form-submissions", headers=auth_headers, json=payload)).status_code == 422
    created = await client.post("/form-submissions", headers=auth_headers, json={**payload, "verifications": {"mobile": confirmed.json()["token"]}})
    assert created.status_code == 201, created.text
    # Once it's in, someone allowed to edit it can correct the number without
    # the submitter's code (the Submissions page) — the code is asked for on submit.
    edited = await client.patch(f"/form-submissions/{created.json()['id']}", headers=auth_headers, json={"field_values": {"mobile": "+91 9847099999"}})
    assert edited.status_code == 200, edited.text
    assert edited.json()["field_values"]["mobile"] == "+91 9847099999"


async def test_verification_needs_a_connector(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    form = await _form(client, auth_headers, [{"name": "email", "type": "email", "label": "Email", "require_verification": True}], public_access=True)
    resp = await client.post(f"/public/forms/{tid}/{form['form_id']}/verify/send", json={"field": "email", "value": "a@example.com"})
    assert resp.status_code == 503 and "connector" in resp.json()["detail"]
    # Only verified fields send codes.
    bad = await client.post("/forms", headers=auth_headers, json={"name": "x", "field_definitions": [{"name": "t", "type": "text", "label": "T", "require_verification": True}]})
    assert bad.status_code == 422


async def test_save_and_continue_later(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    form = await _form(client, auth_headers, CONTACT, public_access=True, allow_drafts=True)
    base = f"/public/forms/{tid}/{form['form_id']}"

    # Part-way: required fields aren't checked yet, shapes are.
    assert (await client.post(f"{base}/drafts", json={"field_values": {"topic": "Parking"}})).status_code == 422
    saved = await client.post(f"{base}/drafts", json={"field_values": {"name": "Asha", "topic": "Fares"}})
    assert saved.status_code == 200, saved.text
    token = saved.json()["resume_token"]

    opened = await client.get(f"{base}/drafts/{token}")
    assert opened.status_code == 200, opened.text
    assert opened.json()["field_values"]["name"] == "Asha" and opened.json()["field_values"]["topic"] == "Fares"
    again = await client.post(f"{base}/drafts", json={"field_values": {"name": "Asha M", "topic": "Fares"}, "resume_token": token})
    assert again.status_code == 200 and again.json()["resume_token"] == token
    assert (await client.get(f"{base}/drafts/{token}")).json()["field_values"]["name"] == "Asha M"

    # A draft isn't a submission yet...
    listed = (await client.get(f"/forms/{form['form_id']}/submissions", headers=auth_headers)).json()
    assert not any((r["field_values"] or {}).get("name") == "Asha M" for r in listed)
    # ...until it's finished: checked in full, numbered, and the link stops working.
    incomplete = await client.post(f"{base}/submissions", json={"field_values": {"name": "Asha M"}, "resume_token": token})
    assert incomplete.status_code == 422
    done = await client.post(f"{base}/submissions", json={"field_values": {"name": "Asha M", "message": "Later, but done."}, "resume_token": token})
    assert done.status_code == 201, done.text
    assert done.json()["references"] == {"Ticket number": "T-001"}
    assert (await client.get(f"{base}/drafts/{token}")).status_code == 404
    assert (await client.get(f"{base}/drafts/not-a-token")).status_code == 404

    # Only on forms that allow drafts.
    plain = await _form(client, auth_headers, CONTACT, public_access=True)
    assert (await client.post(f"/public/forms/{tid}/{plain['form_id']}/drafts", json={"field_values": {}})).status_code == 422
