"""
Contract Management (INDL-08) tests.

Covers:
  - List contracts
  - Create contract (wizard endpoint)
  - Get contract by ID (with eager-loaded line items)
  - Sign contract (auto-invoice, idempotency)
  - Soft delete contract
  - Tenant isolation

NOTE: These tests require the Alembic migrations to be fully applied on the
      test database (``alembic upgrade head``). If the ``contracts`` or
      ``contract_line_items`` tables do not exist the tests will fail with a
      ProgrammingError / UndefinedTableError rather than a test assertion error.
      In that case, run:

          docker-compose exec api alembic upgrade head
      or
          alembic upgrade head  (local venv)

      and re-run the suite.
"""

import base64
import struct
import zlib
from decimal import Decimal
from unittest.mock import AsyncMock, patch
from uuid import uuid4

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

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_tiny_png_b64() -> str:
    """Return a valid base64-encoded 1×1 transparent PNG.

    The PNG data URI is accepted by ContractSignRequest's Pydantic validator
    (``data:image/png;base64,<data>``).  Using a real but minimal PNG avoids
    having to generate a full-size image in tests.
    """
    # Minimal 1×1 RGBA PNG constructed manually (no external imaging lib needed)
    def _png_chunk(chunk_type: bytes, data: bytes) -> bytes:
        length = struct.pack(">I", len(data))
        crc = struct.pack(">I", zlib.crc32(chunk_type + data) & 0xFFFFFFFF)
        return length + chunk_type + data + crc

    png_sig = b"\x89PNG\r\n\x1a\n"
    ihdr_data = struct.pack(">IIBBBBB", 1, 1, 8, 6, 0, 0, 0)  # 1x1 RGBA
    ihdr = _png_chunk(b"IHDR", ihdr_data)
    raw_row = b"\x00\x00\x00\x00\x00"  # filter byte + 4-channel pixel (transparent)
    compressed = zlib.compress(raw_row)
    idat = _png_chunk(b"IDAT", compressed)
    iend = _png_chunk(b"IEND", b"")
    png_bytes = png_sig + ihdr + idat + iend
    return "data:image/png;base64," + base64.b64encode(png_bytes).decode()


TINY_PNG_B64 = _make_tiny_png_b64()

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest_asyncio.fixture
async def tenant_account(db_session: AsyncSession):
    """Fresh tenant account for each test (unique subdomain avoids collisions)."""
    from src.apps.tenants.models.account import Account

    account = Account(
        organization_name="Contract Test Cemetery",
        subdomain=f"contract-test-{uuid4().hex[:8]}",
        contact_email=f"admin-{uuid4().hex[:6]}@contracttest.com",
        plan="starter",
        status="active",
    )
    db_session.add(account)
    await db_session.flush()
    return account


@pytest_asyncio.fixture
async def manager_user(db_session: AsyncSession, tenant_account):
    """Manager-role user for contract creation and signing."""
    from src.apps.auth.models.user import User
    from src.core.security import hash_password

    user = User(
        tenant_id=tenant_account.id,
        email=f"manager-{uuid4().hex[:6]}@contracttest.com",
        password_hash=hash_password("TestPassword123!"),
        first_name="Manager",
        last_name="User",
        role="manager",
        status="active",
    )
    db_session.add(user)
    await db_session.flush()
    return user


@pytest_asyncio.fixture
async def admin_user(db_session: AsyncSession, tenant_account):
    """Administrator-role user for delete operations."""
    from src.apps.auth.models.user import User
    from src.core.security import hash_password

    user = User(
        tenant_id=tenant_account.id,
        email=f"admin-{uuid4().hex[:6]}@contracttest.com",
        password_hash=hash_password("TestPassword123!"),
        first_name="Admin",
        last_name="User",
        role="administrator",
        status="active",
    )
    db_session.add(user)
    await db_session.flush()
    return user


@pytest_asyncio.fixture
async def manager_headers(manager_user, tenant_account):
    """Authorization headers for the manager user."""
    from src.core.security import create_access_token, build_token_payload

    token = create_access_token(build_token_payload(manager_user, tenant_account))
    return {
        "Authorization": f"Bearer {token}",
        "X-Tenant-ID": str(tenant_account.id),
    }


@pytest_asyncio.fixture
async def admin_headers(admin_user, tenant_account):
    """Authorization headers for the administrator user."""
    from src.core.security import create_access_token, build_token_payload

    token = create_access_token(build_token_payload(admin_user, tenant_account))
    return {
        "Authorization": f"Bearer {token}",
        "X-Tenant-ID": str(tenant_account.id),
    }


