"""
Tests for PATCH /api/v1/sales/opportunities/{id}/stage (INDL-26).
Covers T-01 through T-07 and T-13/14/16/17 from the PRD test matrix.
"""
import pytest
from httpx import AsyncClient
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, stage="inquiry"):
    """Helper: create an opportunity and return its id."""
    resp = await client.post(
        OPPS_URL,
        json={
            "family_name": "Test Family",
            "care_type": "pre_need",
            "contact_email": "test@example.com",
            "estimated_value": 4800,
        },
        headers=_tenant_headers(token, tenant_id),
    )
    assert resp.status_code == 201
    opp_id = resp.json()["data"]["id"]

    if stage != "inquiry":
        # Advance through stages as needed
        stage_path = [
            "inquiry", "site_visit", "proposal_sent", "proposal_accepted",
            "contract_sent", "contract_signed", "partial_payment_received", "fully_paid",
        ]
        idx = stage_path.index(stage)
        for i in range(idx):
            next_stage = stage_path[i + 1]
            patch_resp = await client.patch(
                f"{OPPS_URL}/{opp_id}/stage",
                json={"stage": next_stage},
                headers=_tenant_headers(token, tenant_id),
            )
            assert patch_resp.status_code == 200, f"Failed advancing to {next_stage}: {patch_resp.json()}"

    return opp_id


# ── T-01: Valid transition inquiry → site_visit ───────────────────────────────

