<?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\Exception\AccessChallengeException;
use fkooman\Radius\Exception\AccessRejectException;
use fkooman\Radius\Exception\RadiusException;
use fkooman\Radius\RadiusPacket;
use fkooman\Radius\ServerInfo;
use PHPUnit\Framework\TestCase;

/**
 * @covers \fkooman\Radius\RadiusClient
 *
 * @uses \fkooman\Radius\RadiusPacket
 * @uses \fkooman\Radius\Utils
 * @uses \fkooman\Radius\Password
 * @uses \fkooman\Radius\AttributeCollection
 * @uses \fkooman\Radius\ServerInfo
 * @uses \fkooman\Radius\ClientConfig
 * @uses \fkooman\Radius\PhpSocket
 * @uses \fkooman\Radius\Exception\AccessRejectException
 * @uses \fkooman\Radius\Exception\AccessChallengeException
 * @uses \fkooman\Radius\MessageAuthenticator
 * @uses \fkooman\Radius\ResponseAuthenticator
 */
class RadiusClientTest extends TestCase
{
    public function testRequest(): void
    {
        $t = new TestSocket('02000040c5730d1a0db20e5d6ad5fb603102888e121a41757468656e7469636174696f6e2073756363656564656450127fecf9ed3c28ed5e203de65f98008402');
        $r = new TestRadiusClient($t, 'e38e27cdd0a5f45b801fa2510974fa1f', true);
        $r->addServer(new ServerInfo('udp://10.253.109.1:1812', 's3cr3t'));
        $accessResponse = $r->accessRequest('foo', 'bar');
        $this->assertSame(
            '01000048e38e27cdd0a5f45b801fa2510974fa1f0105666f6f0212a19fb3d0bb571a9059e868d3c2e90761200b6d792d6e61732d696450125f6221b6431e5188b39db84b915c96a8',
            bin2hex($t->getWriteBuffer())
        );
        $this->assertTrue($accessResponse->isAccessAccept());
        $this->assertSame(['Authentication succeeded'], $accessResponse->attributeCollection()->get('Reply-Message'));
        $this->assertSame(['Authentication succeeded'], $accessResponse->attributeCollection()->get(18));
        $this->assertSame([hex2bin('7fecf9ed3c28ed5e203de65f98008402')], $accessResponse->attributeCollection()->get(80));
        $this->assertSame([hex2bin('7fecf9ed3c28ed5e203de65f98008402')], $accessResponse->attributeCollection()->get('Message-Authenticator'));
    }

    public function testVeryLongPasswordRequest(): void
    {
        $t = new TestSocket('020000407a952b8823b81ebdf57b3812c4f19d1d121a41757468656e7469636174696f6e207375636365656465645012ca9ba652ebc9641d79fe860ea828b738');
        $r = new TestRadiusClient($t, 'd7e35415ff3b206ac917d32d823ae9a0', true);
        $r->addServer(new ServerInfo('udp://10.253.109.1:1812', 's3cr3t'));
        $accessResponse = $r->accessRequest('foo', str_repeat('a', 20));
        $this->assertSame(
            '01000058d7e35415ff3b206ac917d32d823ae9a00105666f6f022268688ff4b3fca0b188a1af2edbd88e2d3bfe53969c6af7ecd0af124ffb747edf200b6d792d6e61732d69645012f8799ab500a6aa81a18db4de69a4c52d',
            bin2hex($t->getWriteBuffer())
        );
        $this->assertTrue($accessResponse->isAccessAccept());
    }

    public function testWrongPassword(): void
    {
        $t = new TestSocket('0300003d5e44868cf5ce3285c1defc6aa8082ed8121741757468656e7469636174696f6e206661696c65645012c934254f9b7dc45fb29c261582444a57');
        $r = new TestRadiusClient($t, '11bb163812653e088111c6a7ac6d8311', true);
        $r->addServer(new ServerInfo('udp://10.253.109.1:1812', 's3cr3t'));

        try {
            $r->accessRequest('foo', 'baz');
            $this->fail();
        } catch (AccessRejectException $e) {
            $this->assertSame(
                '0100004811bb163812653e088111c6a7ac6d83110105666f6f02129db946a488988d569dad8266a01660f8200b6d792d6e61732d6964501299e5a8a0b124b503dc21208a2d479208',
                bin2hex($t->getWriteBuffer())
            );
            $this->assertSame(['Authentication failed'], $e->radiusPacket()->attributeCollection()->get('Reply-Message'));
        }
    }

