        if not template or template.user_id != user.id:
            raise HTTPException(status_code=404, detail="Template not found")
    body_html = template.body_html if template else payload.body_html
    body_text = template.body_text if template else payload.body_text
    if not body_html and not body_text:
        raise HTTPException(status_code=400, detail="Provide a template or message body")

    # plain dict recipient for variable substitution (test only). VariableEngine
    # accepts dicts directly; ORM-like objects would need __table__ introspection.
    fr = {
        "email": str(payload.to_email),
        "first_name": "Test",
        "last_name": "Recipient",
        "company_name": await get_setting(db, "company_name", "Acme Corp") or "Acme Corp",
        "company_phone": "555-0100",
        "customer_email": str(payload.to_email),
        "custom_fields": {},
    }

    engine = VariableEngine({})
    g = GeneratedValues()
    if payload.use_random_variables:
        g.invoice_number = engine.generate_invoice_number(set())
        g.amount = engine.generate_amount()
        g.shipping = engine.generate_shipping()
        g.invoice_date = "2025-01-15"
        g.due_date = "2025-01-30"
    unsub_url = f"{cfg.BASE_URL}/unsubscribe/test-token"
    res_html = engine.substitute(body_html or "", fr, g, custom_fields={},
                                 extra={"unsubscribe_link": unsub_url,
                                        "company_phone": fr["company_phone"]}).text
    res_text = engine.substitute(body_text or "", fr, g, custom_fields={},
                                 extra={"unsubscribe_link": unsub_url,
                                        "company_phone": fr["company_phone"]}).text
    subject = engine.substitute(payload.subject, fr, g).text

    # Validate AFTER substitution: bodies legitimately contain %Variables% that
    # are substituted per recipient above. Anything still remaining is a genuine
    # problem (unknown variable, structural issue) and is rejected here.
    problems = message_builder.validate_html(res_html) if res_html else []
    problems = [p for p in problems if not p.startswith("Unsubstituted variables")]
    if problems:
        raise HTTPException(status_code=400,
                            detail=f"HTML rejected: {'; '.join(problems)}")

    attachments = []
    if payload.include_pdf and body_html:
        pdf = await asyncio.get_event_loop().run_in_executor(
            None, message_builder.html_to_pdf, res_html)
        if pdf:
            attachments.append({"filename": "Test_Preview.pdf", "content": pdf,
                                "mime_type": "application/pdf"})

    mime = message_builder.build_mime_message(
        from_email=account.email,
        to_email=str(payload.to_email),
        subject=subject,
        html_body=res_html,
        text_body=res_text,
        attachments=attachments,
        unsubscribe_url=unsub_url,
    )

    # REAL send in a worker thread with a sync DB session (token refresh writes)
    def _real_send():
        with SyncSession() as s:
            acc = s.get(GmailAccount, account.id)
            token = s.query(OAuthToken).filter_by(account_id=account.id).first()
            if not token:
                return {"ok": False, "error": "No OAuth token stored for this account",
                        "error_class": "auth"}
            client = build_gmail_client_from_account(acc, token)
            if client.method == "smtp":
                return client.send_mime_smtp(mime)
            return client.send_mime_api(mime)

    try:
        result = await asyncio.get_event_loop().run_in_executor(None, _real_send)
    except Exception as e:
        await log_audit(db, user.id, "test_send.error", "gmail_account", account.id,
                        {"error": str(e)[:200]})
        return {"ok": False, "stage": "temp_failed", "detail": str(e)[:400],
                "error_class": "unknown", "elapsed_ms": int((time.monotonic() - t0) * 1000)}

    elapsed = int((time.monotonic() - t0) * 1000)
    if result.get("ok"):
        await log_audit(db, user.id, "test_send.success", "gmail_account", account.id,
                        {"to": str(payload.to_email), "message_id": result.get("message_id")})
        return {"ok": True, "stage": "accepted", "detail": "Gmail accepted the message",
                "message_id": result.get("message_id"),
                "thread_id": result.get("thread_id"),
                "method": client_method(account),
                "provider_response": _clip(str(result.get("raw_response", ""))),
                "elapsed_ms": elapsed}
    # provider refused — surface the REAL error, never fake success
    err = str(result.get("error", "") or result.get("detail", ""))
    await log_audit(db, user.id, "test_send.failed", "gmail_account", account.id,
                    {"to": str(payload.to_email), "error": err[:200]})
    return {"ok": False, "stage": "perm_failed", "detail": err[:400],
            "error_class": result.get("error_class", "unknown"),
            "method": client_method(account),
            "provider_response": _clip(str(result.get("raw_response", ""))),
            "elapsed_ms": elapsed}


def _clip(s: str) -> str:
    return s[:300]


def client_method(account: GmailAccount) -> str:
    return account.sending_method or "api"


@router.get("/accounts")
async def test_accounts(db: AsyncSession = Depends(get_db),
                        user: User = Depends(get_current_user)):
