# backend/tests/test_phase16_mail.py — phase-16 실계정 메일(Gmail/Outlook OAuth + 멀티계정 + 발송)
import time

import pytest
from sqlmodel import select

from app.config import get_settings
from app.connectors import oauth
from app.connectors.base import RawRecord
from app.connectors.mail.normalize import mail_account_id_for, normalize_email
from app.connectors.mail.outbound import send_outbound
from app.connectors.mail.real_outlook import OutlookConnector
from app.connectors.registry import ConnectorRegistry, _impl
from app.crypto import encrypt_token
from app.models import (
    ConnectorAccount,
    ConnectorDomain,
    ConnectorMode,
    ConnState,
    MailAccount,
    OAuthState,
    OutboundMail,
    Sent,
)


class _FakeResp:
    def __init__(self, status, payload=None):
        self.status_code = status
        self._payload = payload or {}

    def json(self):
        return self._payload

    def raise_for_status(self):
        if self.status_code >= 400:
            raise RuntimeError(f"http {self.status_code}")


@pytest.fixture()
def oauth_creds(monkeypatch):
    monkeypatch.setenv("GOOGLE_CLIENT_ID", "g-cid")
    monkeypatch.setenv("GOOGLE_CLIENT_SECRET", "g-sec")
    monkeypatch.setenv("MICROSOFT_CLIENT_ID", "m-cid")
    monkeypatch.setenv("MICROSOFT_CLIENT_SECRET", "m-sec")
    get_settings.cache_clear()
    yield
    get_settings.cache_clear()


def _mock_token_and_identity(monkeypatch, *, email, token=None):
    tok = token or {"access_token": "AT", "refresh_token": "RT", "expires_in": 3600, "scope": "s"}
    monkeypatch.setattr(oauth.httpx, "post", lambda *a, **k: _FakeResp(200, tok))
    # google: gmail/v1/users/me/profile → emailAddress ; microsoft: graph /me → mail
    monkeypatch.setattr(
        oauth.httpx,
        "get",
        lambda *a, **k: _FakeResp(200, {"emailAddress": email, "mail": email}),
    )


# ── OAuth: Outlook provider + tenant URL ──
def test_outlook_authorize_url_uses_microsoft_tenant(session, oauth_creds):
    s, _ = session
    url = oauth.start_oauth(s, "mail", "outlook", redirect_after="/settings?tab=mail")
    assert url.startswith("https://login.microsoftonline.com/common/oauth2/v2.0/authorize")
    assert "code_challenge=" in url and "state=" in url  # PKCE 보존
    assert "Mail.Read" in url and "Mail.Send" in url and "offline_access" in url


# ── 멀티계정: 이메일별 고유 계정 id + MailAccount upsert ──
def test_finish_oauth_creates_unique_account_per_email(session, oauth_creds, monkeypatch):
    s, _ = session

    def connect(provider, email):
        url = oauth.start_oauth(s, "mail", provider)
        state = url.split("state=")[1].split("&")[0]
        _mock_token_and_identity(monkeypatch, email=email)
        return oauth.finish_oauth(s, code="C", state=state)

    a1 = connect("gmail", "alice@gmail.com")
    a2 = connect("gmail", "bob@gmail.com")
    a3 = connect("outlook", "carol@outlook.com")

    # 같은 provider 여도 이메일이 다르면 계정 id 가 갈린다(덮어쓰기 없음)
    assert a1.id != a2.id
    assert a1.external_account_id == "alice@gmail.com"
    assert a3.provider == "outlook" and a3.mode == ConnectorMode.real
    # 메일 페이지 노출용 MailAccount 도 함께 생성
    assert s.get(MailAccount, mail_account_id_for("alice@gmail.com")) is not None
    assert s.get(MailAccount, mail_account_id_for("carol@outlook.com")) is not None
    # state row 소거
    assert s.get(OAuthState, "x") is None


