from io import BytesIO
from pathlib import Path

import pytest

from thriftpy2 import _compat
from thriftpy2 import load
from thriftpy2.protocol import binary as proto
from thriftpy2.thrift import TPayload, TType
from thriftpy2.utils import hexlify, serialize
from thriftpy2.transport.memory import TMemoryBuffer

TEST_DIR = Path(__file__).parent


class TItem(TPayload):
    thrift_spec = {
        1: (TType.I32, "id", False),
        2: (TType.LIST, "phones", (TType.STRING), False),
    }
    default_spec = [("id", None), ("phones", None)]


def test_pack_i8():
    b = BytesIO()
    proto.write_val(b, TType.I08, 123)
    assert "7b" == hexlify(b.getvalue())


def test_unpack_i8():
    b = BytesIO(b"{")
    assert 123 == proto.read_val(b, TType.I08)


def test_pack_i16():
    b = BytesIO()
    proto.write_val(b, TType.I16, 12345)
    assert "30 39" == hexlify(b.getvalue())


def test_unpack_i16():
    b = BytesIO(b"09")
    assert 12345 == proto.read_val(b, TType.I16)


def test_pack_i32():
    b = BytesIO()
    proto.write_val(b, TType.I32, 1234567890)
    assert "49 96 02 d2" == hexlify(b.getvalue())


def test_unpack_i32():
    b = BytesIO(b"I\x96\x02\xd2")
    assert 1234567890 == proto.read_val(b, TType.I32)


def test_pack_i64():
    b = BytesIO()
    proto.write_val(b, TType.I64, 1234567890123456789)
    assert "11 22 10 f4 7d e9 81 15" == hexlify(b.getvalue())


def test_unpack_i64():
    b = BytesIO(b"\x11\"\x10\xf4}\xe9\x81\x15")
    assert 1234567890123456789 == proto.read_val(b, TType.I64)


def test_pack_double():
    b = BytesIO()
    proto.write_val(b, TType.DOUBLE, 1234567890.1234567890)
    assert "41 d2 65 80 b4 87 e6 b7" == hexlify(b.getvalue())


def test_unpack_double():
    b = BytesIO(b"A\xd2e\x80\xb4\x87\xe6\xb7")
    assert 1234567890.1234567890 == proto.read_val(b, TType.DOUBLE)


def test_pack_string():
    b = BytesIO()
    proto.write_val(b, TType.STRING, "hello world!")
    assert "00 00 00 0c 68 65 6c 6c 6f 20 77 6f 72 6c 64 21" == \
        hexlify(b.getvalue())

    b = BytesIO()
    proto.write_val(b, TType.STRING, "你好世界")
    assert "00 00 00 0c e4 bd a0 e5 a5 bd e4 b8 96 e7 95 8c" == \
        hexlify(b.getvalue())


def test_unpack_string():
    b = BytesIO(b"\x00\x00\x00\x0c"
                b"\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xb8\x96\xe7\x95\x8c")
    assert "你好世界" == proto.read_val(b, TType.STRING)


def test_unpack_binary():
    bs = BytesIO(b"\x00\x00\x00\x0c"
                 b"\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xb8\x96\xe7\x95\x8c")
    assert "你好世界".encode("utf-8") == proto.read_val(
        bs, TType.STRING, decode_response=False)


def test_strict_decode():
    bs = BytesIO(b"\x00\x00\x00\x0c\x00"  # there is a redundant '\x00'
                 b"\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xb8\x96\xe7\x95\x8c")
    with pytest.raises(UnicodeDecodeError):
        proto.read_val(bs, TType.STRING, decode_response=True,
                       strict_decode=True)


def test_write_message_begin():
    b = BytesIO()
    proto.TBinaryProtocol(b).write_message_begin("test", TType.STRING, 1)
    assert "80 01 00 0b 00 00 00 04 74 65 73 74 00 00 00 01" == \
        hexlify(b.getvalue())


def test_write_message_begin_not_strict():
    b = BytesIO()
    proto.TBinaryProtocol(b, strict_write=False) \
        .write_message_begin("test", TType.STRING, 1)
    assert "00 00 00 04 74 65 73 74 0b 00 00 00 01" == \
        hexlify(b.getvalue())


def test_read_message_begin():
    b = BytesIO(b"\x80\x01\x00\x0b\x00\x00\x00\x04test\x00\x00\x00\x01")
    res = proto.TBinaryProtocol(b).read_message_begin()
    assert res == ("test", TType.STRING, 1)


def test_read_message_begin_not_strict():
    b = BytesIO(b"\x00\x00\x00\x04test\x0b\x00\x00\x00\x01")
    res = proto.TBinaryProtocol(b, strict_read=False).read_message_begin()
    assert res == ("test", TType.STRING, 1)


def test_write_struct():
    b = BytesIO()
    item = TItem(id=123, phones=["123456", "abcdef"])
    proto.TBinaryProtocol(b).write_struct(item)
    assert ("08 00 01 00 00 00 7b 0f 00 02 0b 00 00 00 02 00 00 00 "
            "06 31 32 33 34 35 36 00 00 00 06 61 62 63 64 65 66 00") == \
        hexlify(b.getvalue())


