"""The workspace's own account emails (app/crud/account_emails.py): their
templates, "Forgot password?", and the password-changed and
registration-received notices. Delivery is mocked; the emails go through
the job queue, run here with jobs.run_due."""

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

import smtplib
from sqlalchemy import text

from app.core import jobs
from app.crud import account_emails
from app.models.reporting import Connector
from tests.conftest import TEST_ADMIN_PASSWORD, AsyncSessionLocal

SEND = "app.crud.account_emails._send_email_sync"


async def _connector(tenant_id) -> uuid.UUID:
    async with AsyncSessionLocal() as db:
        async with db.begin():
            c = Connector(tenant_id=tenant_id, name="Pytest SMTP", type="email_smtp", configuration={"host": "smtp.example.com", "from": "noreply@example.com"}, status="enabled")
            db.add(c)
            await db.flush()
            return c.id


async def _deliver(tenant_id) -> list[tuple[str, str, str]]:
    """Runs the queued account emails; returns (to, subject, body) for each sent."""
    sent: list[tuple[str, str, str]] = []
    with patch(SEND, side_effect=lambda cfg, to, subject, body: sent.append((to, subject, body))):
        await jobs.run_due([account_emails.SEND], tenant_id=tenant_id)
    return sent


async def _queued(tenant_id) -> int:
    async with AsyncSessionLocal() as db:
        return (await db.execute(text("SELECT count(*) FROM jobs WHERE tenant_id = :t AND kind = :k AND status = 'queued'"), {"t": tenant_id, "k": account_emails.SEND})).scalar()


def test_templates_fill_in_their_placeholders():
    s = account_emails.EmailSettings()
    subject, body = account_emails.render(s, "password_reset", {"name": "Asha", "workspace": "Acme", "link": "https://x/y", "expires_in": "30 minutes"})
    assert subject == "Reset your Acme password"
    assert "Hello Asha," in body and "https://x/y" in body and "30 minutes" in body
    custom = account_emails.EmailSettings.model_validate(
        {"templates": {"password_changed": {"subject": "Changed: {{ name }}\n{{nope}}", "body": "Hi {{name}} {{unknown}}"}}}
    )
    subject, body = account_emails.render(custom, "password_changed", {"name": "Asha"})
    assert subject == "Changed: Asha {{nope}}"  # one line; unknown placeholders stay as written
    assert body == "Hi Asha {{unknown}}"


async def test_settings_round_trip(client, test_tenant, auth_headers):
    tid = test_tenant["tenant_id"]
    got = (await client.get("/tenants/me/email-settings", headers=auth_headers)).json()
    assert got["connector_name"] is None
    assert [t["key"] for t in got["templates"]] == ["password_reset", "password_changed", "registration_received"]
    assert all(t["enabled"] and t["custom_subject"] is None for t in got["templates"])

    connector_id = await _connector(tid)
    default_body = account_emails.TEMPLATES["password_changed"].body
    resp = await client.put(
        "/tenants/me/email-settings", headers=auth_headers,
        json={"connector_id": str(connector_id), "templates": {
            "registration_received": {"enabled": False},
            "password_changed": {"subject": "Heads up, {{name}}", "body": default_body},
        }},
    )
    assert resp.status_code == 200, resp.text
    templates = {t["key"]: t for t in resp.json()["templates"]}
    assert resp.json()["connector_name"] == "Pytest SMTP"
    assert templates["registration_received"]["enabled"] is False
    assert templates["password_changed"]["custom_subject"] == "Heads up, {{name}}"
    assert templates["password_changed"]["custom_body"] is None  # same as the default: kept as "default"

    # Only an email connector will do.
    async with AsyncSessionLocal() as db:
        async with db.begin():
            hook = Connector(tenant_id=tid, name="Hook", type="webhook", configuration={}, status="enabled")
            db.add(hook)
            await db.flush()
            hook_id = hook.id
    assert (await client.put("/tenants/me/email-settings", headers=auth_headers, json={"connector_id": str(hook_id)})).status_code == 422