# ── normalize: Graph(Outlook) 메시지 → 내부 Email 필드 ──
def test_normalize_outlook_golden(session):
    acct = ConnectorAccount(
        id="ca-mail-outlook-xyz",
        domain=ConnectorDomain.mail,
        provider="outlook",
        external_account_id="me@outlook.com",
    )
    payload = {
        "id": "AAMk-1",
        "subject": "분기 리포트 검토 부탁",
        "bodyPreview": "안녕하세요, 리포트 초안입니다.",
        "body": {"contentType": "text", "content": "안녕하세요,\n리포트 초안입니다.\n확인 부탁드려요."},
        "from": {"emailAddress": {"name": "현우", "address": "hyunwoo@corp.com"}},
        "toRecipients": [{"emailAddress": {"address": "me@outlook.com"}}],
        "isRead": False,
        "flag": {"flagStatus": "flagged"},
        "hasAttachments": True,
        "categories": ["업무"],
        "receivedDateTime": "2026-06-15T09:00:00Z",
    }
    raw = RawRecord(external_id="AAMk-1", payload=payload, etag="W/1")
    norm = normalize_email(acct, raw, provider="outlook")
    f = norm.fields
    assert f["account"] == mail_account_id_for("me@outlook.com")
    # 발신자 표시 이름 보존('이름 <메일>') — 목록은 이름만, 리더는 이름+주소 노출.
    assert f["from_key"] == "현우 <hyunwoo@corp.com>"
    assert f["subject"] == "분기 리포트 검토 부탁"
    assert f["read"] is False and f["starred"] is True and f["has_attach"] is True
    assert "리포트 초안입니다." in " ".join(f["body"])
    assert f["labels"] == ["업무"]


def test_normalize_outlook_html_body_stripped(session):
    acct = ConnectorAccount(id="ca-o2", domain=ConnectorDomain.mail, provider="outlook")
    payload = {
        "id": "h1",
        "subject": "공지",
        "bodyPreview": "프리뷰",
        "body": {"contentType": "html", "content": "<div><p>안녕하세요</p><p>좋은 하루</p></div>"},
        "from": {"emailAddress": {"address": "x@y.com"}},
        "isRead": True,
    }
    norm = normalize_email(acct, RawRecord(external_id="h1", payload=payload), provider="outlook")
    # 태그 제거 + 블록(<p>) 경계마다 줄바꿈 → 읽기 좋은 다중 줄.
    assert norm.fields["body"] == ["안녕하세요", "좋은 하루"]


# ── registry: provider 로 Gmail/Outlook 분기 ──
def test_connector_dispatch_by_provider(session):
    s, _ = session
    from app.connectors.mail.real_gmail import GmailConnector

    assert _impl("mail", "real", "outlook").__name__ == "OutlookConnector"
    assert _impl("mail", "real", "gmail").__name__ == "GmailConnector"
    out_acct = ConnectorAccount(
        id="ca-o", domain=ConnectorDomain.mail, provider="outlook", mode=ConnectorMode.real
    )
    gm_acct = ConnectorAccount(
        id="ca-g", domain=ConnectorDomain.mail, provider="gmail", mode=ConnectorMode.real
    )
    assert isinstance(ConnectorRegistry.get(s, out_acct), OutlookConnector)
    assert isinstance(ConnectorRegistry.get(s, gm_acct), GmailConnector)


# ── 발송: real 연결 계정 → connector.send_mail 호출 ──
def test_send_outbound_real_calls_connector(session, monkeypatch):
    s, _ = session
    acct = ConnectorAccount(
        id="ca-mail-gmail-abc",
        domain=ConnectorDomain.mail,
        provider="gmail",
        mode=ConnectorMode.real,
        state=ConnState.connected,
        external_account_id="me@gmail.com",
        token_enc=encrypt_token({"access_token": "AT", "expires_at": int(time.time()) + 9999}),
    )
    s.add(acct)
    ob = OutboundMail(
        id="ob-1",
        approval_id="ap-1",
        connector_account_id=acct.id,
        mail_account=mail_account_id_for("me@gmail.com"),
        to="친구 <pal@x.com>",
        subject="안녕",
        body="잘 지내?",
        status="pending",
    )
    s.add(ob)
    s.commit()

    calls = {}
    from app.connectors.mail.real_gmail import GmailConnector

    def fake_send(self, session, *, to, subject, body, cc="", bcc="", attachments=None):
        calls.update(to=to, subject=subject, body=body)
        return "sent"

    monkeypatch.setattr(GmailConnector, "send_mail", fake_send)
    out = send_outbound(s, ob)
    assert out.status == "sent" and out.sent_at is not None
    assert calls == {"to": "친구 <pal@x.com>", "subject": "안녕", "body": "잘 지내?"}


