# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: CC0-1.0

from __future__ import annotations

from io import BytesIO
from pathlib import Path

import pytest
from hypothesis import example, given, settings
from hypothesis.strategies import binary, characters, text

import pikepdf.codec


def test_encodable_table():
    for ordnum in pikepdf.codec.PDFDOC_ENCODABLE:
        char = chr(ordnum)
        pdfdoc_encoded = char.encode('pdfdoc')
        involuted = pdfdoc_encoded.decode('pdfdoc')
        assert char == involuted


def test_encode():
    assert 'abc'.encode('pdfdoc') == b'abc'
    with pytest.raises(UnicodeEncodeError):
        '你好'.encode('pdfdoc')
    assert '你好 world'.encode('pdfdoc', 'replace') == b'?? world'
    assert '你好 world'.encode('pdfdoc', 'ignore') == b' world'


def test_decode():
    assert b'A'.decode('pdfdoc') == 'A'
    assert b'\xa0'.decode('pdfdoc') == '€'


def test_unicode_surrogate():
    with pytest.raises(UnicodeEncodeError):
        '\ud800'.encode('pdfdoc')


@given(binary())
@example(b'\x9f')
@example(b'\xfe\xff')
@example(b'\xff\xfe')
def test_codec_involution(b):
    # For most binary strings, there is a pdfdoc decoding and the encoding of that
    # decoding recovers the initial string.
    try:
        assert b.decode('pdfdoc').encode('pdfdoc') == b
    except UnicodeDecodeError as e:
        # 0x7f, 0x9f, and 0xad have no defined mapping to Unicode, so we expect
        # strings contain them to raise a decoding exception
        assert set(e.object[e.start : e.end]) & set(b'\x7f\x9f\xad')
    except UnicodeEncodeError as e:
        assert "'pdfdoc' codec can't encode characters in position 0-1" in str(e)
        assert b.startswith(b'\xfe\xff') or b.startswith(b'\xff\xfe')


@given(text())
@example('\xfe\xff')
def test_break_encode(s):
    try:
        encoded_bytes = s.encode('pdfdoc')
    except UnicodeEncodeError:
        # Characters not in PDFDocEncoding will raise UnicodeEncodeError
        return
    else:
        try:
            assert encoded_bytes.decode('pdfdoc') == s, "encode -> decode failed"
        except UnicodeDecodeError:
            # Some byte sequences (0x7f, 0x9f, 0xad) have no Unicode mapping
            return


# whitelist_categories ensures that the listed Unicode categories will be produced
# whitelist_characters adds further characters (everything that is pdfdoc encodable)
# We specifically add Cs, surrogates, which the C++ binding layer needs extra
# help with (historically a pybind11 quirk; still exercised under nanobind).
pdfdoc_text = text(
    alphabet=characters(
        whitelist_categories=('N', 'L', 'M', 'P', 'Cs'),
        whitelist_characters=[chr(c) for c in pikepdf.codec.PDFDOC_ENCODABLE],
    ),
    max_size=1000,
)


@given(pdfdoc_text)
@example('\r\n')
@example('\r')
@example('\n')
@settings(deadline=60000)  # CI workers can be flakey
def test_open_encoding_pdfdoc_write(tmp_path_factory, s):
    folder = tmp_path_factory.mktemp('pdfdoc')
    txt = folder / 'pdfdoc.txt'
    with open(txt, 'w', encoding='pdfdoc', newline='') as f:
        try:
            f.write(s)
        except UnicodeEncodeError:
            return
    assert txt.read_bytes() == s.encode('pdfdoc')


@given(pdfdoc_text)
@settings(deadline=60000)  # CI workers can be flakey
@example('\r\n')
@example('\r')
@example('\n')
def test_open_encoding_pdfdoc_read(tmp_path_factory, s: str):
    folder = tmp_path_factory.mktemp('pdfdoc')
    txt: Path = folder / 'pdfdoc.txt'
    with open(txt, 'w', encoding='pdfdoc', newline='') as f:
        try:
            f.write(s)
        except UnicodeEncodeError:
            return
    with open(txt, encoding='pdfdoc', newline='') as f:
        result: str = f.read()
    assert result == s


@given(pdfdoc_text)
def test_stream_writer(s):
    bio = BytesIO()
    sw = pikepdf.codec.PdfDocStreamWriter(bio)
    try:
        sw.write(s)
    except UnicodeEncodeError:
        return
    bio.seek(0)
    data = bio.read()
    assert data == s.encode('pdfdoc')


@given(pdfdoc_text)
def test_stream_reader(s):
    try:
        bio = BytesIO(s.encode('pdfdoc_pikepdf'))
    except UnicodeEncodeError:
        return
    sr = pikepdf.codec.PdfDocStreamReader(bio)
    result = sr.read()
    assert result == s


def test_pdfdoc_encode_lookup_error():
    not_in_pdfdoc = '你好'
    with pytest.raises(LookupError):
        not_in_pdfdoc.encode('pdfdoc_pikepdf', errors="invalid error handler")


@given(binary(max_size=30))
def test_pdfdoc_qpdf_decode_consistency(b):
    pdfdoc_result, qpdf_result = None, None
    pdfdoc_error = False
    try:
        pdfdoc_result = b.decode('pdfdoc')
    except UnicodeDecodeError:
        pdfdoc_error = True

    qpdf_error = False
    try:
        qpdf_result = pikepdf._core.pdf_doc_to_utf8(b)
        if '\ufffd' in qpdf_result:
            qpdf_error = True
    except Exception:
        qpdf_error = True

    assert pdfdoc_error == qpdf_error
    if not pdfdoc_error and not qpdf_error:
        assert pdfdoc_result == qpdf_result


@given(text(max_size=30))
def test_pdfdoc_qpdf_encode_consistency(t):
    pdfdoc_result, qpdf_result = None, None
    pdfdoc_error = False
    try:
        pdfdoc_result = t.encode('pdfdoc')
    except UnicodeEncodeError:
        pdfdoc_error = True

    qpdf_error = False
    try:
        qpdf_success, qpdf_result = pikepdf._core.utf8_to_pdf_doc(t, b'\xad')
        if not qpdf_success:
            qpdf_error = True
    except Exception:
        qpdf_error = True

    assert pdfdoc_error == qpdf_error
    if not pdfdoc_error and not qpdf_error:
        assert pdfdoc_result == qpdf_result