# ---------------------------------------------------------------------------
# Convenience: create a contract via the API and return its parsed data dict
# ---------------------------------------------------------------------------

async def _create_contract(client: AsyncClient, headers: dict, overrides: dict | None = None) -> dict:
    """POST to /api/v1/sales/contracts and assert 201; return response data."""
    payload = {
        "contract_type": "pre_need",
        "purchaser_name": "Jane Test",
        "purchaser_email": "jane@test.com",
        "payment_plan_type": "full",
        "line_items": [
            {"description": "Plot fee", "quantity": 1, "unit_price": 1000.00},
            {"description": "Opening fee", "quantity": 2, "unit_price": 250.00},
        ],
    }
    if overrides:
        payload.update(overrides)

    resp = await client.post("/api/v1/sales/contracts", json=payload, headers=headers)
    assert resp.status_code == 201, f"Expected 201, got {resp.status_code}: {resp.text}"
    return resp.json()["data"]


# ===========================================================================
# 1. List contracts
# ===========================================================================


@pytest.mark.asyncio
async def test_list_contracts_empty(client: AsyncClient, manager_headers, manager_user):
    """Returns 200 with an empty items list when the tenant has no contracts."""
    resp = await client.get("/api/v1/sales/contracts", headers=manager_headers)
    assert resp.status_code == 200
    body = resp.json()
    assert body["success"] is True
    # paginated() returns { data: [...], total: N, page: N, page_size: N, pages: N }
    assert isinstance(body["data"], list)
    # Either empty or previously-inserted rows are 0 for this brand-new tenant
    assert body["total"] == 0


@pytest.mark.asyncio
async def test_list_contracts_requires_auth(client: AsyncClient, tenant_account):
    """Returns 401 without an Authorization header."""
    resp = await client.get(
        "/api/v1/sales/contracts",
        headers={"X-Tenant-ID": str(tenant_account.id)},
    )
    assert resp.status_code == 401


# ===========================================================================
# 2. Create contract
# ===========================================================================


@pytest.mark.asyncio
async def test_create_contract_returns_201(client: AsyncClient, manager_headers, manager_user, tenant_account):
    """Creates a draft contract with line items; validates contract_number and status."""
    data = await _create_contract(client, manager_headers)

    assert data["status"] == "draft"
    assert data["contract_number"].startswith("CNT-")
    assert data["purchaser_name"] == "Jane Test"
    assert isinstance(data["line_items"], list)
    assert len(data["line_items"]) == 2


@pytest.mark.asyncio
async def test_create_contract_total_with_hst(client: AsyncClient, manager_headers, manager_user, tenant_account):
    """total_amount = sum(qty * unit_price) * 1.13 (13% HST)."""
    data = await _create_contract(client, manager_headers)

    # subtotal: 1*1000 + 2*250 = 1500; total with 13% HST = 1695.00
    expected_total = Decimal("1500.00") * Decimal("1.13")
    assert Decimal(str(data["total_amount"])) == expected_total.quantize(Decimal("0.01"))


@pytest.mark.asyncio
async def test_create_contract_requires_manager_role(client: AsyncClient, tenant_account, db_session):
    """Staff-role user (below MANAGER) gets 403 on create."""
    from src.apps.auth.models.user import User
    from src.core.security import hash_password, create_access_token, build_token_payload

    staff = User(
        tenant_id=tenant_account.id,
        email=f"staff-{uuid4().hex[:6]}@test.com",
        password_hash=hash_password("Password123!"),
        first_name="Staff",
        last_name="User",
        role="staff",
        status="active",
    )
    db_session.add(staff)
    await db_session.flush()

    token = create_access_token(build_token_payload(staff, tenant_account))
    headers = {
        "Authorization": f"Bearer {token}",
        "X-Tenant-ID": str(tenant_account.id),
    }
    resp = await client.post(
        "/api/v1/sales/contracts",
        json={
            "contract_type": "pre_need",
            "purchaser_name": "Someone",
            "payment_plan_type": "full",
            "line_items": [],
        },
        headers=headers,
    )
    assert resp.status_code == 403


# ===========================================================================
# 3. Get contract
# ===========================================================================


