"""
Tests for build_contract_pdf_bytes' bank-details "Payment Details" section
(src/apps/sales/services/contract_pdf_service.py).

Covers:
  1. The full, real account number is embedded in the generated PDF when
     given a decrypted bank_details dict (BankDetailsService.
     get_decrypted_for_tenant's shape) — the purchaser needs it to actually
     complete an EFT/wire transfer. The function itself renders whatever
     account_number it's handed verbatim: it is the CALLER's job to decide
     masked vs. full (get_decrypted_for_tenant vs. get_masked_for_tenant),
     not this builder's.
  2. The new "To complete your payment..." instruction line appears
     immediately above "Payment Details" when bank details are configured,
     and is omitted when they are not.
  3. The SSRF/DoS security-fix-loop regression: free-text fields
     (bank_name, account_holder_name) that reach ReportLab's Paragraph
     mini-HTML parser are escaped via xml.sax.saxutils.escape before
     interpolation, so a value like '<img src="http://.../x"/>' cannot
     crash PDF generation or be interpreted as markup (SSRF vector).

No DB/HTTP involved — build_contract_pdf_bytes takes plain objects, so a
lightweight SimpleNamespace stand-in is used for contract/account, and a
plain dict for bank_details (matches the shape BankDetailsService.
get_decrypted_for_tenant/get_masked_for_tenant actually return).

Text is extracted with pypdf (already a project dependency — see
requirements.txt, "PDF text extraction for AI record extraction") rather
than grepping the raw bytes, since ReportLab's SimpleDocTemplate compresses
content streams (FlateDecode) by default, so substrings are not reliably
greppable in the raw PDF bytes.
"""
from decimal import Decimal
from types import SimpleNamespace

from pypdf import PdfReader
from io import BytesIO

from src.apps.sales.services.contract_pdf_service import build_contract_pdf_bytes

INSTRUCTION_TEXT = "To complete your payment, please use the bank details below"


def _contract(**overrides) -> SimpleNamespace:
    data = dict(
        contract_number="CNT-0001",
        purchaser_name="Jane Test",
        total_amount=Decimal("1695.00"),
        purchaser_signature_b64=None,
        witness_name=None,
    )
    data.update(overrides)
    return SimpleNamespace(**data)


def _account(**overrides) -> SimpleNamespace:
    data = dict(
        organization_name="Riverside Cemetery",
        contact_email="info@riverside.example",
        contact_phone="555-1234",
    )
    data.update(overrides)
    return SimpleNamespace(**data)


def _bank_details(**overrides) -> dict:
    data = dict(
        account_holder_name="Riverside Cemetery Corp",
        bank_name="Test National Bank",
        institution_number="001",
        transit_number="12345",
        account_number="1234566789",
        account_type="chequing",
        currency="CAD",
        branch_address="1 Bank St, Toronto, ON",
    )
    data.update(overrides)
    return data


def _extract_text(pdf_bytes: bytes) -> str:
    reader = PdfReader(BytesIO(pdf_bytes))
    return "\n".join(page.extract_text() or "" for page in reader.pages)


