# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""This test module tests PrimitiveType based conversions of values to/from bytes

Notes:
    Boolean:
        - Stored as 0x00 for False and non-zero byte for True
    Integer:
        - Stored as 4 bytes in little-endian order
        - 84202 is 0...01|01001000|11101010 in binary
    Long:
        - Stored as 8 bytes in little-endian order
        - 200L is 0...0|11001000 in binary
        - 11001000 -> 200 (-56), 00000000 -> 0, ... , 00000000 -> 0
    Double:
        - Stored as 8 bytes in little-endian order
        - floating point numbers are represented as sign * 2ˆexponent * mantissa
        - 6.0 is 1 * 2ˆ4 * 1.5 and encoded as 01000000|00011000|0...0
        - 00000000 -> 0, ... , 00011000 -> 24, 01000000 -> 64
    Date:
        - Stored as days from 1970-01-01 in a 4-byte little-endian int
        - 1000 is 0...0|00000011|11101000 in binary
        - 11101000 -> 232 (-24), 00000011 -> 3, ... , 00000000 -> 0
    Time:
        - Stored as microseconds from midnight in an 8-byte little-endian long
        - 10000L is 0...0|00100111|00010000 in binary
        - 00010000 -> 16, 00100111 -> 39, ... , 00000000 -> 0
    Timestamp:
        - Stored as microseconds from 1970-01-01 00:00:00.000000 in an 8-byte little-endian long
        - 400000L is 0...110|00011010|10000000 in binary
        - 10000000 -> 128 (-128), 00011010 -> 26, 00000110 -> 6, ... , 00000000 -> 0
    TimestampNano:
        - Stored as nanoseconds from 1970-01-01 00:00:00.000000000 in an 8-byte little-endian long
        - 400000000L is 00010111|11010111|10000100|00000000 in binary
        - 00000000 -> 0, 10000100 -> 124 (-124), 11010111 -> 41 (-41), 00010111 -> 23, ... , 00000000 -> 0
    String:
        - Stored as UTF-8 bytes (without length)
        - 'A' -> 65, 'B' -> 66, 'C' -> 67
    UUID:
        - Stored as 16-byte big-endian values
        - f79c3e09-677c-4bbd-a479-3f349cb785e7 is encoded as F7 9C 3E 09 67 7C 4B BD A4 79 3F 34 9C B7 85 E7
        - 0xF7 -> 11110111 -> 247 (-9), 0x9C -> 10011100 -> 156 (-100), 0x3E -> 00111110 -> 62,
        - 0x09 -> 00001001 -> 9, 0x67 -> 01100111 -> 103, 0x7C -> 01111100 -> 124,
        - 0x4B -> 01001011 -> 75, 0xBD -> 10111101 -> 189 (-67), 0xA4 -> 10100100 -> 164 (-92),
        - 0x79 -> 01111001 -> 121, 0x3F -> 00111111 -> 63, 0x34 -> 00110100 -> 52,
        - 0x9C -> 10011100 -> 156 (-100), 0xB7 -> 10110111 -> 183 (-73), 0x85 -> 10000101 -> 133 (-123),
        - 0xE7 -> 11100111 -> 231 (-25)
    Fixed:
        - Stored directly
        - 'a' -> 97, 'b' -> 98
    Binary:
        - Stored directly
        - 'Z' -> 90
    Decimal:
        - Stored as unscaled values in the form of two's-complement big-endian binary
          using the minimum number of bytes for the values
        - 345 is 0...1|01011001 in binary
        - 00000001 -> 1, 01011001 -> 89
    Float:
        - Stored as 4 bytes in little-endian order
        - floating point numbers are represented as sign * 2ˆexponent * mantissa
        - -4.5F is -1 * 2ˆ2 * 1.125 and encoded as 11000000|10010000|0...0 in binary
        - 00000000 -> 0, 00000000 -> 0, 10010000 -> 144 (-112), 11000000 -> 192 (-64),