@pytest.mark.asyncio
async def test_get_contract_includes_line_items(client: AsyncClient, manager_headers, manager_user, tenant_account):
    """GET /contracts/{id} returns the full contract with line_items eager-loaded."""
    created = await _create_contract(client, manager_headers)
    contract_id = created["id"]

    resp = await client.get(f"/api/v1/sales/contracts/{contract_id}", headers=manager_headers)
    assert resp.status_code == 200
    data = resp.json()["data"]
    assert data["id"] == contract_id
    assert len(data["line_items"]) == 2
    # ContractDetailResponse includes purchaser_signature_b64 (None for unsigned)
    assert "purchaser_signature_b64" in data


@pytest.mark.asyncio
async def test_get_contract_not_found(client: AsyncClient, manager_headers, manager_user, tenant_account):
    """Returns 404 for a non-existent UUID."""
    resp = await client.get(
        f"/api/v1/sales/contracts/{uuid4()}",
        headers=manager_headers,
    )
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_get_contract_cross_tenant_returns_404(client: AsyncClient, db_session):
    """Tenant B cannot retrieve a contract that belongs to Tenant A."""
    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.contract import Contract

    # --- Tenant A: create contract directly in DB ---
    tenant_a = Account(
        organization_name="Cemetery Tenant A",
        subdomain=f"tenant-a-{uuid4().hex[:8]}",
        contact_email=f"a-{uuid4().hex[:6]}@test.com",
        plan="starter",
        status="active",
    )
    tenant_b = Account(
        organization_name="Cemetery Tenant B",
        subdomain=f"tenant-b-{uuid4().hex[:8]}",
        contact_email=f"b-{uuid4().hex[:6]}@test.com",
        plan="starter",
        status="active",
    )
    db_session.add_all([tenant_a, tenant_b])
    await db_session.flush()

    user_a = User(
        tenant_id=tenant_a.id,
        email=f"ua-{uuid4().hex[:6]}@test.com",
        password_hash=hash_password("Password123!"),
        first_name="User",
        last_name="A",
        role="manager",
        status="active",
    )
    user_b = User(
        tenant_id=tenant_b.id,
        email=f"ub-{uuid4().hex[:6]}@test.com",
        password_hash=hash_password("Password123!"),
        first_name="User",
        last_name="B",
        role="manager",
        status="active",
    )
    db_session.add_all([user_a, user_b])
    await db_session.flush()

    # Directly insert a contract for tenant A
    contract_a = Contract(
        tenant_id=tenant_a.id,
        contract_number=f"CNT-{uuid4().hex[:6]}",
        contract_type="pre_need",
        purchaser_name="Test Person",
        status="draft",
        total_amount=Decimal("100.00"),
        created_by=user_a.id,
    )
    db_session.add(contract_a)
    await db_session.flush()

    # Tenant B's token + tenant header
    token_b = create_access_token(build_token_payload(user_b, tenant_b))
    headers_b = {
        "Authorization": f"Bearer {token_b}",
        "X-Tenant-ID": str(tenant_b.id),
    }

    resp = await client.get(f"/api/v1/sales/contracts/{contract_a.id}", headers=headers_b)
    assert resp.status_code == 404, (
        f"Expected 404 (tenant isolation), got {resp.status_code}: {resp.text}"
    )


# ===========================================================================
# 4. Sign contract
# ===========================================================================


@pytest.mark.asyncio
async def test_sign_contract_success(client: AsyncClient, manager_headers, manager_user, tenant_account):
    """Signing a draft contract sets status='signed', signed_at, and witness_name."""
    created = await _create_contract(client, manager_headers)
    contract_id = created["id"]

    with patch("arq.create_pool", new_callable=AsyncMock) as mock_pool:
        mock_pool.return_value = AsyncMock()
        mock_pool.return_value.enqueue_job = AsyncMock()
        mock_pool.return_value.aclose = AsyncMock()

        resp = await client.post(
            f"/api/v1/sales/contracts/{contract_id}/sign",
            json={
                "purchaser_signature_b64": TINY_PNG_B64,
                "witness_name": "Witness Person",
            },
            headers=manager_headers,
        )

    assert resp.status_code == 200, f"Expected 200, got {resp.status_code}: {resp.text}"
    data = resp.json()["data"]
    assert data["status"] == "signed"
    assert data["witness_name"] == "Witness Person"
    assert data["signed_at"] is not None
    # ContractDetailResponse includes the signature blob back
    assert data["purchaser_signature_b64"] == TINY_PNG_B64


