"""
Tests for GET /api/public/contracts/{subdomain}/{contract_number}/pdf —
the unauthenticated contract-PDF redirect used by the contract QR code
(a scanning phone can't send the X-Tenant-Subdomain header the SPA injects,
so the tenant is resolved from the subdomain path segment instead).

Covers:
  - 404 for an unknown subdomain
  - 404 for a known tenant but unknown/soft-deleted contract_number
  - 302 redirect to a presigned S3 URL when S3 + pdf_s3_key are available
  - 200 streamed PDF fallback when S3 is not configured
  - Bank details on the streamed (non-S3) PDF fallback: masked account
    number present, full plaintext number never present, and the endpoint
    stays a graceful 200 when bank details are unconfigured for the tenant
    (this file was previously NOT updated for the bank-details feature —
    flagged by the Code Reviewer as a gap)
"""
from decimal import Decimal
from io import BytesIO
from unittest.mock import patch
from uuid import uuid4

import pytest
import pytest_asyncio
from cryptography.fernet import Fernet
from httpx import AsyncClient
from pypdf import PdfReader
from sqlalchemy.ext.asyncio import AsyncSession

BASE = "/api/public/contracts"

_TEST_FERNET_KEY = Fernet.generate_key().decode()


@pytest.fixture(autouse=True)
def _bank_details_encryption_key():
    """See tests/test_bank_details.py for the full rationale: local/dev
    leaves BANK_DETAILS_ENCRYPTION_KEY blank, which would make
    BankDetailsService.upsert raise RuntimeError. Only the bank-details
    tests below actually call upsert, but patching it for the whole module
    is harmless for the other tests here (they never touch bank_details)."""
    from src.core.config import settings as real_settings

    with patch.object(real_settings, "BANK_DETAILS_ENCRYPTION_KEY", _TEST_FERNET_KEY):
        yield


@pytest_asyncio.fixture
async def pdf_tenant_account(db_session: AsyncSession):
    from src.apps.tenants.models.account import Account

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


async def _make_contract(db_session, account, *, contract_number, pdf_s3_key=None, deleted=False, status="signed"):
    from datetime import datetime, timezone
    from src.apps.sales.models.contract import Contract

    contract = Contract(
        tenant_id=account.id,
        contract_number=contract_number,
        status=status,
        total_amount=Decimal("1000.00"),
        purchaser_name="Jane Test",
        pdf_s3_key=pdf_s3_key,
    )
    if deleted:
        contract.deleted_at = datetime.now(timezone.utc)
    db_session.add(contract)
    await db_session.flush()
    return contract


@pytest.mark.asyncio
async def test_unknown_subdomain_returns_404(client: AsyncClient):
    resp = await client.get(f"{BASE}/does-not-exist-subdomain/CNT-0001/pdf")
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_unknown_contract_number_returns_404(client: AsyncClient, pdf_tenant_account):
    resp = await client.get(f"{BASE}/{pdf_tenant_account.subdomain}/NO-SUCH-CONTRACT/pdf")
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_soft_deleted_contract_returns_404(
    client: AsyncClient, db_session: AsyncSession, pdf_tenant_account
):
    await _make_contract(
        db_session, pdf_tenant_account, contract_number="CNT-DELETED", deleted=True
    )
    resp = await client.get(f"{BASE}/{pdf_tenant_account.subdomain}/CNT-DELETED/pdf")
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_draft_contract_returns_404_not_the_pdf(
    client: AsyncClient, db_session: AsyncSession, pdf_tenant_account
):
    """This endpoint is public and unauthenticated, and contract numbers are
    sequential/guessable — it must not disclose a PDF before the contract is
    signed, even though the authenticated GET /sales/contracts/{id}/pdf has
    no such gate (it doesn't need one; it's already access-controlled)."""
    await _make_contract(
        db_session, pdf_tenant_account, contract_number="CNT-DRAFT", status="draft"
    )
    resp = await client.get(f"{BASE}/{pdf_tenant_account.subdomain}/CNT-DRAFT/pdf")
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_signed_contract_with_s3_key_redirects_to_presigned_url(
    client: AsyncClient, db_session: AsyncSession, pdf_tenant_account
):
    await _make_contract(
        db_session,
        pdf_tenant_account,
        contract_number="CNT-S3",
        pdf_s3_key="contracts/cnt-s3.pdf",
    )

    fake_presigned = "https://s3.example.com/contracts/cnt-s3.pdf?X-Amz-Signature=abc"

    # `get_presigned_contract_pdf_url` does `from src.core.config import settings`
    # inside the function body (matching this codebase's lazy-import convention
    # for settings in service functions), so the real settings singleton's
    # attributes must be patched directly rather than the module reference.
    from src.core.config import settings as real_settings

    with patch.object(real_settings, "AWS_ACCESS_KEY_ID", "fake-key"), \
         patch.object(real_settings, "AWS_SECRET_ACCESS_KEY", "fake-secret"), \
         patch.object(real_settings, "AWS_REGION", "us-east-1"), \
         patch.object(real_settings, "S3_BUCKET", "fake-bucket"), \
         patch("boto3.client") as mock_boto_client:
        mock_s3 = mock_boto_client.return_value
        mock_s3.generate_presigned_url.return_value = fake_presigned

        resp = await client.get(
            f"{BASE}/{pdf_tenant_account.subdomain}/CNT-S3/pdf",
            follow_redirects=False,
        )

    assert resp.status_code == 302
    assert resp.headers["location"] == fake_presigned


