"""
Tests for POST /api/v1/sales/opportunities/{id}/mark-lost (INDL-26).
Covers T-08 through T-12, T-15, T-18 from the PRD test matrix.
"""
import pytest
from datetime import datetime, timezone
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.apps.sales.models.opportunity import Opportunity


OPPS_URL = "/api/v1/sales/opportunities"


def _tenant_headers(token: str, tenant_id: str):
    return {
        "Authorization": f"Bearer {token}",
        "X-Tenant-ID": tenant_id,
    }


async def _create_opportunity(client, token, tenant_id):
    resp = await client.post(
        OPPS_URL,
        json={
            "family_name": "Lost Family",
            "care_type": "pre_need",
            "contact_email": "lost@example.com",
            "estimated_value": 4800,
        },
        headers=_tenant_headers(token, tenant_id),
    )
    assert resp.status_code == 201
    return resp.json()["data"]["id"]


# ── T-08: Happy path — valid loss_reason from proposal_sent ──────────────────

@pytest.mark.asyncio
async def test_mark_lost_happy_path(
    client: AsyncClient,
    db_session: AsyncSession,
    admin_token: str,
    test_account,
):
    """T-08: POST /mark-lost with valid reason from proposal_sent → 200; all loss fields set."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id))

    # Advance to proposal_sent
    for stage in ["site_visit", "proposal_sent"]:
        await client.patch(
            f"{OPPS_URL}/{opp_id}/stage",
            json={"stage": stage},
            headers=_tenant_headers(admin_token, str(test_account.id)),
        )

    resp = await client.post(
        f"{OPPS_URL}/{opp_id}/mark-lost",
        json={"loss_reason": "competitor", "loss_notes": "Client chose another provider."},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 200
    data = resp.json()["data"]
    assert data["stage"] == "lost"
    assert data["lost_reason"] == "competitor"
    assert data["lost_from_stage"] == "proposal_sent"
    assert data["lost_at"] is not None
    assert data["lost_notes"] == "Client chose another provider."

    # Verify in DB
    result = await db_session.execute(select(Opportunity).where(Opportunity.id == opp_id))
    opp = result.scalar_one()
    assert opp.stage == "lost"
    assert opp.lost_reason == "competitor"
    assert opp.lost_from_stage == "proposal_sent"
    assert opp.lost_at is not None


# ── T-09: Missing loss_reason → 422 ──────────────────────────────────────────

@pytest.mark.asyncio
async def test_mark_lost_missing_reason(
    client: AsyncClient, admin_token: str, test_account
):
    """T-09: POST /mark-lost without loss_reason → 422."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id))
    resp = await client.post(
        f"{OPPS_URL}/{opp_id}/mark-lost",
        json={"loss_notes": "No reason given."},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 422


# ── T-10: Invalid loss_reason value → 422 ────────────────────────────────────

@pytest.mark.asyncio
async def test_mark_lost_invalid_reason(
    client: AsyncClient, admin_token: str, test_account
):
    """T-10: POST /mark-lost with invalid loss_reason → 422; enum validation error."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id))
    resp = await client.post(
        f"{OPPS_URL}/{opp_id}/mark-lost",
        json={"loss_reason": "bad_value"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 422


# ── T-11: Already lost → 422 ─────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_mark_lost_already_lost(
    client: AsyncClient, admin_token: str, test_account
):
    """T-11: POST /mark-lost on an already-lost opportunity → 422."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id))
    # First mark as lost
    await client.post(
        f"{OPPS_URL}/{opp_id}/mark-lost",
        json={"loss_reason": "price"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    # Second attempt
    resp = await client.post(
        f"{OPPS_URL}/{opp_id}/mark-lost",
        json={"loss_reason": "timing"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 422
    assert "already" in resp.json()["message"].lower()


# ── T-12: view_only → 403 ────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_mark_lost_view_only_forbidden(
    client: AsyncClient, db_session: AsyncSession, admin_token: str, test_account
):
    """T-12: POST /mark-lost by view_only → 403."""
    from src.apps.auth.models.user import User
    from src.core.security import hash_password, create_access_token, build_token_payload

    opp_id = await _create_opportunity(client, admin_token, str(test_account.id))

    view_user = User(
        tenant_id=test_account.id,
        email="view.lost@testcemetery.com",
        password_hash=hash_password("Test1234"),
        first_name="View",
        last_name="Lost",
        role="view_only",
        status="active",
    )
    db_session.add(view_user)
    await db_session.flush()
    token = create_access_token(build_token_payload(view_user, test_account))

    resp = await client.post(
        f"{OPPS_URL}/{opp_id}/mark-lost",
        json={"loss_reason": "price"},
        headers=_tenant_headers(token, str(test_account.id)),
    )
    assert resp.status_code == 403


# ── T-15: lost_from_stage captures correct stage ─────────────────────────────

@pytest.mark.asyncio
async def test_mark_lost_captures_from_stage(
    client: AsyncClient,
    db_session: AsyncSession,
    admin_token: str,
    test_account,
):
    """T-15: Mark from contract_sent; assert lost_from_stage = 'contract_sent' in DB."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id))

    for stage in ["site_visit", "proposal_sent", "proposal_accepted", "contract_sent"]:
        await client.patch(
            f"{OPPS_URL}/{opp_id}/stage",
            json={"stage": stage},
            headers=_tenant_headers(admin_token, str(test_account.id)),
        )

    resp = await client.post(
        f"{OPPS_URL}/{opp_id}/mark-lost",
        json={"loss_reason": "unresponsive"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 200
    assert resp.json()["data"]["lost_from_stage"] == "contract_sent"

    result = await db_session.execute(select(Opportunity).where(Opportunity.id == opp_id))
    opp = result.scalar_one()
    assert opp.lost_from_stage == "contract_sent"


# ── T-18: loss_notes > 2000 chars → 422 ──────────────────────────────────────

@pytest.mark.asyncio
async def test_mark_lost_notes_too_long(
    client: AsyncClient, admin_token: str, test_account
):
    """T-18: POST /mark-lost with loss_notes > 2000 chars → 422."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id))
    resp = await client.post(
        f"{OPPS_URL}/{opp_id}/mark-lost",
        json={"loss_reason": "other", "loss_notes": "x" * 2001},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 422


# ── All 5 valid loss reasons accepted ────────────────────────────────────────

@pytest.mark.asyncio
@pytest.mark.parametrize("reason", ["price", "timing", "competitor", "unresponsive", "other"])
async def test_mark_lost_all_valid_reasons(
    client: AsyncClient, admin_token: str, test_account, reason: str
):
    """All 5 valid loss_reason values are accepted."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id))
    resp = await client.post(
        f"{OPPS_URL}/{opp_id}/mark-lost",
        json={"loss_reason": reason},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 200
    assert resp.json()["data"]["lost_reason"] == reason


# ── GET opportunities includes all loss fields for lost opportunities ──────────

@pytest.mark.asyncio
async def test_get_opportunity_loss_fields(
    client: AsyncClient, admin_token: str, test_account
):
    """T-13 variant: GET opportunity returns all 4 loss fields populated after mark-lost."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id))
    await client.post(
        f"{OPPS_URL}/{opp_id}/mark-lost",
        json={"loss_reason": "price", "loss_notes": "Budget too tight."},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    get_resp = await client.get(
        f"{OPPS_URL}/{opp_id}",
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert get_resp.status_code == 200
    data = get_resp.json()["data"]
    assert data["lost_reason"] == "price"
    assert data["lost_notes"] == "Budget too tight."
    assert data["lost_at"] is not None
    assert data["lost_from_stage"] == "inquiry"
