<?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\Attributes\DataProvider;
use Thrift\Protocol\TBinaryProtocol;
use Thrift\Protocol\TCompactProtocol;
use Thrift\Protocol\TJSONProtocol;
use Thrift\Transport\TMemoryBuffer;
use Thrift\Type\TType;

class BoundaryValuesTest extends TestCase
{
    private static $WRITE_TYPE_MAP = [
        'writeByte' => TType::BYTE,
        'writeI16' => TType::I16,
        'writeI32' => TType::I32,
        'writeI64' => TType::I64,
        'writeDouble' => TType::DOUBLE,
        'writeString' => TType::STRING,
        'writeBool' => TType::BOOL,
    ];

    #[DataProvider('boundaryProvider')]
    public function testBoundaryValues($protocolClass, $writeMethod, $readMethod, $value)
    {
        $this->assertRoundtrip($protocolClass, $writeMethod, $readMethod, $value);
    }

    public static function boundaryProvider(): array
    {
        $cases = [
            // integers
            'byte min' => ['writeMethod' => 'writeByte', 'readMethod' => 'readByte', 'value' => -128],
            'byte max' => ['writeMethod' => 'writeByte', 'readMethod' => 'readByte', 'value' => 127],
            'byte zero' => ['writeMethod' => 'writeByte', 'readMethod' => 'readByte', 'value' => 0],
            'i16 min' => ['writeMethod' => 'writeI16', 'readMethod' => 'readI16', 'value' => -32768],
            'i16 max' => ['writeMethod' => 'writeI16', 'readMethod' => 'readI16', 'value' => 32767],
            'i16 zero' => ['writeMethod' => 'writeI16', 'readMethod' => 'readI16', 'value' => 0],
            'i32 min' => ['writeMethod' => 'writeI32', 'readMethod' => 'readI32', 'value' => -2147483648],
            'i32 max' => ['writeMethod' => 'writeI32', 'readMethod' => 'readI32', 'value' => 2147483647],
            'i32 zero' => ['writeMethod' => 'writeI32', 'readMethod' => 'readI32', 'value' => 0],
            'i64 min' => ['writeMethod' => 'writeI64', 'readMethod' => 'readI64', 'value' => PHP_INT_MIN],
            'i64 max' => ['writeMethod' => 'writeI64', 'readMethod' => 'readI64', 'value' => PHP_INT_MAX],
            'i64 zero' => ['writeMethod' => 'writeI64', 'readMethod' => 'readI64', 'value' => 0],

            // doubles
            'double zero' => ['writeMethod' => 'writeDouble', 'readMethod' => 'readDouble', 'value' => 0.0],
            'double negative zero' => ['writeMethod' => 'writeDouble', 'readMethod' => 'readDouble', 'value' => -0.0],
            'double max' => ['writeMethod' => 'writeDouble', 'readMethod' => 'readDouble', 'value' => PHP_FLOAT_MAX],
            'double min' => ['writeMethod' => 'writeDouble', 'readMethod' => 'readDouble', 'value' => PHP_FLOAT_MIN],
            'double epsilon' => ['writeMethod' => 'writeDouble', 'readMethod' => 'readDouble', 'value' => PHP_FLOAT_EPSILON],
            'double very small' => ['writeMethod' => 'writeDouble', 'readMethod' => 'readDouble', 'value' => 1e-300],

            // strings
            'empty string' => ['writeMethod' => 'writeString', 'readMethod' => 'readString', 'value' => ''],
            'null byte' => ['writeMethod' => 'writeString', 'readMethod' => 'readString', 'value' => "\x00"],
            'unicode' => ['writeMethod' => 'writeString', 'readMethod' => 'readString', 'value' => "Привіт 🌍"],
            'long string' => ['writeMethod' => 'writeString', 'readMethod' => 'readString', 'value' => str_repeat('a', 1024)],

            // booleans
            'bool true' => ['writeMethod' => 'writeBool', 'readMethod' => 'readBool', 'value' => true],
            'bool false' => ['writeMethod' => 'writeBool', 'readMethod' => 'readBool', 'value' => false],
        ];

        return self::expandWithProtocols($cases);
    }

    private static function expandWithProtocols(array $cases): array
    {
        $protocols = [
            'Binary' => TBinaryProtocol::class,
            'Compact' => TCompactProtocol::class,
            'JSON' => TJSONProtocol::class,
        ];

        $expanded = [];
        foreach ($protocols as $protoName => $protoClass) {
            foreach ($cases as $caseName => $case) {
                $expanded[$protoName . ' ' . $caseName] = [
                    'protocolClass' => $protoClass,
                    'writeMethod' => $case['writeMethod'],
                    'readMethod' => $case['readMethod'],
                    'value' => $case['value'],
                ];
            }
        }

        return $expanded;
    }

    private function assertRoundtrip($protocolClass, $writeMethod, $readMethod, $value)
    {
        $fieldType = self::$WRITE_TYPE_MAP[$writeMethod];
        $transport = new TMemoryBuffer();
        $protocol = new $protocolClass($transport);

        $protocol->writeStructBegin('Test');
        $protocol->writeFieldBegin('field', $fieldType, 1);
        $protocol->$writeMethod($value);
        $protocol->writeFieldEnd();
        $protocol->writeFieldStop();
        $protocol->writeStructEnd();

        $transport2 = new TMemoryBuffer($transport->getBuffer());
        $protocol2 = new $protocolClass($transport2);
        $protocol2->readStructBegin($name);
        $protocol2->readFieldBegin($fname, $ftype, $fid);
        $protocol2->$readMethod($result);
        $protocol2->readFieldEnd();
        $protocol2->readStructEnd();

        $this->assertEquals($value, $result);
    }
}
