"""
Backend tests for Contract Wizard Phase 2 — INDL-28.
Covers T-01 through T-16 from the PRD test matrix.
"""
from __future__ import annotations

from decimal import Decimal

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

from src.apps.billing.models.invoice import Invoice
from src.apps.sales.models.contract import Contract
from src.apps.sales.models.opportunity import Opportunity

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


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


# ── Helpers ───────────────────────────────────────────────────────────────────

async def _create_opportunity(client: AsyncClient, token: str, tenant_id) -> str:
    resp = await client.post(
        OPPS_URL,
        json={"family_name": "McLeod Family", "care_type": "pre_need", "estimated_value": 5000},
        headers=_headers(token, tenant_id),
    )
    assert resp.status_code == 201, resp.text
    return resp.json()["data"]["id"]


async def _create_contract(
    client: AsyncClient,
    token: str,
    tenant_id,
    *,
    payment_plan_type: str = "full",
    purchaser_email: str | None = "buyer@example.ca",
    opportunity_id: str | None = None,
    total_amount: str = "3000.00",
) -> dict:
    body = {
        "contract_type": "pre_need",
        "purchaser_name": "Mary McLeod",
        "payment_plan_type": payment_plan_type,
        "line_items": [
            {"description": "Burial plot A-12", "quantity": 1, "unit_price": total_amount},
        ],
    }
    if purchaser_email:
        body["purchaser_email"] = purchaser_email
    if opportunity_id:
        body["opportunity_id"] = opportunity_id
    resp = await client.post(CONTRACTS_URL, json=body, headers=_headers(token, tenant_id))
    assert resp.status_code == 201, resp.text
    return resp.json()["data"]


async def _sign_contract(
    client: AsyncClient,
    token: str,
    tenant_id,
    contract_id: str,
) -> None:
    FAKE_SIG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
    resp = await client.post(
        f"{CONTRACTS_URL}/{contract_id}/sign",
        json={"purchaser_signature_b64": FAKE_SIG},
        headers=_headers(token, tenant_id),
    )
    assert resp.status_code == 200, resp.text


async def _issue_contract(
    client: AsyncClient,
    token: str,
    tenant_id,
    contract_id: str,
) -> dict:
    resp = await client.patch(
        f"{CONTRACTS_URL}/{contract_id}/issue",
        headers=_headers(token, tenant_id),
    )
    return resp


# ── T-01: Issue endpoint exists and returns 200 ───────────────────────────────

@pytest.mark.asyncio
async def test_issue_endpoint_returns_200(
    client: AsyncClient, admin_token: str, test_account
):
    """T-01: PATCH /contracts/{id}/issue on a signed contract → 200."""
    contract = await _create_contract(client, admin_token, test_account.id)
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    resp = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    assert resp.status_code == 200, resp.text
    data = resp.json()["data"]
    assert data["contract_id"] == contract["id"]
    assert data["contract_number"] == contract["contract_number"]


# ── T-02: Response contains payment_link_url ─────────────────────────────────

@pytest.mark.asyncio
async def test_issue_response_has_payment_link(
    client: AsyncClient, admin_token: str, test_account
):
    """T-02: Issue response contains a non-empty payment_link_url."""
    contract = await _create_contract(client, admin_token, test_account.id)
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    resp = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    data = resp.json()["data"]
    assert "payment_link_url" in data
    url = data["payment_link_url"]
    assert url.startswith("https://")
    assert "/pay/" in url


# ── T-03: email_sent_to matches purchaser_email ───────────────────────────────

@pytest.mark.asyncio
async def test_issue_email_sent_to_matches_purchaser(
    client: AsyncClient, admin_token: str, test_account
):
    """T-03: email_sent_to in response equals the purchaser_email."""
    contract = await _create_contract(
        client, admin_token, test_account.id, purchaser_email="buyer@example.ca"
    )
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    resp = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    data = resp.json()["data"]
    assert data["email_sent_to"] == "buyer@example.ca"


# ── T-04: Full payment plan creates exactly 1 invoice ────────────────────────