@pytest.mark.asyncio
async def test_signed_contract_without_s3_streams_pdf_fallback(
    client: AsyncClient, db_session: AsyncSession, pdf_tenant_account
):
    """No pdf_s3_key at all — falls through to the on-demand ReportLab build."""
    await _make_contract(db_session, pdf_tenant_account, contract_number="CNT-STREAM")

    resp = await client.get(f"{BASE}/{pdf_tenant_account.subdomain}/CNT-STREAM/pdf")

    assert resp.status_code == 200
    assert resp.headers["content-type"] == "application/pdf"
    assert resp.content[:4] == b"%PDF"


# ---------------------------------------------------------------------------
# Bank details on the public, unauthenticated PDF fallback
# ---------------------------------------------------------------------------

async def _configure_bank_details(db_session: AsyncSession, account, *, account_number: str):
    from src.apps.settings.schemas.requests import BankDetailsUpsertRequest
    from src.apps.settings.services.bank_details_service import BankDetailsService

    payload = BankDetailsUpsertRequest(
        account_holder_name="Riverside Cemetery Corp",
        bank_name="Test National Bank",
        institution_number="001",
        transit_number="12345",
        account_number=account_number,
        account_type="chequing",
        currency="CAD",
        branch_address="123 Bank St, Toronto, ON",
    )
    return await BankDetailsService.upsert(db_session, account.id, payload)


@pytest.mark.asyncio
async def test_signed_contract_pdf_includes_masked_bank_details_never_full_number(
    client: AsyncClient, db_session: AsyncSession, pdf_tenant_account
):
    """Public PDF fallback (no S3 configured) for a signed contract with
    bank details on file must show the masked account number and must
    never leak the full plaintext number anywhere in the response bytes."""
    known_account_number = "9988776655"
    await _configure_bank_details(db_session, pdf_tenant_account, account_number=known_account_number)
    await _make_contract(db_session, pdf_tenant_account, contract_number="CNT-BANK")

    resp = await client.get(f"{BASE}/{pdf_tenant_account.subdomain}/CNT-BANK/pdf")

    assert resp.status_code == 200
    assert resp.headers["content-type"] == "application/pdf"
    assert resp.content[:4] == b"%PDF"
    assert known_account_number.encode() not in resp.content

    text = "\n".join(page.extract_text() or "" for page in PdfReader(BytesIO(resp.content)).pages)
    assert "****6655" in text
    assert known_account_number not in text


@pytest.mark.asyncio
async def test_signed_contract_pdf_gracefully_handles_unconfigured_bank_details(
    client: AsyncClient, db_session: AsyncSession, pdf_tenant_account
):
    """No bank_details row at all for this tenant — the public endpoint must
    still return 200 with a valid PDF (the 'not yet configured' fallback
    text), not 404/500."""
    await _make_contract(db_session, pdf_tenant_account, contract_number="CNT-NOBANK")

    resp = await client.get(f"{BASE}/{pdf_tenant_account.subdomain}/CNT-NOBANK/pdf")

    assert resp.status_code == 200
    assert resp.headers["content-type"] == "application/pdf"
    assert resp.content[:4] == b"%PDF"