    public function testMangledResponseAuthenticator(): void
    {
        $this->expectException(RadiusException::class);
        $this->expectExceptionMessage('RADIUS Response Authenticator has unexpected value');
        // we change the last two bytes from 02 -> 01 in the response
        // authenticator, just to see it fail
        $t = new TestSocket('02000040c5730d1a0db20e5d6ad5fb603102888e121a41757468656e7469636174696f6e2073756363656564656450127fecf9ed3c28ed5e203de65f98008401');
        $r = new TestRadiusClient($t, 'e38e27cdd0a5f45b801fa2510974fa1f', true);
        $r->addServer(new ServerInfo('udp://10.253.109.1:1812', 's3cr3t'));
        $r->accessRequest('foo', 'bar');
    }

    public function testNoAttributes(): void
    {
        $t = new TestSocket('02000014dec8f66691136e6ec4c097d9e7cc145a');
        $r = new TestRadiusClient($t, '735c001ce211b484a1531c071ca531f9', false);
        $serverInfo = new ServerInfo('udp://10.43.43.6:1812', 's3cr3t');
        $r->addServer($serverInfo);
        $accessResponse = $r->accessRequest('foo', 'bar');
        $this->assertTrue($accessResponse->isAccessAccept());
    }

    public function testVsa(): void
    {
        $t = new TestSocket('0200002fb415550653fd5ba8ad95b16508d8f90e120d48692074657374696e67211a0e00000c041908666f6f626172');
        $r = new TestRadiusClient($t, '0c2f00a9344e818880f2010d1a9302a0', false);
        $serverInfo = new ServerInfo('udp://127.0.0.1:1812', 'testing123');
        $r->addServer($serverInfo);
        $accessResponse = $r->accessRequest('testing', 'password');
        $this->assertSame(
            '0100004c0c2f00a9344e818880f2010d1a9302a0010974657374696e67021286d017b088a223223f32994607a88fbd200b6d792d6e61732d69645012a75442f5e1863c74c68e01e8b5d3bbf2',
            bin2hex($t->getWriteBuffer())
        );
        $this->assertTrue($accessResponse->isAccessAccept());
        $this->assertSame(['Hi testing!'], $accessResponse->attributeCollection()->get(18));
        $this->assertSame(['foobar'], $accessResponse->attributeCollection()->get('3076.25'));
    }

    public function testRequestAcceptChallenge(): void
    {
        $t = new TestSocket('0b00003fcc4269ce271f458e71741edf9e78201c1215706c6561736520656e74657220746f6b656e3a18163036353333313835343231353638353038323838');
        $r = new TestRadiusClient($t, '557c530595585f2ce403f15df8bcc927', false);
        $serverInfo = new ServerInfo('udp://10.253.109.1:1812', 'phpradiusfkoomantest');
        $r->addServer($serverInfo);

        try {
            $r->accessRequest('foo', 'bar');
            $this->fail();
        } catch (AccessChallengeException $e) {
            $this->assertSame(
                '01000048557c530595585f2ce403f15df8bcc9270105666f6f0212a35cec832d2f1a1cf4d11a2ab6172477200b6d792d6e61732d696450127f5f8c7671d065f26198f8c1c419e854',
                bin2hex($t->getWriteBuffer())
            );
            $this->assertSame(['please enter token:'], $e->radiusPacket()->attributeCollection()->get('Reply-Message'));
            $this->assertSame(['06533185421568508288'], $e->radiusPacket()->attributeCollection()->get('State'));
            $radClientState = $e->radiusPacket()->toBytes();
            $this->assertSame('0b00003fcc4269ce271f458e71741edf9e78201c1215706c6561736520656e74657220746f6b656e3a18163036353333313835343231353638353038323838', bin2hex($radClientState));
            $t = new TestSocket('0201003013df4ed2ef20c55d1d28f398db82c714121c707269766163794944454120616363657373206772616e746564');
            $r = new TestRadiusClient($t, '5566bf20c433b0912b89e3bc30d2d307', false);
            $serverInfo = new ServerInfo('udp://10.253.109.1:1812', 'phpradiusfkoomantest');
            $r->addServer($serverInfo);
            $accessResponse = $r->accessRequest('foo', '220083', RadiusPacket::fromBytes($radClientState));
            $this->assertSame(
                '0101005e5566bf20c433b0912b89e3bc30d2d3070105666f6f0212f5a0233396bcf0f29afb3afe1f06af12200b6d792d6e61732d6964181630363533333138353432313536383530383238385012055c8459640e59ce826ff8daa46d75e5',
                bin2hex($t->getWriteBuffer())
            );
            $this->assertSame(['privacyIDEA access granted'], $accessResponse->attributeCollection()->get('Reply-Message'));
        }
    }
}