"""

import struct
import uuid
from datetime import (
    date,
    datetime,
    time,
    timezone,
)
from decimal import Decimal
from typing import Any

import pytest

import pyiceberg.utils.decimal as decimal_util
from pyiceberg import conversions
from pyiceberg.types import (
    BinaryType,
    BooleanType,
    DateType,
    DecimalType,
    DoubleType,
    FixedType,
    FloatType,
    IntegerType,
    LongType,
    PrimitiveType,
    StringType,
    TimestampNanoType,
    TimestampType,
    TimestamptzNanoType,
    TimestamptzType,
    TimeType,
    UUIDType,
)


@pytest.mark.parametrize(
    "value, expected_result",
    [
        (Decimal("1.2345"), 12345),
        (Decimal("12.345"), 12345),
        (Decimal("1234.5"), 12345),
        (Decimal("9999999.9999"), 99999999999),
        (Decimal("1.0"), 10),
        (Decimal("1"), 1),
        (Decimal("0.1"), 1),
        (Decimal("0.12345"), 12345),
        (Decimal("0.0000001"), 1),
    ],
)
def test_decimal_to_unscaled(value: Decimal, expected_result: int) -> None:
    """Test converting a decimal to an unscaled value"""
    assert decimal_util.decimal_to_unscaled(value=value) == expected_result


@pytest.mark.parametrize(
    "unscaled, scale, expected_result",
    [
        (12345, 4, Decimal("1.2345")),
        (12345, 3, Decimal("12.345")),
        (12345, 1, Decimal("1234.5")),
        (99999999999, 4, Decimal("9999999.9999")),
        (1, 1, Decimal("0.1")),
        (1, 0, Decimal("1")),
        (12345, 5, Decimal("0.12345")),
        (1, 7, Decimal("0.0000001")),
    ],
)
def test_unscaled_to_decimal(unscaled: int, scale: int, expected_result: Decimal) -> None:
    """Test converting an unscaled value to a decimal with a specified scale"""
    assert decimal_util.unscaled_to_decimal(unscaled=unscaled, scale=scale) == expected_result


@pytest.mark.parametrize(
    "primitive_type, value_str, expected_result",
    [
        (BooleanType(), "true", True),
        (BooleanType(), "false", False),
        (BooleanType(), "TRUE", True),
        (BooleanType(), "FALSE", False),
        (IntegerType(), "1", 1),
        (IntegerType(), "9999", 9999),
        (LongType(), "123456789", 123456789),
        (FloatType(), "1.1", 1.1),
        (DoubleType(), "99999.9", 99999.9),
        (DecimalType(5, 2), "123.45", Decimal("123.45")),
        (StringType(), "foo", "foo"),
        (UUIDType(), "f79c3e09-677c-4bbd-a479-3f349cb785e7", uuid.UUID("f79c3e09-677c-4bbd-a479-3f349cb785e7")),
        (FixedType(3), "foo", b"foo"),
        (BinaryType(), "foo", b"foo"),
    ],
)
def test_partition_to_py(primitive_type: PrimitiveType, value_str: str, expected_result: Any) -> None:
    """Test converting a partition value to a python built-in"""
    assert conversions.partition_to_py(primitive_type, value_str) == expected_result


@pytest.mark.parametrize(
    "primitive_type",
    [
        (BinaryType()),
        (BooleanType()),
        (DateType()),
        (DecimalType(2, 1)),
        (DoubleType()),
        (FixedType(1)),
        (FloatType()),
        (IntegerType()),
        (LongType()),
        (StringType()),
        (TimestampType()),
        (TimestamptzType()),
        (TimeType()),
        (UUIDType()),
    ],
)
def test_none_partition_values(primitive_type: PrimitiveType) -> None:
    """Test converting a partition value to a python built-in"""
    assert conversions.partition_to_py(primitive_type, None) is None  # type: ignore


@pytest.mark.parametrize(
    "primitive_type",
    [
        (BinaryType()),
        (BooleanType()),
        (DateType()),
        (DecimalType(2, 1)),
        (DoubleType()),
        (FixedType(1)),
        (FloatType()),
        (IntegerType()),
        (LongType()),
        (StringType()),
        (TimestampType()),
        (TimestamptzType()),
        (TimeType()),
        (UUIDType()),
    ],
)
def test_hive_default_partition_values(primitive_type: PrimitiveType) -> None:
    """Test converting a partition value to a python built-in"""
    assert conversions.partition_to_py(primitive_type, "__HIVE_DEFAULT_PARTITION__") is None


@pytest.mark.parametrize(
    "primitive_type, value, should_raise",
    [
        (IntegerType(), "123.45", True),
        (IntegerType(), "1234567.89", True),
        (IntegerType(), "123.00", True),
        (IntegerType(), "1234567.00", True),
        (LongType(), "123.45", True),
        (LongType(), "1234567.89", True),
        (LongType(), "123.00", True),
        (LongType(), "1234567.00", True),
        (IntegerType(), "12345", False),
        (IntegerType(), "123456789", False),
        (IntegerType(), "12300", False),
        (IntegerType(), "123456700", False),
        (LongType(), "12345", False),
        (LongType(), "123456789", False),
        (LongType(), "12300", False),
        (LongType(), "123456700", False),
    ],
)
def test_partition_to_py_raise_on_incorrect_precision_or_scale(
    primitive_type: PrimitiveType, value: str, should_raise: bool
) -> None:
    if should_raise:
        with pytest.raises(ValueError) as exc_info:
            conversions.partition_to_py(primitive_type, value)

        assert f"Cannot convert partition value, value cannot have fractional digits for {primitive_type} partition" in str(
            exc_info.value
        )
    else:
        conversions.partition_to_py(primitive_type, value)


@pytest.mark.parametrize(
    "primitive_type, b, result",
    [
        (BooleanType(), b"\x00", False),
        (BooleanType(), b"\x01", True),
        (IntegerType(), b"\xd2\x04\x00\x00", 1234),
        (LongType(), b"\xd2\x04\x00\x00\x00\x00\x00\x00", 1234),
        (DoubleType(), b"\x8d\x97\x6e\x12\x83\xc0\xf3\x3f", 1.2345),
        (DateType(), b"\xe8\x03\x00\x00", 1000),
        (DateType(), b"\xd2\x04\x00\x00", 1234),
        (TimeType(), b"\x10'\x00\x00\x00\x00\x00\x00", 10000),
        (TimeType(), b"\x00\xe8vH\x17\x00\x00\x00", 100000000000),
        (TimestamptzType(), b"\x80\x1a\x06\x00\x00\x00\x00\x00", 400000),
        (TimestamptzType(), b"\x00\xe8vH\x17\x00\x00\x00", 100000000000),
        (TimestampType(), b"\x80\x1a\x06\x00\x00\x00\x00\x00", 400000),
        (TimestampType(), b"\x00\xe8vH\x17\x00\x00\x00", 100000000000),
        (TimestampNanoType(), b"\00\x84\xd7\x17\x00\x00\x00\x00", 400000000),
        (TimestamptzNanoType(), b"\00\x84\xd7\x17\x00\x00\x00\x00", 400000000),
        (StringType(), b"ABC", "ABC"),
        (StringType(), b"foo", "foo"),
        (
            UUIDType(),
            b"\xf7\x9c>\tg|K\xbd\xa4y?4\x9c\xb7\x85\xe7",
            b"\xf7\x9c>\tg|K\xbd\xa4y?4\x9c\xb7\x85\xe7",
        ),
        (UUIDType(), b"\xf7\x9c>\tg|K\xbd\xa4y?4\x9c\xb7\x85\xe7", b"\xf7\x9c>\tg|K\xbd\xa4y?4\x9c\xb7\x85\xe7"),
        (FixedType(3), b"foo", b"foo"),
        (BinaryType(), b"foo", b"foo"),
        (DecimalType(5, 2), b"\x30\x39", Decimal("123.45")),
        (DecimalType(7, 4), b"\x12\xd6\x87", Decimal("123.4567")),
        (DecimalType(7, 4), b"\xff\xed\x29\x79", Decimal("-123.4567")),
    ],
)
def test_from_bytes(primitive_type: PrimitiveType, b: bytes, result: Any) -> None:
    """Test converting from bytes"""
    assert conversions.from_bytes(primitive_type, b) == result


@pytest.mark.parametrize(
    "primitive_type, b, result",
    [
        (BooleanType(), b"\x00", False),
        (BooleanType(), b"\x01", True),
        (IntegerType(), b"\xeaH\x01\x00", 84202),
        (IntegerType(), b"\xd2\x04\x00\x00", 1234),
        (LongType(), b"\xc8\x00\x00\x00\x00\x00\x00\x00", 200),
        (LongType(), b"\xd2\x04\x00\x00\x00\x00\x00\x00", 1234),
        (DoubleType(), b"\x00\x00\x00\x00\x00\x00\x18@", 6.0),
        (DoubleType(), b"\x8d\x97n\x12\x83\xc0\xf3?", 1.2345),
        (DateType(), b"\xe8\x03\x00\x00", 1000),
        (DateType(), b"\xd2\x04\x00\x00", 1234),
        (TimeType(), b"\x10'\x00\x00\x00\x00\x00\x00", 10000),
        (TimeType(), b"\x00\xe8vH\x17\x00\x00\x00", 100000000000),
        (TimestamptzType(), b"\x80\x1a\x06\x00\x00\x00\x00\x00", 400000),
        (TimestamptzType(), b"\x00\xe8vH\x17\x00\x00\x00", 100000000000),
        (TimestampType(), b"\x00\xe8vH\x17\x00\x00\x00", 100000000000),
        (StringType(), b"ABC", "ABC"),
        (StringType(), b"foo", "foo"),
        (
            UUIDType(),
            b"\xf7\x9c>\tg|K\xbd\xa4y?4\x9c\xb7\x85\xe7",
            b"\xf7\x9c>\tg|K\xbd\xa4y?4\x9c\xb7\x85\xe7",
        ),
        (UUIDType(), b"\xf7\x9c>\tg|K\xbd\xa4y?4\x9c\xb7\x85\xe7", b"\xf7\x9c>\tg|K\xbd\xa4y?4\x9c\xb7\x85\xe7"),
        (FixedType(3), b"foo", b"foo"),
        (BinaryType(), b"foo", b"foo"),
        (DecimalType(5, 2), b"\x30\x39", Decimal("123.45")),
        (DecimalType(3, 2), b"\x01Y", Decimal("3.45")),
        # decimal on 3-bytes to test that we use the minimum number of bytes and not a power of 2
        # 1234567 is 00010010|11010110|10000111 in binary
        # 00010010 -> 18, 11010110 -> 214, 10000111 -> 135
        (DecimalType(7, 4), b"\x12\xd6\x87", Decimal("123.4567")),
        # negative decimal to test two's complement
        # -1234567 is 11101101|00101001|01111001 in binary
        # 11101101 -> 237, 00101001 -> 41, 01111001 -> 121
        (DecimalType(7, 4), b"\xed)y", Decimal("-123.4567")),
        # test empty byte in decimal
        # 11 is 00001011 in binary
        # 00001011 -> 11
        (DecimalType(10, 3), b"\x0b", Decimal("0.011")),
        (DecimalType(4, 2), b"\x04\xd2", Decimal("12.34")),
        (FloatType(), b"\x00\x00\x90\xc0", struct.unpack("<f", struct.pack("<f", -4.5))[0]),
        (FloatType(), b"\x19\x04\x9e?", struct.unpack("<f", struct.pack("<f", 1.2345))[0]),
    ],
)
def test_round_trip_conversion(primitive_type: PrimitiveType, b: bytes, result: Any) -> None:
    """Test round trip conversions of calling `conversions.from_bytes` and then `conversions.to_bytes` on the result"""
    value_from_bytes = conversions.from_bytes(primitive_type, b)
    assert value_from_bytes == result

    bytes_from_value = conversions.to_bytes(primitive_type, value_from_bytes)
    assert bytes_from_value == b


@pytest.mark.parametrize(
    "primitive_type, v, result",
    [
        (
            UUIDType(),
            uuid.UUID("f79c3e09-677c-4bbd-a479-3f349cb785e7"),
            b"\xf7\x9c>\tg|K\xbd\xa4y?4\x9c\xb7\x85\xe7",
        ),
        (UUIDType(), uuid.UUID("f79c3e09-677c-4bbd-a479-3f349cb785e7"), b"\xf7\x9c>\tg|K\xbd\xa4y?4\x9c\xb7\x85\xe7"),
    ],
)
def test_uuid_to_bytes(primitive_type: PrimitiveType, v: Any, result: bytes) -> None:
    bytes_from_value = conversions.to_bytes(primitive_type, v)
    assert bytes_from_value == result


@pytest.mark.parametrize(
    "primitive_type, b, result",
    [
        (
            DecimalType(38, 21),
            b"\tI\xb0\xf7\x13\xe9\x180s\xb9\x1e~\xa2\xb3j\x83",
            Decimal("12345678912345678.123456789123456789123"),
        ),
        (DecimalType(38, 22), b'\tI\xb0\xf7\x13\xe9\x16\xbb\x01/L\xc3+B)"', Decimal("1234567891234567.1234567891234567891234")),
        (
            DecimalType(38, 23),
            b"\tI\xb0\xf7\x13\xe9\nB\xa1\xad\xe5+3\x15\x9bY",
            Decimal("123456789123456.12345678912345678912345"),
        ),
        (
            DecimalType(38, 24),
            b"\tI\xb0\xf7\x13\xe8\xa2\xbb\xe9g\xba\x86w\xd8\x11\x80",
            Decimal("12345678912345.123456789123456789123456"),
        ),
        (
            DecimalType(38, 25),
            b"\tI\xb0\xf7\x13\xe5k:\xd2x\xdd\x04\xc8p\xaf\x07",
            Decimal("1234567891234.1234567891234567891234567"),
        ),
        (DecimalType(38, 26), b"\tI\xb0\xf7\x13\xcd\x85\xc5\x0387<8f\xd6N", Decimal("123456789123.12345678912345678912345678")),
        (DecimalType(38, 27), b"\tI\xb0\xf7\x131F\xfd\xc7y\xca9|\x04_\x15", Decimal("12345678912.123456789123456789123456789")),
        (
            DecimalType(38, 28),
            b"\tI\xb0\xf7\x10R\x01r\x11\xda\x08[\x08+\xb6\xd3",
            Decimal("1234567891.1234567891234567891234567891"),
        ),
        (
            DecimalType(38, 29),
            b"\tI\xb0\xf7\x13\xe9\x18[7\xc1x\x0b\x91\xb5$@",
            Decimal("123456789.12345678912345678912345678912"),
        ),
        (
            DecimalType(38, 30),
            b"\tI\xb0\xed\x1e\xdf\x80\x03G;\x16\x9b\xf1\x13j\x83",
            Decimal("12345678.123456789123456789123456789123"),
        ),
        (DecimalType(38, 31), b'\tI\xb0\x96+\xac)d(p6)\xea\xc2)"', Decimal("1234567.1234567891234567891234567891234")),
        (
            DecimalType(38, 32),
            b"\tI\xad\xae\xe3h\xe7O\xb5\x14\xbc\xdc+\x95\x9bY",
            Decimal("123456.12345678912345678912345678912345"),
        ),
        (
            DecimalType(38, 33),
            b"\tI\x95\x94>5\x93\xde\xb9.\xefS\xb3\xd8\x11\x80",
            Decimal("12345.123456789123456789123456789123456"),
        ),
        (
            DecimalType(38, 34),
            b"\tH\xd5\xd7\x90x\xdf\x08\x1a\xf6C\t\x06p\xaf\x07",
            Decimal("1234.1234567891234567891234567891234567"),
        ),
        (DecimalType(38, 35), b"\tCE\x82\x85\xc7Vf$M\x16\x82@f\xd6N", Decimal("123.12345678912345678912345678912345678")),
        (DecimalType(21, 16), b"\x06\xb1:\xe3\xc4N\x94\xaf\x07", Decimal("12345.1234567891234567")),
        (DecimalType(22, 17), b"B\xecL\xe5\xab\x11\xce\xd6N", Decimal("12345.12345678912345678")),
        (DecimalType(23, 18), b"\x02\x9d;\x00\xf8\xae\xb2\x14_\x15", Decimal("12345.123456789123456789")),
        (DecimalType(24, 19), b"\x1a$N\t\xb6\xd2\xf4\xcb\xb6\xd3", Decimal("12345.1234567891234567891")),
        (DecimalType(25, 20), b"\x01\x05k\x0ca$=\x8f\xf5$@", Decimal("12345.12345678912345678912")),
        (DecimalType(26, 21), b"\n6.{\xcbjg\x9f\x93j\x83", Decimal("12345.123456789123456789123")),
        (DecimalType(27, 22), b'f\x1d\xd0\xd5\xf2(\x0c;\xc2)"', Decimal("12345.1234567891234567891234")),
        (DecimalType(28, 23), b"\x03\xfd*([u\x90zU\x95\x9bY", Decimal("12345.12345678912345678912345")),
        (DecimalType(29, 24), b"'\xe3\xa5\x93\x92\x97\xa4\xc7W\xd8\x11\x80", Decimal("12345.123456789123456789123456")),
        (DecimalType(30, 25), b"\x01\x8e\xe4w\xc3\xb9\xeco\xc9np\xaf\x07", Decimal("12345.1234567891234567891234567")),
        (DecimalType(31, 26), b"\x0f\x94\xec\xad\xa5C<]\xdePf\xd6N", Decimal("12345.12345678912345678912345678")),
    ],
)
def test_round_trip_conversion_large_decimals(primitive_type: PrimitiveType, b: bytes, result: Any) -> None:
    """Test round trip conversions of calling `conversions.from_bytes` and then `conversions.to_bytes` on the result"""
    value_from_bytes = conversions.from_bytes(primitive_type, b)
    assert value_from_bytes == result

    bytes_from_value = conversions.to_bytes(primitive_type, value_from_bytes)
    assert bytes_from_value == b


@pytest.mark.parametrize(
    "primitive_type, expected_max_value",
    [
        (DecimalType(6, 2), Decimal("9999.99")),
        (DecimalType(10, 10), Decimal(".9999999999")),
        (DecimalType(2, 1), Decimal("9.9")),
        (DecimalType(38, 37), Decimal("9.9999999999999999999999999999999999999")),
        (DecimalType(20, 1), Decimal("9999999999999999999.9")),
    ],
)
def test_max_value_round_trip_conversion(primitive_type: DecimalType, expected_max_value: Decimal) -> None:
    """Test round trip conversions of maximum DecimalType values"""
    b = conversions.to_bytes(primitive_type, expected_max_value)
    value_from_bytes = conversions.from_bytes(primitive_type, b)

    assert value_from_bytes == expected_max_value


@pytest.mark.parametrize(
    "primitive_type, expected_min_value",
    [
        (DecimalType(6, 2), Decimal("-9999.99")),
        (DecimalType(10, 10), Decimal("-.9999999999")),
        (DecimalType(2, 1), Decimal("-9.9")),
        (DecimalType(38, 37), Decimal("-9.9999999999999999999999999999999999999")),
        (DecimalType(20, 1), Decimal("-9999999999999999999.9")),
    ],
)
def test_min_value_round_trip_conversion(primitive_type: DecimalType, expected_min_value: Decimal) -> None:
    """Test round trip conversions of minimum DecimalType values"""
    b = conversions.to_bytes(primitive_type, expected_min_value)
    value_from_bytes = conversions.from_bytes(primitive_type, b)

    assert value_from_bytes == expected_min_value


def test_raise_on_unregistered_type() -> None:
    """Test raising when a conversion is attempted for a type that has no registered method"""

    class FooUnknownType:
        def __repr__(self) -> str:
            return "FooUnknownType()"

    with pytest.raises(TypeError) as exc_info:
        conversions.partition_to_py(FooUnknownType(), "foo")  # type: ignore
    assert "Cannot convert 'foo' to unsupported type: FooUnknownType()" in str(exc_info.value)

    with pytest.raises(TypeError) as exc_info:
        conversions.to_bytes(FooUnknownType(), "foo")  # type: ignore
    assert "scale does not match FooUnknownType()" in str(exc_info.value)

    with pytest.raises(TypeError) as exc_info:
        conversions.from_bytes(FooUnknownType(), b"foo")  # type: ignore
    assert "Cannot deserialize bytes, type FooUnknownType() not supported: b'foo'" in str(exc_info.value)


@pytest.mark.parametrize(
    "primitive_type, value, expected_error_message",
    [
        (DecimalType(7, 3), Decimal("123.4567"), "Cannot serialize value, scale of value does not match type decimal(7, 3): 4"),
        (
            DecimalType(18, 8),
            Decimal("123456789.123456789"),
            "Cannot serialize value, scale of value does not match type decimal(18, 8): 9",
        ),
        (
            DecimalType(36, 34),
            Decimal("1.23456789123456789123456789123456789"),
            "Cannot serialize value, scale of value does not match type decimal(36, 34): 35",
        ),
        (
            DecimalType(7, 2),
            Decimal("1234567.89"),
            "Cannot serialize value, precision of value is greater than precision of type decimal(7, 2): 9",
        ),
        (
            DecimalType(17, 9),
            Decimal("123456789.123456789"),
            "Cannot serialize value, precision of value is greater than precision of type decimal(17, 9): 18",
        ),
        (
            DecimalType(35, 35),
            Decimal("1.23456789123456789123456789123456789"),
            "Cannot serialize value, precision of value is greater than precision of type decimal(35, 35): 36",
        ),
    ],
)
def test_raise_on_incorrect_precision_or_scale(primitive_type: DecimalType, value: Decimal, expected_error_message: str) -> None:
    with pytest.raises(ValueError) as exc_info:
        conversions.to_bytes(primitive_type, value)

    assert expected_error_message in str(exc_info.value)


@pytest.mark.parametrize(
    "primitive_type, value, expected_bytes",
    [
        (TimestampType(), datetime(2023, 3, 1, 19, 25, 0), b"\x00\xbb\r\xab\xdb\xf5\x05\x00"),
        (TimestamptzType(), datetime(2023, 3, 1, 19, 25, 0, tzinfo=timezone.utc), b"\x00\xbb\r\xab\xdb\xf5\x05\x00"),
        (DateType(), date(2023, 3, 1), b"\xd9K\x00\x00"),
        (TimeType(), time(12, 30, 45, 500000), b"`\xc8\xeb|\n\x00\x00\x00"),
    ],
)
def test_datetime_obj_to_bytes(primitive_type: PrimitiveType, value: datetime | date | time, expected_bytes: bytes) -> None:
    bytes_from_value = conversions.to_bytes(primitive_type, value)

    assert bytes_from_value == expected_bytes


@pytest.mark.parametrize(
    "primitive_type, value, expected",
    [
        (BooleanType(), True, True),
        (IntegerType(), 34, 34),
        (LongType(), 34, 34),
        (FloatType(), 1.0, 1.0),
        (DoubleType(), 1.0, 1.0),
        (DecimalType(9, 4), Decimal("123.4500"), "123.4500"),
        (DecimalType(9, 0), Decimal("2"), "2"),
        (DecimalType(9, -20), Decimal("2E+20"), "2E+20"),
        (DateType(), date(2017, 11, 16), "2017-11-16"),
        (TimeType(), time(22, 31, 8, 123456), "22:31:08.123456"),
        (TimestampType(), datetime(2017, 11, 16, 22, 31, 8, 123456), "2017-11-16T22:31:08.123456"),
        (TimestamptzType(), datetime(2017, 11, 16, 22, 31, 8, 123456, tzinfo=timezone.utc), "2017-11-16T22:31:08.123456+00:00"),
        (StringType(), "iceberg", "iceberg"),
        (BinaryType(), b"\x01\x02\x03\xff", "010203ff"),
        (FixedType(4), b"\x01\x02\x03\xff", "010203ff"),
    ],
)
def test_json_single_serialization(primitive_type: PrimitiveType, value: Any, expected: Any) -> None:
    json_val = conversions.to_json(primitive_type, value)
    assert json_val == expected


@pytest.mark.parametrize(
    "primitive_type, value",
    [
        (BooleanType(), True),
        (IntegerType(), 34),
        (LongType(), 34),
        (FloatType(), 1.0),
        (DoubleType(), 1.0),
        (DecimalType(9, 4), Decimal("123.4500")),
        (DecimalType(9, 0), Decimal("2")),
        (DecimalType(9, -20), Decimal("2E+20")),
        (DateType(), date(2017, 11, 16)),
        (TimeType(), time(22, 31, 8, 123456)),
        (TimestampType(), datetime(2017, 11, 16, 22, 31, 8, 123456)),
        (TimestamptzType(), datetime(2017, 11, 16, 22, 31, 8, 123456, tzinfo=timezone.utc)),
        (StringType(), "iceberg"),
        (BinaryType(), b"\x01\x02\x03\xff"),
        (FixedType(4), b"\x01\x02\x03\xff"),
    ],
)
def test_json_serialize_roundtrip(primitive_type: PrimitiveType, value: Any) -> None:
    assert value == conversions.from_json(primitive_type, conversions.to_json(primitive_type, value))
