<?php

declare(strict_types=1);

/*
 * eduVPN - End-user friendly VPN.
 *
 * Copyright: 2014-2023, The Commons Conservancy eduVPN Programme
 * SPDX-License-Identifier: AGPL-3.0+
 */

namespace Vpn\Portal\Tests;

use PHPUnit\Framework\TestCase;
use Vpn\Portal\Cfg\Config;
use Vpn\Portal\Cfg\ProfileConfig;
use Vpn\Portal\Cfg\WireGuardConfig;
use Vpn\Portal\Exception\ProtocolException;
use Vpn\Portal\Protocol;

/**
 * @covers \Vpn\Portal\Protocol
 *
 * @uses \Vpn\Portal\Cfg\ProfileConfig
 * @uses \Vpn\Portal\Cfg\WireGuardConfig
 * @uses \Vpn\Portal\FileIO
 * @uses \Vpn\Portal\Cfg\Config
 */
final class ProtocolTest extends TestCase
{
    private const WIREGUARD_PUBLIC_KEY = 'vb+cQ2qTSwNjcnht2cURSubD/NC8CsD0/QGygtrb5Es=';

    public function testParseMimeType(): void
    {
        static::assertSame(
            ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
            Protocol::parseMimeType(null)
        );
        static::assertSame(
            ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
            Protocol::parseMimeType('foo')
        );
        static::assertSame(
            ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => false, 'wireguard+tcp' => false],
            Protocol::parseMimeType('application/x-openvpn-profile')
        );
        static::assertSame(
            ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => false, 'wireguard+tcp' => false],
            Protocol::parseMimeType('application/x-openvpn-profile, application/x-openvpn-profile')
        );
        static::assertSame(
            ['openvpn+udp' => false, 'openvpn+tcp' => false, 'wireguard+udp' => true, 'wireguard+tcp' => false],
            Protocol::parseMimeType('application/x-wireguard-profile')
        );
        static::assertSame(
            ['openvpn+udp' => false, 'openvpn+tcp' => false, 'wireguard+udp' => true, 'wireguard+tcp' => false],
            Protocol::parseMimeType('application/x-wireguard-profile, foo/bar')
        );
        static::assertSame(
            ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
            Protocol::parseMimeType('application/x-wireguard-profile, application/x-openvpn-profile')
        );
        static::assertSame(
            ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => true],
            Protocol::parseMimeType('application/x-wireguard-profile, application/x-openvpn-profile, application/x-wireguard+tcp-profile')
        );
        static::assertSame(
            ['openvpn+udp' => false, 'openvpn+tcp' => false, 'wireguard+udp' => false, 'wireguard+tcp' => true],
            Protocol::parseMimeType('application/x-wireguard+tcp-profile')
        );
    }

    /**
     * This is the default deployment, with OpenVPN and WireGuard enabled, but
     * no WireGuard+TCP support yet, neither in client, nor in server. The
     * client does NOT prefer TCP.
     */
    public function testDefault(): void
    {
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        $this->assertSame(
            ['openvpn+udp', 'openvpn+tcp', 'wireguard+udp'],
            Protocol::determine(
                $defaultConfig->wireGuardConfig(),
                $defaultConfig->profileConfig('default'),
                ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
                self::WIREGUARD_PUBLIC_KEY,
                false
            )
        );
    }

    /**
     * This is the default deployment, with OpenVPN and WireGuard enabled, but
     * no WireGuard+TCP support yet, neither in client, nor in server. The
     * client DOES prefer TCP.
     */
    public function testDefaultPreferTcp(): void
    {
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        $this->assertSame(
            ['openvpn+tcp', 'openvpn+udp', 'wireguard+udp'],
            Protocol::determine(
                $defaultConfig->wireGuardConfig(),
                $defaultConfig->profileConfig('default'),
                ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
                self::WIREGUARD_PUBLIC_KEY,
                true
            )
        );
    }

    /**
     * This is the default deployment, with OpenVPN and WireGuard enabled, but
     * no WireGuard+TCP support yet, neither in client, nor in server. The
     * preferred protocol in the server is *WireGuard*. The client does NOT
     * prefer TCP.
     */
    public function testPreferWireGuard(): void
    {
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        $this->assertSame(
            ['wireguard+udp', 'openvpn+udp', 'openvpn+tcp'],
            Protocol::determine(
                $defaultConfig->wireGuardConfig(),
                new ProfileConfig(
                    array_merge(
                        $defaultConfig->profileConfig('default')->toArray(),
                        ['preferredProto' => 'wireguard']
                    )
                ),
                ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
                self::WIREGUARD_PUBLIC_KEY,
                false
            )
        );
    }

    /**
     * This is the default deployment, with OpenVPN and WireGuard enabled, but
     * no WireGuard+TCP support yet, neither in client, nor in server. The
     * preferred protocol in the server is *WireGuard*. The client DOES prefer
     * TCP.
     */
    public function testPreferWireGuardPreferTcp(): void
    {
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        $this->assertSame(
            ['openvpn+tcp', 'wireguard+udp', 'openvpn+udp'],
            Protocol::determine(
                $defaultConfig->wireGuardConfig(),
                self::profileConfigOverride($defaultConfig->profileConfig('default'), ['preferredProto' => 'wireguard']),
                ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
                self::WIREGUARD_PUBLIC_KEY,
                true
            )
        );
    }

    /**
     * This is the *recommended* deployment before WireGuard+TCP support was a
     * thing, with OpenVPN and WireGuard enabled, but only OpenVPN+TCP. The
     * preferred protocol in the server is *WireGuard*. The client does *NOT*
     * prefer TCP.
     */
    public function testRecommended(): void
    {
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        $this->assertSame(
            ['wireguard+udp', 'openvpn+tcp'],
            Protocol::determine(
                $defaultConfig->wireGuardConfig(),
                self::profileConfigOverride($defaultConfig->profileConfig('default'), ['preferredProto' => 'wireguard', 'oUdpPortList' => []]),
                ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
                self::WIREGUARD_PUBLIC_KEY,
                false
            )
        );
    }

    /**
     * This is the *recommended* deployment before WireGuard+TCP support was a
     * thing, with OpenVPN and WireGuard enabled, but only OpenVPN+TCP. The
     * preferred protocol in the server is *WireGuard*. The client *DOES*
     * prefer TCP.
     */
    public function testRecommendedPreferTcp(): void
    {
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        $this->assertSame(
            ['openvpn+tcp', 'wireguard+udp'],
            Protocol::determine(
                $defaultConfig->wireGuardConfig(),
                self::profileConfigOverride($defaultConfig->profileConfig('default'), ['preferredProto' => 'wireguard', 'oUdpPortList' => []]),
                ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
                self::WIREGUARD_PUBLIC_KEY,
                true
            )
        );
    }

    /**
     * This is the default deployment, with OpenVPN and WireGuard enabled and
     * WireGuard+TCP server support. The client does NOT support WireGuard+TCP
     * and does not prefer TCP.
     */
    public function testDefaultServerWireGuardTcp(): void
    {
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        $this->assertSame(
            ['openvpn+udp', 'openvpn+tcp', 'wireguard+udp'],
            Protocol::determine(
                self::wgConfigOverride($defaultConfig->wireGuardConfig(), ['enableProxy' => true]),
                $defaultConfig->profileConfig('default'),
                ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
                self::WIREGUARD_PUBLIC_KEY,
                false
            )
        );
    }

    /**
     * This is the default deployment, with OpenVPN and WireGuard enabled and
     * WireGuard+TCP server support. The client does NOT support WireGuard+TCP
     * and DOES prefer TCP.
     */
    public function testDefaultServerWireGuardTcpPreferTcp(): void
    {
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        $this->assertSame(
            ['openvpn+tcp', 'openvpn+udp', 'wireguard+udp'],
            Protocol::determine(
                self::wgConfigOverride($defaultConfig->wireGuardConfig(), ['enableProxy' => true]),
                $defaultConfig->profileConfig('default'),
                ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
                self::WIREGUARD_PUBLIC_KEY,
                true
            )
        );
    }

    /**
     * This is the default deployment, but with OpenVPN disabled. No
     * WireGuard+TCP support yet, neither in client, nor in server. The client
     * does NOT prefer TCP.
     */
    public function testDefaultNoOpenVpn(): void
    {
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        $this->assertSame(
            ['wireguard+udp'],
            Protocol::determine(
                $defaultConfig->wireGuardConfig(),
                self::profileConfigOverride($defaultConfig->profileConfig('default'), ['oRangeFour' => [], 'oRangeSix' => []]),
                ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
                self::WIREGUARD_PUBLIC_KEY,
                false
            )
        );
    }

    /**
     * This is the default deployment, but with OpenVPN disabled. No
     * WireGuard+TCP support yet, neither in client, nor in server. The client
     * DOES prefer TCP.
     *
     * NOTE: this is interesting, the client has "Prefer TCP", but the server
     * just ignores it and provides a WireGuard+UDP config...
     */
    public function testDefaultNoOpenVpnPreferTcp(): void
    {
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        $this->assertSame(
            ['wireguard+udp'],
            Protocol::determine(
                $defaultConfig->wireGuardConfig(),
                self::profileConfigOverride($defaultConfig->profileConfig('default'), ['oRangeFour' => [], 'oRangeSix' => []]),
                ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
                self::WIREGUARD_PUBLIC_KEY,
                true
            )
        );
    }

    /**
     * This is the default deployment, but with WireGuard disabled. The client
     * does NOT prefer TCP.
     */
    public function testDefaultNoWireGuard(): void
    {
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        $this->assertSame(
            ['openvpn+udp', 'openvpn+tcp'],
            Protocol::determine(
                $defaultConfig->wireGuardConfig(),
                self::profileConfigOverride($defaultConfig->profileConfig('default'), ['wRangeFour' => [], 'wRangeSix' => []]),
                ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
                self::WIREGUARD_PUBLIC_KEY,
                false
            )
        );
    }

    /**
     * This is the default deployment, but with WireGuard disabled. The client
     * DOES prefer TCP.
     */
    public function testDefaultNoWireGuardPreferTcp(): void
    {
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        $this->assertSame(
            ['openvpn+tcp', 'openvpn+udp'],
            Protocol::determine(
                $defaultConfig->wireGuardConfig(),
                self::profileConfigOverride($defaultConfig->profileConfig('default'), ['wRangeFour' => [], 'wRangeSix' => []]),
                ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
                self::WIREGUARD_PUBLIC_KEY,
                true
            )
        );
    }

    /**
     * The server only supports WireGuard+TCP, but the client does not. This
     * should result in an error.
     */
    public function testServerWireGuardTcpOnly(): void
    {
        $this->expectException(ProtocolException::class);
        $this->expectExceptionMessage('no common VPN protocol [CLIENT=openvpn+udp,openvpn+tcp,wireguard+udp SERVER=wireguard+tcp]');
        $defaultConfig = Config::fromFile(dirname(__DIR__) . '/config/config.php.example');
        Protocol::determine(
            self::wgConfigOverride($defaultConfig->wireGuardConfig(), ['enableProxy' => true, 'onlyProxy' => true]),
            self::profileConfigOverride($defaultConfig->profileConfig('default'), ['oRangeFour' => [], 'oRangeSix' => []]),
            ['openvpn+udp' => true, 'openvpn+tcp' => true, 'wireguard+udp' => true, 'wireguard+tcp' => false],
            self::WIREGUARD_PUBLIC_KEY,
            false
        );
    }

    private static function profileConfigOverride(ProfileConfig $profileConfig, array $configOverride): ProfileConfig
    {
        return new ProfileConfig(
            array_merge(
                $profileConfig->toArray(),
                $configOverride
            )
        );
    }

    private static function wgConfigOverride(WireGuardConfig $wgConfig, array $configOverride): WireGuardConfig
    {
        return new WireGuardConfig(
            array_merge(
                $wgConfig->toArray(),
                $configOverride
            )
        );
    }
}
