<?php

/*
 * 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.
 *
 */

declare(strict_types=1);

namespace Test\Thrift\Unit\Lib\Protocol;

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\Attributes\DataProvider;
use Thrift\Exception\TProtocolException;
use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TTransport;
use Thrift\Type\TType;

class TBinaryProtocolTest extends TestCase
{
    private const VERSION_MASK = 0xffff0000;
    private const VERSION_1 = 0x80010000;

    #[DataProvider('writeMessageBeginDataProvider')]
    public function testWriteMessageBegin(
        $strictWrite,
        $name,
        $type,
        $seqid,
        $writeCallsParams,
        $writeCallsResults,
        $expectedResult
    ) {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, $strictWrite);

        $transport->expects($this->exactly(count($writeCallsParams)))
                  ->method('write')
                  ->willReturnCallback(function (...$callArgs) use ($writeCallsParams, $writeCallsResults) {
                      static $iteration = 0;
                      $expected = $writeCallsParams[$iteration];
                    foreach ($expected as $i => $exp) {
                        if ($exp instanceof Constraint) {
                            $this->assertThat($callArgs[$i], $exp);
                        } else {
                            $this->assertSame($exp, $callArgs[$i]);
                        }
                    }

                      return $writeCallsResults[$iteration++];
                  });

        $result = $protocol->writeMessageBegin($name, $type, $seqid);
        $this->assertEquals($expectedResult, $result);
    }

    public static function writeMessageBeginDataProvider()
    {
        $type = TType::STRING;
        $seqid = 555;

        yield 'strictWrite=true' => [
            'strictWrite' => true,
            'name' => 'testName',
            'type' => $type,
            'seqid' => $seqid,
            'writeCallsParams' => [
                [pack('N', self::VERSION_1 | $type)], #writeI32
                [pack('N', strlen('testName'))], #writeStringLen
                ['testName'], #writeString
                [pack('N', $seqid)], #writeI32
            ],
            'writeCallsResults' => [
                4,
                4,
                8,
                4,
            ],
            'expectedResult' => 20,
        ];

        yield 'strictWrite=false' => [
            'strictWrite' => false,
            'name' => 'testName',
            'type' => $type,
            'seqid' => $seqid,
            'writeCallsParams' => [
                [pack('N', strlen('testName'))], #writeStringLen
                ['testName'], #writeString
                [pack('c', $type)], #writeByte
                [pack('N', $seqid)], #writeI32
            ],
            'writeCallsResults' => [
                4,
                8,
                1,
                4,
            ],
            'expectedResult' => 17,
        ];
    }

    public function testWriteMessageEnd()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->writeMessageEnd());
    }

    public function testWriteStructBegin()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->writeStructBegin('testName'));
    }

    public function testWriteStructEnd()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->writeStructEnd());
    }

    public function testWriteFieldBegin()
    {
        $fieldName = 'testName';
        $fieldType = TType::STRING;
        $fieldId = 555;

        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $expectedWriteArgs = [
            [pack('c', $fieldType)], #writeByte
            [pack('n', $fieldId)], #writeI16
        ];
        $writeReturns = [1, 2];
        $transport
            ->expects($this->exactly(2))
            ->method('write')
            ->willReturnCallback(function (...$callArgs) use ($expectedWriteArgs, $writeReturns) {
                static $iteration = 0;
                $expected = $expectedWriteArgs[$iteration];
                foreach ($expected as $i => $exp) {
                    if ($exp instanceof Constraint) {
                        $this->assertThat($callArgs[$i], $exp);
                    } else {
                        $this->assertSame($exp, $callArgs[$i]);
                    }
                }

                return $writeReturns[$iteration++];
            });

        $this->assertEquals(3, $protocol->writeFieldBegin($fieldName, $fieldType, $fieldId));
    }

    public function testWriteFieldEnd()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->writeFieldEnd());
    }

    public function testWriteFieldStop()
    {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('write')
            ->with(pack('c', TType::STOP)); #writeByte

        $this->assertEquals(1, $protocol->writeFieldStop());
    }

    public function testWriteMapBegin()
    {
        $keyType = TType::I32;
        $valType = TType::STRING;
        $size = 99;

        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $expectedWriteArgs = [
            [pack('c', $keyType)], #writeByte
            [pack('c', $valType)], #writeByte
            [pack('N', $size)], #writeI32
        ];
        $writeReturns = [1, 1, 4];
        $transport
            ->expects($this->exactly(3))
            ->method('write')
            ->willReturnCallback(function (...$callArgs) use ($expectedWriteArgs, $writeReturns) {
                static $iteration = 0;
                $expected = $expectedWriteArgs[$iteration];
                foreach ($expected as $i => $exp) {
                    if ($exp instanceof Constraint) {
                        $this->assertThat($callArgs[$i], $exp);
                    } else {
                        $this->assertSame($exp, $callArgs[$i]);
                    }
                }

                return $writeReturns[$iteration++];
            });

        $this->assertEquals(6, $protocol->writeMapBegin($keyType, $valType, $size));
    }

    public function testWriteMapEnd()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->writeMapEnd());
    }

    public function testWriteListBegin()
    {
        $elemType = TType::I32;
        $size = 99;

        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $expectedWriteArgs = [
            [pack('c', $elemType)], #writeByte
            [pack('N', $size)], #writeI32
        ];
        $writeReturns = [1, 4];
        $transport
            ->expects($this->exactly(2))
            ->method('write')
            ->willReturnCallback(function (...$callArgs) use ($expectedWriteArgs, $writeReturns) {
                static $iteration = 0;
                $expected = $expectedWriteArgs[$iteration];
                foreach ($expected as $i => $exp) {
                    if ($exp instanceof Constraint) {
                        $this->assertThat($callArgs[$i], $exp);
                    } else {
                        $this->assertSame($exp, $callArgs[$i]);
                    }
                }

                return $writeReturns[$iteration++];
            });

        $this->assertEquals(5, $protocol->writeListBegin($elemType, $size));
    }

    public function testWriteListEnd()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->writeListEnd());
    }

    public function testWriteSetBegin()
    {
        $elemType = TType::I32;
        $size = 99;

        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $expectedWriteArgs = [
            [pack('c', $elemType)], #writeByte
            [pack('N', $size)], #writeI32
        ];
        $writeReturns = [1, 4];
        $transport
            ->expects($this->exactly(2))
            ->method('write')
            ->willReturnCallback(function (...$callArgs) use ($expectedWriteArgs, $writeReturns) {
                static $iteration = 0;
                $expected = $expectedWriteArgs[$iteration];
                foreach ($expected as $i => $exp) {
                    if ($exp instanceof Constraint) {
                        $this->assertThat($callArgs[$i], $exp);
                    } else {
                        $this->assertSame($exp, $callArgs[$i]);
                    }
                }

                return $writeReturns[$iteration++];
            });

        $this->assertEquals(5, $protocol->writeSetBegin($elemType, $size));
    }

    public function testWriteSetEnd()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->writeSetEnd());
    }

    public function testWriteBool()
    {
        $value = true;
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('write')
            ->with(pack('c', (int)$value)); #writeByte

        $this->assertEquals(1, $protocol->writeBool($value));
    }

    public function testWriteByte()
    {
        $value = 1;
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('write')
            ->with(pack('c', $value)); #writeByte

        $this->assertEquals(1, $protocol->writeByte($value));
    }

    public function testWriteI16()
    {
        $value = 1;
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('write')
            ->with(pack('n', $value)); #writeI16

        $this->assertEquals(2, $protocol->writeI16($value));
    }

    public function testWriteI32()
    {
        $value = 1;
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('write')
            ->with(pack('N', $value)); #writeI32

        $this->assertEquals(4, $protocol->writeI32($value));
    }

    public function testWriteI64For32BitArchitecture()
    {
        if (PHP_INT_SIZE !== 4) {
            $this->markTestSkipped('Test is only for 32 bit architecture');
        }
        $value = 1;
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $neg = $value < 0;

        if ($neg) {
            $value *= -1;
        }

        $hi = (int)($value / 4294967296);
        $lo = (int)$value;

        if ($neg) {
            $hi = ~$hi;
            $lo = ~$lo;
            if (($lo & (int)0xffffffff) == (int)0xffffffff) {
                $lo = 0;
                $hi++;
            } else {
                $lo++;
            }
        }

        $transport
            ->expects($this->once())
            ->method('write')
            ->with(pack('N2', $hi, $lo)); #writeI64

        $this->assertEquals(8, $protocol->writeI64($value));
    }

    public function testWriteI64For64BitArchitecture()
    {
        if (PHP_INT_SIZE == 4) {
            $this->markTestSkipped('Test is only for 64 bit architecture');
        }
        $value = 1;
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $hi = $value >> 32;
        $lo = $value & 0xFFFFFFFF;

        $transport
            ->expects($this->once())
            ->method('write')
            ->with(pack('N2', $hi, $lo)); #writeI64

        $this->assertEquals(8, $protocol->writeI64($value));
    }

    public function testWriteDouble()
    {
        $value = 1;
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('write')
            ->with(strrev(pack('d', $value))); #writeDouble

        $this->assertEquals(8, $protocol->writeDouble($value));
    }

    public function testWriteString()
    {
        $value = 'string';
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $expectedWriteArgs = [
            [pack('N', strlen($value))], #writeI32,
            [$value], #write,
        ];
        $writeReturns = [4, 6];
        $transport
            ->expects($this->exactly(2))
            ->method('write')
            ->willReturnCallback(function (...$callArgs) use ($expectedWriteArgs, $writeReturns) {
                static $iteration = 0;
                $expected = $expectedWriteArgs[$iteration];
                foreach ($expected as $i => $exp) {
                    if ($exp instanceof Constraint) {
                        $this->assertThat($callArgs[$i], $exp);
                    } else {
                        $this->assertSame($exp, $callArgs[$i]);
                    }
                }

                return $writeReturns[$iteration++];
            });

        $this->assertEquals(10, $protocol->writeString($value));
    }

    public function testWriteUuid()
    {
        $uuid = '01234567-89ab-cdef-0123-456789abcdef';
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('write')
            ->with(hex2bin('0123456789abcdef0123456789abcdef'));

        $this->assertEquals(16, $protocol->writeUuid($uuid));
    }

    public function testReadUuid()
    {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('readAll')
            ->with(16)
            ->willReturn(hex2bin('0123456789abcdef0123456789abcdef'));

        $this->assertEquals(16, $protocol->readUuid($value));
        $this->assertEquals('01234567-89ab-cdef-0123-456789abcdef', $value);
    }

    #[DataProvider('readMessageBeginDataProvider')]
    public function testReadMessageBegin(
        $strictRead,
        $readCallsParams,
        $readCallsResults,
        $expectedReadLengthResult,
        $expectedName,
        $expectedType,
        $expectedSeqid,
        $expectedException = null,
        $expectedExceptionMessage = null,
        $expectedExceptionCode = null
    ) {
        if ($expectedException) {
            $this->expectException($expectedException);
            $this->expectExceptionMessage($expectedExceptionMessage);
            $this->expectExceptionCode($expectedExceptionCode);
        }

        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, $strictRead, true);

        $transport->expects($this->exactly(count($readCallsParams)))
                  ->method('readAll')
                  ->willReturnCallback(function (...$callArgs) use ($readCallsParams, $readCallsResults) {
                      static $iteration = 0;
                      $expected = $readCallsParams[$iteration];
                    foreach ($expected as $i => $exp) {
                        if ($exp instanceof Constraint) {
                            $this->assertThat($callArgs[$i], $exp);
                        } else {
                            $this->assertSame($exp, $callArgs[$i]);
                        }
                    }

                      return $readCallsResults[$iteration++];
                  });

        $result = $protocol->readMessageBegin($name, $type, $seqid);
        $this->assertEquals($expectedReadLengthResult, $result);
        $this->assertEquals($expectedName, $name);
        $this->assertEquals($expectedType, $type);
        $this->assertEquals($expectedSeqid, $seqid);
    }

    public static function readMessageBeginDataProvider()
    {
        yield 'strictRead=true' => [
            'strictRead' => true,
            'readCallsParams' => [
                [4], #readI32
                [4], #readStringLen
                [8], #readString
                [4], #readI32
            ],
            'readCallsResults' => [
                pack('N', 0x80010000 | TType::STRING), #version
                pack('N', strlen('testName')),
                'testName',
                pack('N', 555),
            ],
            'expectedReadLengthResult' => 20,
            'expectedName' => 'testName',
            'expectedType' => TType::STRING,
            'expectedSeqid' => 555,
        ];

        yield 'incorrect version' => [
            'strictRead' => true,
            'readCallsParams' => [
                [4], #readI32
            ],
            'readCallsResults' => [
                pack('N', 0x80000000 | TType::STRING), #version
            ],
            'expectedReadLengthResult' => 4,
            'expectedName' => '',
            'expectedType' => 0,
            'expectedSeqid' => 0,
            'expectedException' => TProtocolException::class,
            'expectedExceptionMessage' => 'Bad version identifier: -2147483637',
            'expectedExceptionCode' => TProtocolException::BAD_VERSION,
        ];

        yield 'strictRead=false' => [
            'strictRead' => false,
            'readCallsParams' => [
                [4], #readStringLen
                [8], #readString
                [1], #readByte
                [4], #readI32
            ],
            'readCallsResults' => [
                pack('N', strlen('testName')),
                'testName',
                pack('c', TType::STRING),
                pack('N', 555),
            ],
            'expectedReadLengthResult' => 17,
            'expectedName' => 'testName',
            'expectedType' => TType::STRING,
            'expectedSeqid' => 555,
        ];

        yield 'strictRead=true without version' => [
            'strictRead' => true,
            'readCallsParams' => [
                [4], #readStringLen
            ],
            'readCallsResults' => [
                pack('N', strlen('testName')),
            ],
            'expectedReadLengthResult' => 17,
            'expectedName' => 'testName',
            'expectedType' => TType::STRING,
            'expectedSeqid' => 555,
            'expectedException' => TProtocolException::class,
            'expectedExceptionMessage' => 'No version identifier, old protocol client?',
            'expectedExceptionCode' => TProtocolException::BAD_VERSION,
        ];
    }

    public function testReadMessageEnd()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->readMessageEnd());
    }

    public function testReadStructBegin()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->readStructBegin($name));
        $this->assertEquals('', $name);
    }

    public function testReadStructEnd()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->readStructEnd());
    }

    #[DataProvider('readFieldBeginDataProvider')]
    public function testReadFieldBegin(
        $storedFieldType,
        $readCallsParams,
        $readCallsResults,
        $expectedResult,
        $expectedName,
        $expectedFieldType,
        $expectedFieldId
    ) {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->exactly(count($readCallsParams)))
            ->method('readAll')
            ->willReturnCallback(function (...$callArgs) use ($readCallsParams, $readCallsResults) {
                static $iteration = 0;
                $expected = $readCallsParams[$iteration];
                foreach ($expected as $i => $exp) {
                    if ($exp instanceof Constraint) {
                        $this->assertThat($callArgs[$i], $exp);
                    } else {
                        $this->assertSame($exp, $callArgs[$i]);
                    }
                }

                return $readCallsResults[$iteration++];
            });

        $this->assertEquals($expectedResult, $protocol->readFieldBegin($name, $fieldType, $fieldId));
        $this->assertEquals($expectedName, $name);
        $this->assertEquals($expectedFieldType, $fieldType);
        $this->assertEquals($expectedFieldId, $fieldId);
    }

    public static function readFieldBeginDataProvider()
    {
        yield 'default' => [
            'storedFieldType' => TType::STRING,
            'readCallsParams' => [
                [1], #readByte
                [2], #readI16
            ],
            'readCallsResults' => [
                pack('c', TType::STRING),
                pack('n', 555),
            ],
            'expectedResult' => 3,
            'expectedName' => '',
            'expectedFieldType' => TType::STRING,
            'expectedFieldId' => 555,
        ];

        yield 'stop field' => [
            'storedFieldType' => TType::STOP,
            'readCallsParams' => [
                [1], #readByte
            ],
            'readCallsResults' => [
                pack('c', TType::STOP),
            ],
            'expectedResult' => 1,
            'expectedName' => '',
            'expectedFieldType' => 0,
            'expectedFieldId' => 0,
        ];
    }

    public function testReadFieldEnd()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->readFieldEnd());
    }

    public function testReadMapBegin()
    {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $expectedReadAllArgs = [
            [1], #readByte
            [1], #readByte
            [4], #readI32
        ];
        $readAllReturns = [
            pack('c', TType::I32),
            pack('c', TType::STRING),
            pack('N', 555),
        ];
        $transport
            ->expects($this->exactly(3))
            ->method('readAll')
            ->willReturnCallback(function (...$callArgs) use ($expectedReadAllArgs, $readAllReturns) {
                static $iteration = 0;
                $expected = $expectedReadAllArgs[$iteration];
                foreach ($expected as $i => $exp) {
                    if ($exp instanceof Constraint) {
                        $this->assertThat($callArgs[$i], $exp);
                    } else {
                        $this->assertSame($exp, $callArgs[$i]);
                    }
                }

                return $readAllReturns[$iteration++];
            });

        $this->assertEquals(6, $protocol->readMapBegin($keyType, $valType, $size));
        $this->assertEquals(TType::I32, $keyType);
        $this->assertEquals(TType::STRING, $valType);
        $this->assertEquals(555, $size);
    }

    public function testReadMapEnd()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->readMapEnd());
    }

    public function testReadListBegin()
    {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $expectedReadAllArgs = [
            [1], #readByte
            [4], #readI32
        ];
        $readAllReturns = [
            pack('c', TType::I32),
            pack('N', 555),
        ];
        $transport
            ->expects($this->exactly(2))
            ->method('readAll')
            ->willReturnCallback(function (...$callArgs) use ($expectedReadAllArgs, $readAllReturns) {
                static $iteration = 0;
                $expected = $expectedReadAllArgs[$iteration];
                foreach ($expected as $i => $exp) {
                    if ($exp instanceof Constraint) {
                        $this->assertThat($callArgs[$i], $exp);
                    } else {
                        $this->assertSame($exp, $callArgs[$i]);
                    }
                }

                return $readAllReturns[$iteration++];
            });

        $this->assertEquals(5, $protocol->readListBegin($elemType, $size));
        $this->assertEquals(TType::I32, $elemType);
        $this->assertEquals(555, $size);
    }

    public function testReadListEnd()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->readListEnd());
    }

    public function testReadSetBegin()
    {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $expectedReadAllArgs = [
            [1], #readByte
            [4], #readI32
        ];
        $readAllReturns = [
            pack('c', TType::I32),
            pack('N', 555),
        ];
        $transport
            ->expects($this->exactly(2))
            ->method('readAll')
            ->willReturnCallback(function (...$callArgs) use ($expectedReadAllArgs, $readAllReturns) {
                static $iteration = 0;
                $expected = $expectedReadAllArgs[$iteration];
                foreach ($expected as $i => $exp) {
                    if ($exp instanceof Constraint) {
                        $this->assertThat($callArgs[$i], $exp);
                    } else {
                        $this->assertSame($exp, $callArgs[$i]);
                    }
                }

                return $readAllReturns[$iteration++];
            });

        $this->assertEquals(5, $protocol->readSetBegin($elemType, $size));
        $this->assertEquals(TType::I32, $elemType);
        $this->assertEquals(555, $size);
    }

    public function testReadSetEnd()
    {
        $transport = $this->createStub(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $this->assertEquals(0, $protocol->readSetEnd());
    }

    public function testReadBool()
    {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('readAll')
            ->with(1) #readByte
            ->willReturn(pack('c', 1));

        $this->assertEquals(1, $protocol->readBool($value));
        $this->assertTrue($value);
    }

    public function testReadByte()
    {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('readAll')
            ->with(1) #readByte
            ->willReturn(pack('c', 1));

        $this->assertEquals(1, $protocol->readByte($value));
        $this->assertEquals(1, $value);
    }

    #[DataProvider('readI16DataProvider')]
    public function testReadI16(
        $storedValue,
        $expectedValue
    ) {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('readAll')
            ->with(2) #readI16
            ->willReturn(pack('n', $storedValue));

        $this->assertEquals(2, $protocol->readI16($value));
        $this->assertEquals($expectedValue, $value);
    }

    public static function readI16DataProvider()
    {
        yield 'positive' => [1, 1];
        yield 'negative' => [-1, -1];
    }

    #[DataProvider('readI16DataProvider')]
    public function testReadI32(
        $storedValue,
        $expectedValue
    ) {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('readAll')
            ->with(4) #readI32
            ->willReturn(pack('N', $storedValue));

        $this->assertEquals(4, $protocol->readI32($value));
        $this->assertEquals($expectedValue, $value);
    }

    public function readI32DataProvider()
    {
        yield 'positive' => [1, 1];
        yield 'negative' => [-1, -1];
    }

    #[DataProvider('readI64For32BitArchitectureDataProvider')]
    public function testReadI64For32BitArchitecture(
        $storedValue,
        $expectedValue
    ) {
        if (PHP_INT_SIZE !== 4) {
            $this->markTestSkipped('Test is only for 32 bit architecture');
        }

        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $neg = $storedValue < 0;

        if ($neg) {
            $storedValue *= -1;
        }

        $hi = (int)($storedValue / 4294967296);
        $lo = (int)$storedValue;

        if ($neg) {
            $hi = ~$hi;
            $lo = ~$lo;
            if (($lo & (int)0xffffffff) == (int)0xffffffff) {
                $lo = 0;
                $hi++;
            } else {
                $lo++;
            }
        }

        $transport
            ->expects($this->once())
            ->method('write')
            ->with(pack('N2', $hi, $lo)); #writeI64

        $this->assertEquals(8, $protocol->readI64($value));
        $this->assertEquals($expectedValue, $value);
    }

    public static function readI64For32BitArchitectureDataProvider()
    {
        $storedValueRepresent = function ($value) {
            // PHP_INT_MIN (-2^63) cannot be safely negated:
            // -PHP_INT_MIN overflows the 64-bit signed integer range.
            if ($value === PHP_INT_MIN) {
                return pack('N2', 0x80000000, 0x00000000);
            }

            $neg = $value < 0;

            if ($neg) {
                $value *= -1;
            }

            $hi = (int)($value / 4294967296);
            $lo = (int)$value;

            if ($neg) {
                $hi = ~$hi;
                $lo = ~$lo;
                if (($lo & (int)0xffffffff) == (int)0xffffffff) {
                    $lo = 0;
                    $hi++;
                } else {
                    $lo++;
                }
            }

            return pack('N2', $hi, $lo);
        };

        yield 'positive' => [
            'storedValue' => $storedValueRepresent(1),
            'expectedValue' => 1,
        ];

        yield 'max' => [
            'storedValue' => $storedValueRepresent(PHP_INT_MAX),
            'expectedValue' => PHP_INT_MAX,
        ];

        yield 'min' => [
            'storedValue' => $storedValueRepresent(PHP_INT_MIN),
            'expectedValue' => PHP_INT_MIN,
        ];

        yield 'negative' => [
            'storedValue' => $storedValueRepresent(-1),
            'expectedValue' => -1,
        ];
    }

    #[DataProvider('readI64For64BitArchitectureDataProvider')]
    public function testReadI64For64BitArchitecture(
        $storedValue,
        $expectedValue
    ) {
        if (PHP_INT_SIZE == 4) {
            $this->markTestSkipped('Test is only for 64 bit architecture');
        }
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('readAll')
            ->with(8) #readI64
            ->willReturn($storedValue);

        $this->assertEquals(8, $protocol->readI64($value));
        $this->assertEquals($expectedValue, $value);
    }

    public static function readI64For64BitArchitectureDataProvider()
    {
        $storedValueRepresent = function ($value) {
            $hi = $value >> 32;
            $lo = $value & 0xFFFFFFFF;

            return pack('N2', $hi, $lo);
        };

        yield 'positive' => [
            'storedValue' => $storedValueRepresent(1),
            'expectedValue' => 1,
        ];

        yield 'max' => [
            'storedValue' => $storedValueRepresent(PHP_INT_MAX),
            'expectedValue' => PHP_INT_MAX,
        ];

        yield 'min' => [
            'storedValue' => $storedValueRepresent(PHP_INT_MIN),
            'expectedValue' => PHP_INT_MIN,
        ];

        yield 'negative' => [
            'storedValue' => $storedValueRepresent(-1),
            'expectedValue' => -1,
        ];
    }

    public function testReadDouble()
    {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->once())
            ->method('readAll')
            ->with(8) #readDouble
            ->willReturn(strrev(pack('d', 789)));

        $this->assertEquals(8, $protocol->readDouble($value));
        $this->assertEquals(789, $value);
    }

    #[DataProvider('readStringDataProvider')]
    public function testReadString(
        $readCallsParams,
        $readCallsResults,
        $expectedLength,
        $expectedValue
    ) {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport
            ->expects($this->exactly(count($readCallsParams)))
            ->method('readAll')
            ->willReturnCallback(function (...$callArgs) use ($readCallsParams, $readCallsResults) {
                static $iteration = 0;
                $expected = $readCallsParams[$iteration];
                foreach ($expected as $i => $exp) {
                    if ($exp instanceof Constraint) {
                        $this->assertThat($callArgs[$i], $exp);
                    } else {
                        $this->assertSame($exp, $callArgs[$i]);
                    }
                }

                return $readCallsResults[$iteration++];
            });

        $this->assertEquals($expectedLength, $protocol->readString($value));
        $this->assertEquals($expectedValue, $value);
    }

    public static function readStringDataProvider()
    {
        $storedValue = '';
        yield 'empty' => [
            'readCallsParams' => [
                [4]
            ],
            'readCallsResults' => [
                pack('N', strlen($storedValue))
            ],
            'expectedLength' => 4,
            'expectedValue' => '',
        ];

        $storedValue = 'string';
        yield 'non-empty' => [
            'readCallsParams' => [
                [4],
                [strlen($storedValue)]
            ],
            'readCallsResults' => [
                pack('N', strlen($storedValue)),
                $storedValue
            ],
            'expectedLength' => 10,
            'expectedValue' => 'string',
        ];
    }

    public function testReadMapBeginRejectsNegativeSize()
    {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $readAllReturns = [
            pack('c', TType::I32),
            pack('c', TType::STRING),
            pack('N', 0xffffffff),
        ];
        $transport->method('readAll')
            ->willReturnCallback(function () use ($readAllReturns) {
                static $iteration = 0;
                return $readAllReturns[$iteration++];
            });

        $this->expectException(TProtocolException::class);
        $this->expectExceptionCode(TProtocolException::NEGATIVE_SIZE);
        $protocol->readMapBegin($keyType, $valType, $size);
    }

    public function testReadListBeginRejectsNegativeSize()
    {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $readAllReturns = [
            pack('c', TType::I32),
            pack('N', 0xffffffff),
        ];
        $transport->method('readAll')
            ->willReturnCallback(function () use ($readAllReturns) {
                static $iteration = 0;
                return $readAllReturns[$iteration++];
            });

        $this->expectException(TProtocolException::class);
        $this->expectExceptionCode(TProtocolException::NEGATIVE_SIZE);
        $protocol->readListBegin($elemType, $size);
    }

    public function testReadSetBeginRejectsNegativeSize()
    {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $readAllReturns = [
            pack('c', TType::I32),
            pack('N', 0xffffffff),
        ];
        $transport->method('readAll')
            ->willReturnCallback(function () use ($readAllReturns) {
                static $iteration = 0;
                return $readAllReturns[$iteration++];
            });

        $this->expectException(TProtocolException::class);
        $this->expectExceptionCode(TProtocolException::NEGATIVE_SIZE);
        $protocol->readSetBegin($elemType, $size);
    }

    public function testReadStringRejectsNegativeSize()
    {
        $transport = $this->createMock(TTransport::class);
        $protocol = new TBinaryProtocol($transport, false, false);

        $transport->method('readAll')
            ->willReturn(pack('N', 0xffffffff));

        $this->expectException(TProtocolException::class);
        $this->expectExceptionCode(TProtocolException::NEGATIVE_SIZE);
        $protocol->readString($value);
    }
}