@pytest.mark.asyncio
async def test_stage_transition_inquiry_to_site_visit(
    client: AsyncClient, admin_token: str, test_account
):
    """T-01: PATCH /stage site_visit from inquiry → 200; stage updated."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id))
    resp = await client.patch(
        f"{OPPS_URL}/{opp_id}/stage",
        json={"stage": "site_visit"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 200
    assert resp.json()["data"]["stage"] == "site_visit"


# ── T-02: proposal_sent → proposal_accepted ───────────────────────────────────

@pytest.mark.asyncio
async def test_stage_transition_proposal_sent_to_accepted(
    client: AsyncClient, admin_token: str, test_account
):
    """T-02: PATCH /stage proposal_accepted from proposal_sent → 200."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id), stage="proposal_sent")
    resp = await client.patch(
        f"{OPPS_URL}/{opp_id}/stage",
        json={"stage": "proposal_accepted"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 200
    assert resp.json()["data"]["stage"] == "proposal_accepted"


# ── T-03: proposal_accepted → contract_sent ───────────────────────────────────

@pytest.mark.asyncio
async def test_stage_transition_proposal_accepted_to_contract_sent(
    client: AsyncClient, admin_token: str, test_account
):
    """T-03: PATCH /stage contract_sent from proposal_accepted → 200."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id), stage="proposal_accepted")
    resp = await client.patch(
        f"{OPPS_URL}/{opp_id}/stage",
        json={"stage": "contract_sent"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 200
    assert resp.json()["data"]["stage"] == "contract_sent"


# ── T-04: Invalid jump → 422 ─────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_stage_transition_invalid_jump(
    client: AsyncClient, admin_token: str, test_account
):
    """T-04: PATCH with jump from inquiry to fully_paid → 422; error names allowed stages."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id))
    resp = await client.patch(
        f"{OPPS_URL}/{opp_id}/stage",
        json={"stage": "fully_paid"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 422
    detail = resp.json()["message"]
    assert "fully_paid" in detail


# ── T-05: Terminal stage fully_paid → 422 ────────────────────────────────────

@pytest.mark.asyncio
async def test_stage_transition_from_fully_paid_terminal(
    client: AsyncClient, admin_token: str, test_account
):
    """T-05: PATCH /stage on fully_paid opportunity → 422; terminal stage message."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id), stage="fully_paid")
    resp = await client.patch(
        f"{OPPS_URL}/{opp_id}/stage",
        json={"stage": "contract_signed"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 422
    assert "terminal" in resp.json()["message"].lower()


# ── T-06: Terminal stage lost → 422 ──────────────────────────────────────────

@pytest.mark.asyncio
async def test_stage_transition_from_lost_terminal(
    client: AsyncClient, admin_token: str, test_account
):
    """T-06: PATCH /stage on a lost opportunity → 422; terminal stage message."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id))
    # Mark as lost first
    await client.post(
        f"{OPPS_URL}/{opp_id}/mark-lost",
        json={"loss_reason": "price"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    resp = await client.patch(
        f"{OPPS_URL}/{opp_id}/stage",
        json={"stage": "site_visit"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 422
    assert "terminal" in resp.json()["message"].lower()


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

@pytest.mark.asyncio
async def test_stage_transition_view_only_forbidden(
    client: AsyncClient, db_session: AsyncSession, admin_token: str, test_account
):
    """T-07: PATCH /stage 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.stage@testcemetery.com",
        password_hash=hash_password("Test1234"),
        first_name="View",
        last_name="Stage",
        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.patch(
        f"{OPPS_URL}/{opp_id}/stage",
        json={"stage": "site_visit"},
        headers=_tenant_headers(token, str(test_account.id)),
    )
    assert resp.status_code == 403


# ── T-13/14: GET with stage=lost filter ──────────────────────────────────────

@pytest.mark.asyncio
async def test_list_opportunities_stage_filter(
    client: AsyncClient, admin_token: str, test_account
):
    """T-13/14: GET /opportunities?stage=lost returns only lost opportunities."""
    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": "timing"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    resp = await client.get(
        f"{OPPS_URL}?stage=lost",
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 200
    items = resp.json()["data"]
    assert all(i["stage"] == "lost" for i in items)


# ── Lost blocked once contract is signed (dedicated /stage endpoint) ─────────

@pytest.mark.asyncio
async def test_mark_lost_blocked_after_contract_signed(
    client: AsyncClient, admin_token: str, test_account
):
    """Mark-lost from contract_signed → 422; deal is won, cannot be marked lost."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id), stage="contract_signed")
    resp = await client.post(
        f"{OPPS_URL}/{opp_id}/mark-lost",
        json={"loss_reason": "price"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 422
    assert "contract" in resp.json()["message"].lower()


@pytest.mark.asyncio
async def test_stage_transition_to_lost_blocked_after_contract_signed(
    client: AsyncClient, admin_token: str, test_account
):
    """PATCH /stage lost from contract_signed → 422; 'lost' no longer in allowed next stages."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id), stage="contract_signed")
    resp = await client.patch(
        f"{OPPS_URL}/{opp_id}/stage",
        json={"stage": "lost"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_mark_lost_allowed_before_contract_signed(
    client: AsyncClient, admin_token: str, test_account
):
    """Mark-lost from contract_sent (before signing) → 200; still eligible."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id), stage="contract_sent")
    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 == 200
    assert resp.json()["data"]["stage"] == "lost"


# ── Generic PATCH /opportunities/{id} enforces the same stage rules ──────────

@pytest.mark.asyncio
async def test_generic_update_blocks_backward_stage_move(
    client: AsyncClient, admin_token: str, test_account
):
    """PATCH /opportunities/{id} with an earlier stage than current → 422 (forward-only)."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id), stage="proposal_sent")
    resp = await client.patch(
        f"{OPPS_URL}/{opp_id}",
        json={"stage": "site_visit"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 422
    assert "back" in resp.json()["message"].lower()


@pytest.mark.asyncio
async def test_generic_update_allows_forward_stage_move(
    client: AsyncClient, admin_token: str, test_account
):
    """PATCH /opportunities/{id} moving forward (even skipping stages) → 200."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id), stage="site_visit")
    resp = await client.patch(
        f"{OPPS_URL}/{opp_id}",
        json={"stage": "proposal_accepted"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 200
    assert resp.json()["data"]["stage"] == "proposal_accepted"


@pytest.mark.asyncio
async def test_generic_update_blocks_lost_after_contract_signed(
    client: AsyncClient, admin_token: str, test_account
):
    """PATCH /opportunities/{id} with stage=lost from contract_signed → 422."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id), stage="contract_signed")
    resp = await client.patch(
        f"{OPPS_URL}/{opp_id}",
        json={"stage": "lost"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 422
    assert "contract" in resp.json()["message"].lower()


@pytest.mark.asyncio
async def test_generic_update_allows_lost_before_contract_signed(
    client: AsyncClient, admin_token: str, test_account
):
    """PATCH /opportunities/{id} with stage=lost before contract signature → 200."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id), stage="proposal_sent")
    resp = await client.patch(
        f"{OPPS_URL}/{opp_id}",
        json={"stage": "lost"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 200
    assert resp.json()["data"]["stage"] == "lost"


@pytest.mark.asyncio
async def test_generic_update_blocks_change_from_terminal_stage(
    client: AsyncClient, admin_token: str, test_account
):
    """PATCH /opportunities/{id} on a fully_paid (terminal) opportunity → 422."""
    opp_id = await _create_opportunity(client, admin_token, str(test_account.id), stage="fully_paid")
    resp = await client.patch(
        f"{OPPS_URL}/{opp_id}",
        json={"stage": "contract_signed"},
        headers=_tenant_headers(admin_token, str(test_account.id)),
    )
    assert resp.status_code == 422
    assert "terminal" in resp.json()["message"].lower()


# ── T-17: Cross-tenant isolation ─────────────────────────────────────────────

@pytest.mark.asyncio
async def test_stage_transition_cross_tenant_404(
    client: AsyncClient, db_session: AsyncSession, test_account
):
    """T-17: Staff from tenant A cannot transition opportunity belonging to tenant B → 404."""
    from src.apps.tenants.models.account import Account
    from src.apps.auth.models.user import User
    from src.core.security import hash_password, create_access_token, build_token_payload
    from src.apps.sales.models.opportunity import Opportunity

    # Create tenant B
    tenant_b = Account(
        organization_name="Cemetery B",
        subdomain="cemb",
        contact_email="admin@cemb.com",
        plan="starter",
        status="active",
    )
    db_session.add(tenant_b)
    await db_session.flush()

    # Create an opportunity for tenant B directly in DB
    opp_b = Opportunity(
        tenant_id=tenant_b.id,
        family_name="B Family",
        stage="inquiry",
    )
    db_session.add(opp_b)
    await db_session.flush()

    # Create admin for tenant A (test_account)
    admin_a = User(
        tenant_id=test_account.id,
        email="admin.a.stage@testcemetery.com",
        password_hash=hash_password("Test1234"),
        first_name="Admin",
        last_name="A",
        role="administrator",
        status="active",
    )
    db_session.add(admin_a)
    await db_session.flush()

    token_a = create_access_token(build_token_payload(admin_a, test_account))

    # Tenant A admin tries to transition tenant B's opportunity
    resp = await client.patch(
        f"{OPPS_URL}/{opp_b.id}/stage",
        json={"stage": "site_visit"},
        headers=_tenant_headers(token_a, str(test_account.id)),
    )
    assert resp.status_code == 404