@pytest.mark.asyncio
async def test_full_plan_creates_one_invoice(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    """T-04: payment_plan_type=full → 1 invoice; invoice amount = contract total_amount."""
    contract = await _create_contract(
        client, admin_token, test_account.id, payment_plan_type="full", total_amount="2000.00"
    )
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    resp = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    data = resp.json()["data"]
    assert len(data["invoices_created"]) == 1

    # Get the contract total (may include tax) and verify invoice matches
    contract_resp = await client.get(
        f"{CONTRACTS_URL}/{contract['id']}",
        headers=_headers(admin_token, test_account.id),
    )
    contract_total = float(contract_resp.json()["data"]["total_amount"])
    assert float(data["invoices_created"][0]["amount"]) == pytest.approx(contract_total, abs=0.01)

    # Confirm in DB
    result = await db_session.execute(
        select(Invoice).where(Invoice.contract_id == contract["id"])
    )
    invoices = result.scalars().all()
    assert len(invoices) == 1


# ── T-05: deposit_50 creates exactly 2 invoices ──────────────────────────────

@pytest.mark.asyncio
async def test_deposit_50_creates_two_invoices(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    """T-05: payment_plan_type=deposit_50 → 2 invoices; each ~50% of total."""
    contract = await _create_contract(
        client, admin_token, test_account.id,
        payment_plan_type="deposit_50",
        total_amount="2000.00",
    )
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    resp = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    data = resp.json()["data"]
    assert len(data["invoices_created"]) == 2

    # Invoices should sum to the contract total
    contract_resp = await client.get(
        f"{CONTRACTS_URL}/{contract['id']}",
        headers=_headers(admin_token, test_account.id),
    )
    contract_total = float(contract_resp.json()["data"]["total_amount"])
    inv_amounts = sorted(float(inv["amount"]) for inv in data["invoices_created"])
    assert sum(inv_amounts) == pytest.approx(contract_total, abs=0.02)
    # Both invoices should be roughly equal (50/50 split)
    assert inv_amounts[0] == pytest.approx(inv_amounts[1], rel=0.01)

    result = await db_session.execute(
        select(Invoice).where(Invoice.contract_id == contract["id"])
    )
    invoices = result.scalars().all()
    assert len(invoices) == 2


# ── T-06: deposit_50 second invoice has no due date ──────────────────────────

@pytest.mark.asyncio
async def test_deposit_50_balance_invoice_has_no_due_date(
    client: AsyncClient, admin_token: str, test_account
):
    """T-06: deposit_50 → balance invoice has due_date=null."""
    contract = await _create_contract(
        client, admin_token, test_account.id,
        payment_plan_type="deposit_50",
        total_amount="2000.00",
    )
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    resp = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    data = resp.json()["data"]
    # At least one invoice must have null due_date (the balance invoice)
    null_due = [inv for inv in data["invoices_created"] if inv.get("due_date") is None]
    assert len(null_due) == 1


# ── T-07: contract_sent_at is set in DB after issue ──────────────────────────

@pytest.mark.asyncio
async def test_contract_sent_at_set_after_issue(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    """T-07: After issue, contract.contract_sent_at is not None."""
    contract = await _create_contract(client, admin_token, test_account.id)
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    await _issue_contract(client, admin_token, test_account.id, contract["id"])

    result = await db_session.execute(
        select(Contract).where(Contract.id == contract["id"])
    )
    db_contract = result.scalar_one()
    assert db_contract.contract_sent_at is not None


# ── T-08: payment_token stored and is 64 hex chars ────────────────────────────

@pytest.mark.asyncio
async def test_payment_token_stored_64_chars(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    """T-08: contract.payment_token is 64 lowercase hex characters."""
    contract = await _create_contract(client, admin_token, test_account.id)
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    await _issue_contract(client, admin_token, test_account.id, contract["id"])

    result = await db_session.execute(
        select(Contract).where(Contract.id == contract["id"])
    )
    db_contract = result.scalar_one()
    assert db_contract.payment_token is not None
    assert len(db_contract.payment_token) == 64
    assert db_contract.payment_token.isalnum()


# ── T-09: Double issue returns 409 ───────────────────────────────────────────

@pytest.mark.asyncio
async def test_double_issue_returns_409(
    client: AsyncClient, admin_token: str, test_account
):
    """T-09: Calling PATCH /issue twice → second call returns 409 Conflict."""
    contract = await _create_contract(client, admin_token, test_account.id)
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    resp1 = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    assert resp1.status_code == 200
    resp2 = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    assert resp2.status_code == 409


# ── T-10: Issue non-existent contract → 404 ──────────────────────────────────

@pytest.mark.asyncio
async def test_issue_nonexistent_contract_returns_404(
    client: AsyncClient, admin_token: str, test_account
):
    """T-10: PATCH /contracts/{unknown-id}/issue → 404."""
    import uuid
    fake_id = str(uuid.uuid4())
    resp = await client.patch(
        f"{CONTRACTS_URL}/{fake_id}/issue",
        headers=_headers(admin_token, test_account.id),
    )
    assert resp.status_code == 404


# ── T-11: Issue without manager role returns 403 ─────────────────────────────

@pytest.mark.asyncio
async def test_issue_requires_manager_role(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    """T-11: Staff user (below manager) cannot issue → 403."""
    from src.apps.auth.models.user import User
    from src.core.security import hash_password, create_access_token, build_token_payload

    staff_user = User(
        tenant_id=test_account.id,
        email="staff_issue@example.ca",
        password_hash=hash_password("Password1!"),
        first_name="Staff",
        last_name="User",
        role="staff",
        status="active",
    )
    db_session.add(staff_user)
    await db_session.flush()
    staff_token = create_access_token(build_token_payload(staff_user, test_account))

    contract = await _create_contract(client, admin_token, test_account.id)
    await _sign_contract(client, admin_token, test_account.id, contract["id"])

    resp = await client.patch(
        f"{CONTRACTS_URL}/{contract['id']}/issue",
        headers=_headers(staff_token, test_account.id),
    )
    assert resp.status_code == 403


# ── T-12: Opportunity stage advances to contract_sent ─────────────────────────

@pytest.mark.asyncio
async def test_issue_advances_opportunity_to_contract_sent(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    """T-12: After issue, linked opportunity.stage = 'contract_sent'."""
    opp_id = await _create_opportunity(client, admin_token, test_account.id)
    contract = await _create_contract(
        client, admin_token, test_account.id, opportunity_id=opp_id
    )
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    resp = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    data = resp.json()["data"]
    assert data["opportunity_stage_updated_to"] == "contract_sent"

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


# ── T-13: No linked opp → opportunity_stage_updated_to is null ───────────────

@pytest.mark.asyncio
async def test_issue_without_opportunity_stage_is_null(
    client: AsyncClient, admin_token: str, test_account
):
    """T-13: Contract with no opportunity → opportunity_stage_updated_to = null."""
    contract = await _create_contract(client, admin_token, test_account.id, opportunity_id=None)
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    resp = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    data = resp.json()["data"]
    assert data["opportunity_stage_updated_to"] is None


# ── T-14: GET /contracts/{id} returns issuance fields after issue ─────────────

@pytest.mark.asyncio
async def test_get_contract_after_issue_has_issuance_fields(
    client: AsyncClient, admin_token: str, test_account
):
    """T-14: After issue, GET /contracts/{id} returns payment_link_url and contract_sent_at."""
    contract = await _create_contract(client, admin_token, test_account.id)
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    await _issue_contract(client, admin_token, test_account.id, contract["id"])

    resp = await client.get(
        f"{CONTRACTS_URL}/{contract['id']}",
        headers=_headers(admin_token, test_account.id),
    )
    assert resp.status_code == 200
    data = resp.json()["data"]
    assert data.get("payment_link_url") is not None
    assert data.get("contract_sent_at") is not None


# ── T-15: Payment token is unique per contract ────────────────────────────────

@pytest.mark.asyncio
async def test_payment_tokens_are_unique(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    """T-15: Two issued contracts have different payment_tokens."""
    c1 = await _create_contract(client, admin_token, test_account.id)
    c2 = await _create_contract(client, admin_token, test_account.id)
    await _sign_contract(client, admin_token, test_account.id, c1["id"])
    await _sign_contract(client, admin_token, test_account.id, c2["id"])
    await _issue_contract(client, admin_token, test_account.id, c1["id"])
    await _issue_contract(client, admin_token, test_account.id, c2["id"])

    r1 = await db_session.execute(select(Contract).where(Contract.id == c1["id"]))
    r2 = await db_session.execute(select(Contract).where(Contract.id == c2["id"]))
    token1 = r1.scalar_one().payment_token
    token2 = r2.scalar_one().payment_token
    assert token1 is not None
    assert token2 is not None
    assert token1 != token2


# ── T-16: No purchaser email → email_sent_to is null ─────────────────────────

@pytest.mark.asyncio
async def test_issue_no_email_sent_to_when_no_purchaser_email(
    client: AsyncClient, admin_token: str, test_account
):
    """T-16: Contract without purchaser_email → email_sent_to = null, still 200."""
    contract = await _create_contract(
        client, admin_token, test_account.id, purchaser_email=None
    )
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    resp = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    assert resp.status_code == 200
    data = resp.json()["data"]
    assert data["email_sent_to"] is None