# ── 발송: 미연결 → failed(가짜 발송 없음, phase-16+) ──
def test_send_outbound_unconnected_fails(session):
    s, _ = session
    ob = OutboundMail(
        id="ob-2",
        approval_id="ap-2",
        connector_account_id=None,
        mail_account="work",
        to="대표님",
        subject="보고",
        body="첨부 확인 부탁드립니다.",
        status="pending",
    )
    s.add(ob)
    s.commit()
    out = send_outbound(s, ob)
    assert out.status == "failed"  # 가짜 Sent 행 생성 없음
    assert s.exec(select(Sent)).all() == []


# ── 발송 실패는 failed 로 격리(서버 안 죽음) ──
def test_send_outbound_failure_isolated(session, monkeypatch):
    s, _ = session
    acct = ConnectorAccount(
        id="ca-fail",
        domain=ConnectorDomain.mail,
        provider="gmail",
        mode=ConnectorMode.real,
        state=ConnState.connected,
        external_account_id="me@gmail.com",
        token_enc=encrypt_token({"access_token": "AT", "expires_at": int(time.time()) + 9999}),
    )
    s.add(acct)
    ob = OutboundMail(
        id="ob-3", approval_id="ap-3", connector_account_id=acct.id, mail_account="x",
        to="a@b.com", subject="s", body="b", status="pending",
    )
    s.add(ob)
    s.commit()
    from app.connectors.mail.real_gmail import GmailConnector

    def boom(self, session, **k):
        raise RuntimeError("network down")

    monkeypatch.setattr(GmailConnector, "send_mail", boom)
    out = send_outbound(s, ob)
    assert out.status == "failed" and "network down" in out.error_detail


# ── API: /connectors/providers 구성 여부 ──
def test_providers_endpoint_unconfigured(client):
    rows = client.get("/api/connectors/providers?domain=mail").json()
    by = {r["provider"]: r for r in rows}
    assert set(by) == {"gmail", "outlook"}
    assert by["gmail"]["configured"] is False and by["outlook"]["configured"] is False


def test_providers_endpoint_configured(client, oauth_creds):
    rows = client.get("/api/connectors/providers").json()
    by = {r["provider"]: r for r in rows}
    assert by["gmail"]["configured"] is True and by["outlook"]["configured"] is True


# ── API: /mail/send (연결된 계정) → 결재 + OutboundMail(pending) 생성 ──
def test_mail_send_creates_pending_outbound(client, session):
    s, _ = session
    acct = ConnectorAccount(
        id="ca-mail-gmail-x", domain=ConnectorDomain.mail, provider="gmail",
        mode=ConnectorMode.real, state=ConnState.connected, external_account_id="me@gmail.com",
    )
    s.add(acct)
    s.commit()
    slug = mail_account_id_for("me@gmail.com")
    r = client.post(
        "/api/mail/send",
        json={"from_account": slug, "to": "대표님", "subject": "보고", "body": "확인 부탁"},
    )
    assert r.status_code == 200
    aid = r.json()["approval_id"]
    s.expire_all()
    ob = s.exec(select(OutboundMail).where(OutboundMail.approval_id == aid)).first()
    assert ob is not None and ob.status == "pending"
    assert ob.connector_account_id == "ca-mail-gmail-x"  # 연결된 real 계정에 묶임
    assert ob.subject == "보고"


def test_mail_send_unconnected_rejected(client, session):
    # 연결된 계정 없으면 400(가짜 발송 없음)
    r = client.post(
        "/api/mail/send",
        json={"from_account": "work", "to": "대표님", "subject": "보고", "body": "x"},
    )
    assert r.status_code == 400


# ── 시작 검증: 미구성 outlook 은 400 ──
def test_oauth_start_outlook_requires_microsoft_client_id(client):
    r = client.get("/api/connectors/oauth/start?domain=mail&provider=outlook")
    assert r.status_code == 400