class TestBuildContractPdfBytesBankDetails:
    def test_no_bank_details_does_not_raise_and_produces_valid_pdf(self):
        pdf_bytes = build_contract_pdf_bytes(_contract(), account=_account(), bank_details=None)
        assert pdf_bytes[:4] == b"%PDF"

        text = _extract_text(pdf_bytes)
        assert "have not yet been configured" in text
        # No bank details configured — nothing to point the reader "below" to.
        assert INSTRUCTION_TEXT not in text

    def test_full_account_number_appears_in_pdf_text(self):
        bank = _bank_details(account_number="9988775544")
        pdf_bytes = build_contract_pdf_bytes(_contract(), account=_account(), bank_details=bank)

        assert pdf_bytes[:4] == b"%PDF"
        text = _extract_text(pdf_bytes)
        assert "9988775544" in text

    def test_instruction_line_appears_immediately_above_payment_details(self):
        bank = _bank_details()
        pdf_bytes = build_contract_pdf_bytes(_contract(), account=_account(), bank_details=bank)
        text = _extract_text(pdf_bytes)

        assert INSTRUCTION_TEXT in text
        assert text.index(INSTRUCTION_TEXT) < text.index("Payment Details")

    def test_caller_supplied_masked_number_is_rendered_verbatim(self):
        """The builder is agnostic about masking — it renders whatever
        account_number it's given. The public/unauthenticated PDF endpoint
        relies on this by passing BankDetailsService.get_masked_for_tenant's
        dict (account_number already "****1234") instead of the decrypted
        one."""
        bank = _bank_details(account_number="****5544")
        pdf_bytes = build_contract_pdf_bytes(_contract(), account=_account(), bank_details=bank)

        text = _extract_text(pdf_bytes)
        assert "****5544" in text

    def test_bank_details_fields_render_in_pdf_text(self):
        bank = _bank_details(
            account_holder_name="Riverside Cemetery Corp",
            bank_name="Test National Bank",
            institution_number="001",
            transit_number="12345",
            account_number="6789012345",
            account_type="savings",
        )
        pdf_bytes = build_contract_pdf_bytes(_contract(), account=_account(), bank_details=bank)
        text = _extract_text(pdf_bytes)

        assert "Riverside Cemetery Corp" in text
        assert "Test National Bank" in text
        assert "001" in text
        assert "12345" in text
        assert "6789012345" in text
        assert "Savings" in text  # .title()-cased in the builder

    def test_ssrf_dos_regression_angle_bracket_payload_in_bank_name_does_not_raise(self):
        """Security regression test for the fixed SSRF/DoS finding: a raw
        '<img src="...">' in a tenant-editable free-text field used to reach
        ReportLab's Paragraph mini-HTML parser unescaped. It must now be
        escaped and rendered as inert literal text, not raise, and not be
        interpreted as markup."""
        payload = '<img src="http://example.invalid/x"/>'
        bank = _bank_details(bank_name=payload, account_holder_name=payload)

        pdf_bytes = build_contract_pdf_bytes(_contract(), account=_account(), bank_details=bank)

        assert pdf_bytes[:4] == b"%PDF"
        text = _extract_text(pdf_bytes)
        # the literal text of the payload should appear (escaped-then-rendered),
        # not be silently dropped or crash the build
        assert "img src=" in text
        assert "example.invalid" in text

    def test_ssrf_dos_regression_angle_bracket_payload_in_account_holder_name(self):
        payload = '<img src="http://example.invalid/evil"/><script>alert(1)</script>'
        bank = _bank_details(account_holder_name=payload)

        pdf_bytes = build_contract_pdf_bytes(_contract(), account=_account(), bank_details=bank)

        assert pdf_bytes[:4] == b"%PDF"

    def test_unclosed_tag_payload_does_not_raise(self):
        """A malformed/unclosed markup fragment was part of the original
        crash vector (ReportLab's mini-HTML parser choking on invalid
        markup) — confirm escaping neutralizes this too."""
        bank = _bank_details(bank_name="<b>Unclosed bank name")

        pdf_bytes = build_contract_pdf_bytes(_contract(), account=_account(), bank_details=bank)
        assert pdf_bytes[:4] == b"%PDF"

    def test_letterhead_cemetery_name_with_angle_brackets_does_not_raise(self):
        """Same escaping hardening applies to the letterhead's cemetery_name
        (account.organization_name), a separate tenant-editable free-text
        field rendered earlier in the same PDF."""
        account = _account(organization_name='<img src="http://example.invalid/y"/> Cemetery')

        pdf_bytes = build_contract_pdf_bytes(_contract(), account=account, bank_details=None)
        assert pdf_bytes[:4] == b"%PDF"