@pytest.mark.asyncio
async def test_sign_contract_auto_creates_invoice(client: AsyncClient, manager_headers, manager_user, tenant_account, db_session):
    """Signing a contract automatically creates one Invoice row in the DB."""
    from src.apps.billing.models.invoice import Invoice

    created = await _create_contract(client, manager_headers)
    contract_id = created["id"]

    with patch("arq.create_pool", new_callable=AsyncMock) as mock_pool:
        mock_pool.return_value = AsyncMock()
        mock_pool.return_value.enqueue_job = AsyncMock()
        mock_pool.return_value.aclose = AsyncMock()

        resp = await client.post(
            f"/api/v1/sales/contracts/{contract_id}/sign",
            json={"purchaser_signature_b64": TINY_PNG_B64},
            headers=manager_headers,
        )

    assert resp.status_code == 200

    # Verify invoice was created in the DB
    result = await db_session.execute(
        select(Invoice).where(Invoice.contract_id == contract_id)
    )
    invoices = result.scalars().all()
    assert len(invoices) == 1, f"Expected 1 auto-created invoice, found {len(invoices)}"
    invoice = invoices[0]
    assert invoice.status == "outstanding"
    assert invoice.tenant_id == tenant_account.id
    # Invoice total must match the contract total
    contract_total = Decimal(str(created["total_amount"]))
    assert invoice.total_amount == contract_total
    assert invoice.balance_due == contract_total


@pytest.mark.asyncio
async def test_sign_contract_idempotent(client: AsyncClient, manager_headers, manager_user, tenant_account, db_session):
    """Calling sign twice must not create a second invoice and must return the same contract."""
    from src.apps.billing.models.invoice import Invoice

    created = await _create_contract(client, manager_headers)
    contract_id = created["id"]

    with patch("arq.create_pool", new_callable=AsyncMock) as mock_pool:
        mock_pool.return_value = AsyncMock()
        mock_pool.return_value.enqueue_job = AsyncMock()
        mock_pool.return_value.aclose = AsyncMock()

        # First sign
        resp1 = await client.post(
            f"/api/v1/sales/contracts/{contract_id}/sign",
            json={"purchaser_signature_b64": TINY_PNG_B64, "witness_name": "First Witness"},
            headers=manager_headers,
        )
        assert resp1.status_code == 200

        # Second sign — should be idempotent
        resp2 = await client.post(
            f"/api/v1/sales/contracts/{contract_id}/sign",
            json={"purchaser_signature_b64": TINY_PNG_B64, "witness_name": "Second Witness"},
            headers=manager_headers,
        )
        assert resp2.status_code == 200

    # Still only one invoice
    result = await db_session.execute(
        select(Invoice).where(Invoice.contract_id == contract_id)
    )
    invoices = result.scalars().all()
    assert len(invoices) == 1, (
        f"Idempotency broken: found {len(invoices)} invoices after two sign calls"
    )

    # The witness_name must be from the *first* successful sign (not overwritten)
    data = resp2.json()["data"]
    assert data["witness_name"] == "First Witness"


@pytest.mark.asyncio
async def test_sign_contract_invalid_base64_rejected(client: AsyncClient, manager_headers, manager_user, tenant_account):
    """Pydantic validator must reject payloads that are not valid base64."""
    created = await _create_contract(client, manager_headers)
    contract_id = created["id"]

    resp = await client.post(
        f"/api/v1/sales/contracts/{contract_id}/sign",
        json={"purchaser_signature_b64": "!!!NOT_VALID_BASE64!!!"},
        headers=manager_headers,
    )
    # Pydantic validation error → 422
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_sign_contract_rejects_non_png_jpeg_data_uri(client: AsyncClient, manager_headers, manager_user, tenant_account):
    """A data URI for image/gif must be rejected with 422."""
    created = await _create_contract(client, manager_headers)
    contract_id = created["id"]

    gif_b64 = "data:image/gif;base64," + base64.b64encode(b"GIF89a\x01\x00\x01\x00").decode()
    resp = await client.post(
        f"/api/v1/sales/contracts/{contract_id}/sign",
        json={"purchaser_signature_b64": gif_b64},
        headers=manager_headers,
    )
    assert resp.status_code == 422


# ===========================================================================
# 5. Soft delete
# ===========================================================================


