<?php

declare(strict_types=1);

/*
 * Copyright (c) 2023-2024 François Kooman <fkooman@tuxed.net>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

namespace fkooman\Radius\Tests;

use fkooman\Radius\RadiusPacket;
use fkooman\Radius\Utils;
use PHPUnit\Framework\TestCase;
use RangeException;

/**
 * @covers \fkooman\Radius\Utils
 */
class UtilsTest extends TestCase
{
    public function testShortToBytesValid(): void
    {
        $this->assertSame('0000', bin2hex(Utils::shortToBytes(0)));
        $this->assertSame('0001', bin2hex(Utils::shortToBytes(1)));
        $this->assertSame('fffe', bin2hex(Utils::shortToBytes(2 ** 16 - 2)));
        $this->assertSame('ffff', bin2hex(Utils::shortToBytes(2 ** 16 - 1)));
    }

    public function testShortToBytesNegative(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('int value MUST be >= 0');
        Utils::shortToBytes(-1);
    }

    public function testShortToBytesTooBig(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('int value MUST be <= 65535');
        Utils::shortToBytes(2 ** 16);
    }

    public function testBytesToShortValid(): void
    {
        $this->assertSame(0, Utils::bytesToShort("\x00\x00"));
        $this->assertSame(1, Utils::bytesToShort("\x00\x01"));
        $this->assertSame(255, Utils::bytesToShort("\x00\xff"));
        $this->assertSame(256, Utils::bytesToShort("\x01\x00"));
        $this->assertSame(257, Utils::bytesToShort("\x01\x01"));
        $this->assertSame(65280, Utils::bytesToShort("\xff\x00"));
        $this->assertSame(65535, Utils::bytesToShort("\xff\xff"));
    }

    public function testBytesToShortTooShort(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('need two bytes');
        Utils::bytesToShort('');
    }

    public function testBytesToShortTooLong(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('need two bytes');
        Utils::bytesToShort("\x01\x02\x03\x04");
    }

    public function testToVendorIdValid(): void
    {
        $this->assertSame('00000c04', bin2hex(Utils::longToBytes(3076)));
        $this->assertSame(3076, Utils::bytesToLong(hex2bin('00000c04')));
    }

    public function testSafeSubstr(): void
    {
        $this->assertSame('foo', Utils::safeSubstr('foo', 0, 3));
        $this->assertSame('fo', Utils::safeSubstr('foo', 0, 2));
        $this->assertSame('oo', Utils::safeSubstr('foo', 1, 2));
        $this->assertSame('', Utils::safeSubstr('foo', 0, 0));
        $this->assertSame('oo', Utils::safeSubstr('foo', 1));
    }

    public function testEmptyStringSafeSubstr(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('empty string');
        Utils::safeSubstr('', 0, 0);
    }

    public function testOutOfRangeOffsetSafeSubstr(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('offset "3" does not exist in string of length "3"');
        Utils::safeSubstr('foo', 3, 1);
    }

    public function testTooLongSafeSubstr(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('cannot get string of length at provided offset [length=3,offset=1,requested_length=5]');
        Utils::safeSubstr('foo', 1, 5);
    }

    public function testNegativeOffsetSafeSubstr(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('negative offset');
        Utils::safeSubstr('foo', -1, 2);
    }

    public function testNegativeLengthSafeSubstr(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('negative length');
        Utils::safeSubstr('foo', 0, -1);
    }

    public function testVerifyPacketTypeValid(): void
    {
        $this->assertSame(RadiusPacket::ACCESS_REQUEST, Utils::verifyPacketType(RadiusPacket::ACCESS_REQUEST));
        $this->assertSame(RadiusPacket::ACCESS_ACCEPT, Utils::verifyPacketType(RadiusPacket::ACCESS_ACCEPT));
        $this->assertSame(RadiusPacket::ACCESS_REJECT, Utils::verifyPacketType(RadiusPacket::ACCESS_REJECT));
        $this->assertSame(RadiusPacket::ACCESS_CHALLENGE, Utils::verifyPacketType(RadiusPacket::ACCESS_CHALLENGE));
    }

    public function testVerifyPacketTypeInvalid(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('packet type 99 not supported');
        Utils::verifyPacketType(99);
    }

    public function testRequireLengthValid(): void
    {
        $this->assertSame('', Utils::requireLength('', 0));
        $this->assertSame('123456', Utils::requireLength('123456', 6));
    }

    public function testRequireLengthInvalid(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('string length MUST be exactly 3 octets');
        Utils::requireLength('123456', 3);
    }

    public function testRequireLengthRangeValid(): void
    {
        $this->assertSame('', Utils::requireLengthRange('', 0, 0));
        $this->assertSame('', Utils::requireLengthRange('', 0, 5));
        $this->assertSame('a', Utils::requireLengthRange('a', 0, 5));
        $this->assertSame('a', Utils::requireLengthRange('a', 1, 1));
    }

    public function testRequireLengthRangeTooShort(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('string length MUST be >= 1 octets');
        Utils::requireLengthRange('', 1, 5);
    }

    public function testRequireLengthRangeInvalidMinLength(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('int value MUST be >= 0');
        Utils::requireLengthRange('foo', -1, 5);
    }

    public function testRequireLengthRangeInvalidMaxLength(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('int value MUST be >= 2');
        Utils::requireLengthRange('foo', 2, -3);
    }

    public function testRequireLengthRangeMaxLessThanMin(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('int value MUST be >= 4');
        Utils::requireLengthRange('foobar', 4, 3);
    }

    public function testRequireLengthRangeTooLong(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('string length MUST be <= 4 octets');
        Utils::requireLengthRange('foobar', 1, 4);
    }

    public function testRequireVendorAttributeIdValid(): void
    {
        $this->assertSame([3076,25], Utils::requireVendorAttributeId('3076.25'));
        $this->assertSame([1,1], Utils::requireVendorAttributeId('1.1'));
    }

    public function testRequireVendorAttributeIdTooManyDots(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('not of format ID.Type');
        Utils::requireVendorAttributeId('1.2.3');
    }

    public function testRequireVendorAttributeIdVendorIdNotNumeric(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('Vendor ID MUST be numeric');
        Utils::requireVendorAttributeId('a.5');
    }

    public function testRequireVendorAttributeIdVendorTypeNotNumeric(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('Vendor Type MUST be numeric');
        Utils::requireVendorAttributeId('3076.a');
    }

    public function testRequireVendorAttributeIdJustDot(): void
    {
        $this->expectException(RangeException::class);
        $this->expectExceptionMessage('Vendor ID MUST be numeric');
        Utils::requireVendorAttributeId('.5');
    }

    public function testBytesToIpAddress(): void
    {
        $this->assertSame('192.168.42.43', Utils::bytesToIpAddress(hex2bin('c0a82a2b')));
        $this->assertSame('0.0.0.0', Utils::bytesToIpAddress(hex2bin('00000000')));
        $this->assertSame('255.255.255.255', Utils::bytesToIpAddress(hex2bin('ffffffff')));
    }

    public function testIpAddressToBytes(): void
    {
        $this->assertSame(hex2bin('c0a82a2b'), Utils::ipAddressToBytes('192.168.42.43'));
    }
}