async def test_forgot_password_sends_a_single_use_link(client, test_tenant):
    tid = test_tenant["tenant_id"]
    forgot = {"tenant_id": str(tid), "email": test_tenant["admin_email"]}

    # No way to send it: the sign-in page doesn't offer it, and nothing is queued.
    assert (await client.get(f"/auth/sso-config?tenant_id={tid}")).json()["password_reset"] is False
    resp = await client.post("/auth/password/forgot", json=forgot)
    assert resp.status_code == 200 and await _queued(tid) == 0

    await _connector(tid)
    assert (await client.get(f"/auth/sso-config?tenant_id={tid}")).json()["password_reset"] is True
    resp = await client.post("/auth/password/forgot", headers={"Origin": "https://app.example.com"}, json=forgot)
    stranger = await client.post("/auth/password/forgot", json={**forgot, "email": "nobody@example.com"})
    assert resp.json() == stranger.json()  # the same answer either way
    again = await client.post("/auth/password/forgot", json=forgot)
    assert again.status_code == 200 and await _queued(tid) == 1  # one per minute

    [(to, subject, body)] = await _deliver(tid)
    assert to == test_tenant["admin_email"] and subject.startswith("Reset your")
    link = re.search(r"https://app\.example\.com/reset-password\?\S+", body).group(0)
    token = parse_qs(urlparse(link).query)["token"][0]

    assert (await client.post("/auth/password/reset", json={"tenant_id": str(tid), "token": token, "new_password": "short"})).status_code == 422
    done = await client.post("/auth/password/reset", json={"tenant_id": str(tid), "token": token, "new_password": "reset-and-new-1"})
    assert done.status_code == 200, done.text
    login = {"tenant_id": str(tid), "email": test_tenant["admin_email"]}
    assert (await client.post("/auth/login", json={**login, "password": "reset-and-new-1"})).status_code == 200
    assert (await client.post("/auth/login", json={**login, "password": TEST_ADMIN_PASSWORD})).status_code == 401
    # Used once.
    reused = await client.post("/auth/password/reset", json={"tenant_id": str(tid), "token": token, "new_password": "another-one-22"})
    assert reused.status_code == 400

    [(_, subject, _)] = await _deliver(tid)
    assert subject.endswith("password was changed")


async def test_password_changes_and_registrations_send_their_notices(client, test_tenant, auth_headers):
    tid = test_tenant["tenant_id"]
    await _connector(tid)
    assert (await client.post("/auth/password", headers=auth_headers, json={"current_password": TEST_ADMIN_PASSWORD, "new_password": "my-own-new-pass"})).status_code == 204
    reg = await client.post(
        "/auth/register",
        json={"tenant_id": str(tid), "organization_name": "Acme Supplies", "name": "Vee", "email": "vee@example.com", "phone": "+15555550100", "password": "a-password-1"},
    )
    assert reg.status_code == 201, reg.text
    sent = await _deliver(tid)
    assert {(to, subject.split()[0]) for to, subject, _ in sent} == {(test_tenant["admin_email"], "Your"), ("vee@example.com", "Your")}
    registration = next(body for to, _, body in sent if to == "vee@example.com")
    assert "Acme Supplies" in registration

    # Switched off: not sent.
    await client.put("/tenants/me/email-settings", headers=auth_headers, json={"templates": {"password_changed": {"enabled": False}}})
    await client.post("/auth/password", headers=auth_headers, json={"current_password": "my-own-new-pass", "new_password": "and-again-new-1"})
    assert await _queued(tid) == 0


async def test_a_failed_send_is_retried_and_a_test_send_reports_back(client, test_tenant, auth_headers):
    tid = test_tenant["tenant_id"]
    resp = await client.post("/tenants/me/email-settings/test", headers=auth_headers, json={"template": "password_reset"})
    assert resp.json()["result"].startswith("Not sent — there's no enabled email connector")

    await _connector(tid)
    sent: list[str] = []
    with patch(SEND, side_effect=lambda cfg, to, subject, body: sent.append(subject)):
        draft = {"templates": {"password_reset": {"subject": "Draft: {{workspace}}"}}}
        resp = await client.post("/tenants/me/email-settings/test", headers=auth_headers, json={"template": "password_reset", "settings": draft})
    assert resp.json()["result"] == "sent" and sent[0].startswith("Draft: Pytest Tenant")

    await client.post("/auth/password", headers=auth_headers, json={"current_password": TEST_ADMIN_PASSWORD, "new_password": "my-own-new-pass"})
    with patch(SEND, side_effect=smtplib.SMTPServerDisconnected("connection closed")):
        await jobs.run_due([account_emails.SEND], tenant_id=tid)
    async with AsyncSessionLocal() as db:
        row = (await db.execute(text("SELECT status, attempts, last_error FROM jobs WHERE tenant_id = :t AND kind = :k"), {"t": tid, "k": account_emails.SEND})).one()
    assert row.status == "queued" and row.attempts == 1 and "connection closed" in row.last_error
