"""INDL-12 — AI biography writer.

The writer rewrites the staff member's own draft. Two things carry the risk and
are covered here: an empty draft must never reach the provider (there would be
nothing to rewrite, so anything produced would be invention), and provider
errors must not leak to the client.

The Azure client is always mocked — these tests make no network call.
"""
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

from src.apps.ai.services.biography_service import BiographyContext, draft_to_text
from src.apps.auth.models.user import User
from src.apps.tenants.models.account import Account
from src.core.security import build_token_payload, create_access_token, hash_password

URL = "/api/v1/ai/generate-biography"


async def _make_account(db: AsyncSession, *, plan: str = "professional") -> Account:
    uid = uuid4().hex[:8]
    acc = Account(
        organization_name=f"Bio Cemetery {uid}",
        subdomain=f"bio-{uid}",
        contact_email=f"admin-{uid}@bio.test",
        plan=plan,
        status="active",
    )
    db.add(acc)
    await db.flush()
    return acc


async def _make_token(db: AsyncSession, account: Account) -> str:
    user = User(
        tenant_id=account.id,
        email=f"staff-{uuid4().hex[:6]}@bio.test",
        password_hash=hash_password("Test1234!"),
        first_name="Staff",
        last_name="User",
        role="staff",
        status="active",
    )
    db.add(user)
    await db.flush()
    return create_access_token(build_token_payload(user, account))


def _headers(token: str, account: Account) -> dict:
    return {"Authorization": f"Bearer {token}", "X-Tenant-ID": str(account.id)}


def _mock_client(text: str) -> MagicMock:
    message = MagicMock()
    message.content = text
    choice = MagicMock()
    choice.message = message
    completion = MagicMock()
    completion.choices = [choice]
    client = MagicMock()
    client.chat.completions.create = AsyncMock(return_value=completion)
    return client


# ─────────────────────────────────────────────────────────────────────────────
# draft_to_text — the TipTap HTML the edit form sends
# ─────────────────────────────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_draft_to_text_flattens_tiptap_html():
    html = "<p>She was a <strong>midwife</strong>.</p><p>Loved roses.</p>"
    assert draft_to_text(html) == "She was a midwife.\n\nLoved roses."


@pytest.mark.asyncio
async def test_draft_to_text_treats_empty_tiptap_markup_as_blank():
    # TipTap serialises an untouched editor as this, not as "".
    assert draft_to_text("<p></p>") == ""
    assert draft_to_text("<p><br></p>") == ""
    assert draft_to_text("   ") == ""


@pytest.mark.asyncio
async def test_context_omits_details_the_record_does_not_have():
    lines = BiographyContext(display_name="Ann Kelly", occupation=None).as_lines()
    assert lines == ["Name: Ann Kelly"]
    assert not any("None" in line for line in lines)


# ─────────────────────────────────────────────────────────────────────────────
# Endpoint
# ─────────────────────────────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_blank_draft_is_rejected_without_calling_the_provider(
    client: AsyncClient, db_session: AsyncSession
):
    acc = await _make_account(db_session)
    token = await _make_token(db_session, acc)
    mock = _mock_client("should never be produced")

    with patch("src.core.config.settings.AZURE_OPENAI_API_KEY", "test-key"), \
         patch("src.core.config.settings.AZURE_OPENAI_ENDPOINT", "https://test.invalid"), \
         patch("openai.AsyncAzureOpenAI", return_value=mock):
        resp = await client.post(
            URL, headers=_headers(token, acc), json={"draft_text": "<p></p>"}
        )

    assert resp.status_code == 422
    assert "Your draft" in resp.json()["message"]
    mock.chat.completions.create.assert_not_called()


@pytest.mark.asyncio
async def test_the_draft_is_sent_to_the_model(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_token(db_session, acc)
    mock = _mock_client("Ann Kelly was a nurse for thirty years.")

    with patch("src.core.config.settings.AZURE_OPENAI_API_KEY", "test-key"), \
         patch("src.core.config.settings.AZURE_OPENAI_ENDPOINT", "https://test.invalid"), \
         patch("openai.AsyncAzureOpenAI", return_value=mock):
        resp = await client.post(
            URL,
            headers=_headers(token, acc),
            json={"draft_text": "<p>nurse for 30 yrs</p>", "first_name": "Ann", "last_name": "Kelly"},
        )

    assert resp.status_code == 200
    assert resp.json()["data"]["biography_text"] == "Ann Kelly was a nurse for thirty years."

    sent = mock.chat.completions.create.call_args.kwargs
    user_message = sent["messages"][1]["content"]
    assert "nurse for 30 yrs" in user_message   # the draft reached the prompt
    assert "Name: Ann Kelly" in user_message    # record context did too
    assert "<p>" not in user_message            # HTML was stripped


@pytest.mark.asyncio
async def test_provider_error_returns_502_without_leaking_detail(
    client: AsyncClient, db_session: AsyncSession
):
    import openai

    acc = await _make_account(db_session)
    token = await _make_token(db_session, acc)

    secret = "azure-internal-trace-with-api-key-abcdef123456"
    mock = MagicMock()
    mock.chat.completions.create = AsyncMock(
        side_effect=openai.APIStatusError(secret, response=MagicMock(status_code=500), body=None)
    )

    with patch("src.core.config.settings.AZURE_OPENAI_API_KEY", "test-key"), \
         patch("src.core.config.settings.AZURE_OPENAI_ENDPOINT", "https://test.invalid"), \
         patch("openai.AsyncAzureOpenAI", return_value=mock):
        resp = await client.post(
            URL, headers=_headers(token, acc), json={"draft_text": "some notes"}
        )

    assert resp.status_code == 502
    assert secret not in resp.text


@pytest.mark.asyncio
async def test_starter_plan_is_refused(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session, plan="starter")
    token = await _make_token(db_session, acc)

    resp = await client.post(URL, headers=_headers(token, acc), json={"draft_text": "notes"})

    assert resp.status_code == 403