def test_read_struct():
    b = BytesIO(b"\x08\x00\x01\x00\x00\x00{\x0f\x00\x02\x0b\x00\x00\x00"
                b"\x02\x00\x00\x00\x06123456\x00\x00\x00\x06abcdef\x00")
    _item = TItem(id=123, phones=["123456", "abcdef"])
    _item2 = TItem()
    proto.TBinaryProtocol(b).read_struct(_item2)
    assert _item == _item2


def test_write_empty_struct():
    b = BytesIO()
    item = TItem()
    proto.TBinaryProtocol(b).write_struct(item)
    assert "00" == hexlify(b.getvalue())


def test_read_empty_struct():
    b = BytesIO(b"\x00")
    _item = TItem()
    _item2 = TItem()
    proto.TBinaryProtocol(b).read_struct(_item2)
    assert _item == _item2


def test_write_huge_struct():
    b = BytesIO()
    item = TItem(id=12345, phones=["1234567890"] * 100000)
    proto.TBinaryProtocol(b).write_struct(item)


@pytest.fixture
def buffer_supports_non_contiguous():
    """Pypy 3.9 and 3.10 feature BytesIO supporting non-contiguous input data."""
    b = BytesIO()
    try:
        b.write(memoryview(b"abcd")[::-1])
    except BufferError:
        return False
    return True


def test_write_memoryview(buffer_supports_non_contiguous):
    # contiguous 8-bit items
    b = TMemoryBuffer()
    data = memoryview(b"hello world!\x01")
    proto.write_val(b, TType.BINARY, data)
    b.flush()
    assert "00 00 00 0d 68 65 6c 6c 6f 20 77 6f 72 6c 64 21 01" == \
        hexlify(b.getvalue())

    # not 8-bit items
    b = TMemoryBuffer()
    data = memoryview(b"0000111122223333").cast("h")
    proto.write_val(b, TType.BINARY, data)
    b.flush()
    assert "00 00 00 10 30 30 30 30 31 31 31 31 32 32 32 32 33 33 33 33" == \
        hexlify(b.getvalue())

    # not contiguous
    b = TMemoryBuffer()
    data = memoryview(b"0123")[::-1]
    if not buffer_supports_non_contiguous:
        with pytest.raises(BufferError, match="contiguous"):
            proto.write_val(b, TType.BINARY, data)
    else:
        proto.write_val(b, TType.BINARY, data)
        b.flush()
        assert "00 00 00 04 33 32 31 30" == \
            hexlify(b.getvalue())


def test_write_bytearray():
    b = TMemoryBuffer()
    proto.write_val(b, TType.BINARY, bytearray("hello world!", "utf-8"))
    b.flush()
    assert "00 00 00 0c 68 65 6c 6c 6f 20 77 6f 72 6c 64 21" == \
        hexlify(b.getvalue())



@pytest.mark.skipif(not _compat.CYTHON, reason="cybin required")
def test_string_binary_equivalency():
    from thriftpy2.protocol.binary import TBinaryProtocolFactory
    from thriftpy2.protocol.cybin import TCyBinaryProtocolFactory
    string_binary_equivalency(TBinaryProtocolFactory)
    string_binary_equivalency(TCyBinaryProtocolFactory)


def string_binary_equivalency(proto_factory):
    container = load(TEST_DIR / "container.thrift")
    l_item = container.ListItem()
    l_item.list_string = ['foo', 'bar']
    l_item.list_list_string = [['foo', 'bar']]

    bl_item = container.BinListItem()
    bl_item.list_binary = ['foo', 'bar']
    bl_item.list_list_binary = [['foo', 'bar']]

    assert serialize(l_item, proto_factory=proto_factory()) == serialize(
        l_item, proto_factory=proto_factory())

    m_item = container.MapItem()
    m_item.map_string = {'foo': 'bar'}
    m_item.map_map_string = {'foo': {'hello': 'world'}}

    bm_item = container.BinMapItem()
    bm_item.map_binary = {'foo': 'bar'}
    bm_item.map_map_binary = {'foo': {'hello': 'world'}}

    assert serialize(m_item, proto_factory=proto_factory()) == serialize(
        bm_item, proto_factory=proto_factory())

    x_item = container.MixItem()
    x_item.list_map = [{'foo': 'bar'}]
    x_item.map_list = {'foo': ['hello', 'world']}

    bx_item = container.BinMixItem()
    bx_item.list_map = [{'foo': 'bar'}]
    bx_item.map_list = {'foo': ['hello', 'world']}

    assert serialize(x_item, proto_factory=proto_factory()) == serialize(
        bx_item, proto_factory=proto_factory())

    l_item = container.ListItem()
    l_item.list_string = ['foo', 'bar'] * 100
    l_item.list_list_string = [['foo', 'bar']] * 100

    l_struct = container.ListStruct()
    l_struct.list_items = [l_item] * 100

    bl_item = container.BinListItem()
    bl_item.list_binary = ['foo', 'bar'] * 100
    bl_item.list_list_binary = [['foo', 'bar']] * 100

    bl_struct = container.BinListStruct()
    bl_struct.list_items = [l_item] * 100

    assert serialize(l_struct, proto_factory=proto_factory()) == serialize(
        bl_struct, proto_factory=proto_factory())