@pytest.mark.asyncio
async def test_soft_delete_contract(client: AsyncClient, admin_headers, manager_headers, admin_user, manager_user, tenant_account, db_session):
    """DELETE /contracts/{id} soft-deletes the contract (deleted_at is set)."""
    from src.apps.sales.models.contract import Contract

    created = await _create_contract(client, manager_headers)
    contract_id = created["id"]

    resp = await client.delete(
        f"/api/v1/sales/contracts/{contract_id}",
        headers=admin_headers,
    )
    assert resp.status_code == 200, f"Expected 200, got {resp.status_code}: {resp.text}"

    # Verify deleted_at is set in the DB
    from uuid import UUID as UUIDType
    result = await db_session.execute(
        select(Contract).where(Contract.id == UUIDType(contract_id))
    )
    contract_row = result.scalar_one_or_none()
    assert contract_row is not None, "Row should still exist (soft delete, not hard delete)"
    assert contract_row.deleted_at is not None, "deleted_at must be set after soft delete"


@pytest.mark.asyncio
async def test_soft_deleted_contract_excluded_from_list(client: AsyncClient, admin_headers, manager_headers, admin_user, manager_user, tenant_account):
    """After soft-delete, the contract must not appear in the list endpoint."""
    created = await _create_contract(client, manager_headers)
    contract_id = created["id"]

    # Delete it
    del_resp = await client.delete(
        f"/api/v1/sales/contracts/{contract_id}",
        headers=admin_headers,
    )
    assert del_resp.status_code == 200

    # List must not contain it — paginated() returns { data: [...], total: N, ... }
    list_resp = await client.get("/api/v1/sales/contracts", headers=manager_headers)
    assert list_resp.status_code == 200
    items = list_resp.json()["data"]
    ids = [item["id"] for item in items]
    assert contract_id not in ids, "Soft-deleted contract must not appear in list"


@pytest.mark.asyncio
async def test_delete_contract_requires_administrator_role(client: AsyncClient, manager_headers, manager_user, tenant_account):
    """Manager role (below ADMINISTRATOR) must receive 403 on delete."""
    created = await _create_contract(client, manager_headers)
    contract_id = created["id"]

    resp = await client.delete(
        f"/api/v1/sales/contracts/{contract_id}",
        headers=manager_headers,
    )
    assert resp.status_code == 403


# ===========================================================================
# 6. Tenant isolation (additional explicit check)
# ===========================================================================


@pytest.mark.asyncio
async def test_tenant_isolation_list(client: AsyncClient, db_session):
    """Tenant B's list endpoint must not surface contracts belonging to Tenant A."""
    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.contract import Contract

    # Set up two separate tenants
    tenant_a = Account(
        organization_name="Isolation Cemetery A",
        subdomain=f"iso-a-{uuid4().hex[:8]}",
        contact_email=f"iso-a-{uuid4().hex[:6]}@test.com",
        plan="starter",
        status="active",
    )
    tenant_b = Account(
        organization_name="Isolation Cemetery B",
        subdomain=f"iso-b-{uuid4().hex[:8]}",
        contact_email=f"iso-b-{uuid4().hex[:6]}@test.com",
        plan="starter",
        status="active",
    )
    db_session.add_all([tenant_a, tenant_b])
    await db_session.flush()

    user_a = User(
        tenant_id=tenant_a.id,
        email=f"ua-iso-{uuid4().hex[:6]}@test.com",
        password_hash=hash_password("Password123!"),
        first_name="User",
        last_name="A",
        role="manager",
        status="active",
    )
    user_b = User(
        tenant_id=tenant_b.id,
        email=f"ub-iso-{uuid4().hex[:6]}@test.com",
        password_hash=hash_password("Password123!"),
        first_name="User",
        last_name="B",
        role="manager",
        status="active",
    )
    db_session.add_all([user_a, user_b])
    await db_session.flush()

    # Insert a contract directly for tenant A
    contract_a = Contract(
        tenant_id=tenant_a.id,
        contract_number=f"CNT-ISO-{uuid4().hex[:6]}",
        contract_type="pre_need",
        purchaser_name="Tenant A Customer",
        status="draft",
        total_amount=Decimal("500.00"),
        created_by=user_a.id,
    )
    db_session.add(contract_a)
    await db_session.flush()

    # Tenant B should see zero contracts in their list
    token_b = create_access_token(build_token_payload(user_b, tenant_b))
    headers_b = {
        "Authorization": f"Bearer {token_b}",
        "X-Tenant-ID": str(tenant_b.id),
    }

    resp = await client.get("/api/v1/sales/contracts", headers=headers_b)
    assert resp.status_code == 200
    body = resp.json()
    # paginated() returns { data: [...], total: N, ... }
    assert body["total"] == 0, (
        f"Tenant B should see 0 contracts but got {body['total']}; isolation broken."
    )
    for item in body["data"]:
        assert item["id"] != str(contract_a.id), (
            "Tenant A's contract leaked into Tenant B's list — isolation violation."
        )
