<?php

declare(strict_types=1);

namespace PhpMyAdmin\Tests;

use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\FieldMetadata;
use PhpMyAdmin\MoTranslator\Loader;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\SqlParser\Context;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\Util;
use PhpMyAdmin\Utils\SessionCache;
use PhpMyAdmin\Version;

use function __;
use function _setlocale;
use function count;
use function date_default_timezone_get;
use function date_default_timezone_set;
use function file_exists;
use function floatval;
use function htmlspecialchars;
use function ini_get;
use function ini_set;
use function str_repeat;
use function str_replace;
use function strlen;
use function trim;

use const LC_ALL;
use const MYSQLI_NUM_FLAG;
use const MYSQLI_PRI_KEY_FLAG;
use const MYSQLI_TYPE_BIT;
use const MYSQLI_TYPE_GEOMETRY;
use const MYSQLI_TYPE_LONG;
use const MYSQLI_TYPE_SHORT;
use const MYSQLI_TYPE_STRING;
use const MYSQLI_UNIQUE_KEY_FLAG;

const FIELD_TYPE_INTEGER = 1;
const FIELD_TYPE_VARCHAR = 253;
const FIELD_TYPE_UNKNOWN = -1;

/**
 * @covers \PhpMyAdmin\Util
 */
class UtilTest extends AbstractTestCase
{
    /**
     * init data for the test
     */
    protected function setUp(): void
    {
        parent::setUp();
        parent::setLanguage();
        parent::setTheme();
    }

    /**
     * Test for listPHPExtensions
     *
     * @requires extension mysqli
     * @requires extension curl
     * @requires extension mbstring
     * @requires extension sodium
     */
    public function testListPHPExtensions(): void
    {
        $this->assertSame(
            [
                'mysqli',
                'curl',
                'mbstring',
                'sodium',
            ],
            Util::listPHPExtensions()
        );
    }

    public function testGetUniqueCondition(): void
    {
        $GLOBALS['db'] = 'db';
        $GLOBALS['cfg']['Server']['DisableIS'] = false;

        $actual = Util::getUniqueCondition(0, [], []);
        $this->assertEquals(['', false, []], $actual);

        $actual = Util::getUniqueCondition(0, [], [], true);
        $this->assertEquals(['', true, []], $actual);
    }

    public function testGetUniqueConditionWithMultipleFields(): void
    {
        $meta = [
            new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) [
                'name' => 'field1',
                'table' => 'table',
                'orgtable' => 'table',
            ]),
            new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) [
                'name' => 'field2',
                'table' => 'table',
                'orgtable' => 'table',
            ]),
            new FieldMetadata(MYSQLI_TYPE_SHORT, MYSQLI_NUM_FLAG, (object) [
                'name' => 'field3',
                'table' => 'table',
                'orgtable' => 'table',
            ]),
            new FieldMetadata(MYSQLI_TYPE_LONG, MYSQLI_NUM_FLAG, (object) [
                'name' => 'field4',
                'table' => 'table',
                'orgtable' => 'table',
            ]),
            new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) [
                'name' => 'field5',
                'table' => 'table',
                'orgtable' => 'table',
                'charsetnr' => 63, // binary
            ]),
            new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) [
                'name' => 'field6',
                'table' => 'table',
                'orgtable' => 'table',
                'charsetnr' => 63, // binary
            ]),
            new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) [
                'name' => 'field7',
                'table' => 'table',
                'orgtable' => 'table',
                'numeric' => false,
                'type' => 'blob',
                'charsetnr' => 32, // armscii8_general_ci
            ]),
            new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) [
                'name' => 'field8',
                'table' => 'table',
                'orgtable' => 'table',
                'numeric' => false,
                'type' => 'blob',
                'charsetnr' => 48, // latin1_general_ci
            ]),
            new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) [
                'name' => 'field9',
                'table' => 'table',
                'orgtable' => 'table',
                'numeric' => false,
                'type' => 'blob',
                'charsetnr' => 63, // binary
            ]),
            new FieldMetadata(MYSQLI_TYPE_GEOMETRY, 0, (object) [
                'name' => 'field10',
                'table' => 'table',
                'orgtable' => 'table',
            ]),
            new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) [
                'name' => 'field11',
                'table' => 'table2',
                'orgtable' => 'table2',
            ]),
            new FieldMetadata(MYSQLI_TYPE_BIT, 0, (object) [
                'name' => 'field12',
                'table' => 'table',
                'orgtable' => 'table',
                'length' => 4,
            ]),
        ];

        $actual = Util::getUniqueCondition(count($meta), $meta, [
            null,
            'value\'s',
            123456,
            123.456,
            'value',
            str_repeat('*', 1001),
            'value',
            'value',
            'value',
            'value',
            'value',
            0x1,
        ], false, 'table');
        $this->assertEquals(
            [
                '`table`.`field1` IS NULL AND `table`.`field2` = \'value\\\'s\' AND `table`.`field3` = 123456'
                . ' AND `table`.`field4` = 123.456 AND `table`.`field5` = CAST(0x76616c7565 AS BINARY)'
                . ' AND `table`.`field7` = \'value\' AND `table`.`field8` = \'value\''
                . ' AND `table`.`field9` = CAST(0x76616c7565 AS BINARY)'
                . ' AND `table`.`field10` = CAST(0x76616c7565 AS BINARY)'
                . ' AND `table`.`field12` = b\'0001\'',
                false,
                [
                    '`table`.`field1`' => 'IS NULL',
                    '`table`.`field2`' => '= \'value\\\'s\'',
                    '`table`.`field3`' => '= 123456',
                    '`table`.`field4`' => '= 123.456',
                    '`table`.`field5`' => '= CAST(0x76616c7565 AS BINARY)',
                    '`table`.`field7`' => '= \'value\'',
                    '`table`.`field8`' => '= \'value\'',
                    '`table`.`field9`' => '= CAST(0x76616c7565 AS BINARY)',
                    '`table`.`field10`' => '',
                    '`table`.`field12`' => '= b\'0001\'',
                ],
            ],
            $actual
        );
    }

    public function testGetUniqueConditionWithSingleBigBinaryField(): void
    {
        $meta = [
            new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) [
                'name' => 'field',
                'table' => 'table',
                'orgtable' => 'table',
                'charsetnr' => 63, // binary
            ]),
        ];

        $actual = Util::getUniqueCondition(1, $meta, [str_repeat('*', 1001)]);
        $this->assertEquals(
            ['CHAR_LENGTH(`table`.`field`)  = 1001', false, ['`table`.`field`' => ' = 1001']],
            $actual
        );
    }

    public function testGetUniqueConditionWithPrimaryKey(): void
    {
        $meta = [
            new FieldMetadata(MYSQLI_TYPE_LONG, MYSQLI_PRI_KEY_FLAG | MYSQLI_NUM_FLAG, (object) [
                'name' => 'id',
                'table' => 'table',
                'orgtable' => 'table',
            ]),
            new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) [
                'name' => 'field',
                'table' => 'table',
                'orgtable' => 'table',
            ]),
        ];

        $actual = Util::getUniqueCondition(count($meta), $meta, [1, 'value']);
        $this->assertEquals(['`table`.`id` = 1', true, ['`table`.`id`' => '= 1']], $actual);
    }

    public function testGetUniqueConditionWithUniqueKey(): void
    {
        $meta = [
            new FieldMetadata(MYSQLI_TYPE_STRING, MYSQLI_UNIQUE_KEY_FLAG, (object) [
                'name' => 'id',
                'table' => 'table',
                'orgtable' => 'table',
            ]),
            new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) [
                'name' => 'field',
                'table' => 'table',
                'orgtable' => 'table',
            ]),
        ];

        $actual = Util::getUniqueCondition(count($meta), $meta, ['unique', 'value']);
        $this->assertEquals(['`table`.`id` = \'unique\'', true, ['`table`.`id`' => '= \'unique\'']], $actual);
    }

    /**
     * Test for Util::getUniqueCondition
     * note: GROUP_FLAG = MYSQLI_NUM_FLAG = 32769
     *
     * @param FieldMetadata[] $meta     Meta Information for Field
     * @param array           $row      Current Ddata Row
     * @param array           $expected Expected Result
     * @psalm-param array<int, mixed> $row
     * @psalm-param array{string, bool, array<string, string>} $expected
     *
     * @dataProvider providerGetUniqueConditionForGroupFlag
     */
    public function testGetUniqueConditionForGroupFlag(array $meta, array $row, array $expected): void
    {
        $fieldsCount = count($meta);
        $actual = Util::getUniqueCondition($fieldsCount, $meta, $row);

        $this->assertEquals($expected, $actual);
    }

    /**
     * Provider for testGetUniqueConditionForGroupFlag
     *
     * @return array<string, array{FieldMetadata[], array<int, mixed>, array{string, bool, array<string, string>}}>
     */
    public function providerGetUniqueConditionForGroupFlag(): array
    {
        return [
            'field type is integer, value is number - not escape string' => [
                [
                    new FieldMetadata(FIELD_TYPE_INTEGER, MYSQLI_NUM_FLAG, (object) [
                        'name' => 'col',
                        'table' => 'table',
                        'orgtable' => 'table',
                    ]),
                ],
                [123],
                [
                    '`table`.`col` = 123',
                    false,
                    ['`table`.`col`' => '= 123'],
                ],
            ],
            'field type is unknown, value is string - escape string' => [
                [
                    new FieldMetadata(FIELD_TYPE_UNKNOWN, MYSQLI_NUM_FLAG, (object) [
                        'name' => 'col',
                        'table' => 'table',
                        'orgtable' => 'table',
                    ]),
                ],
                ['test'],
                [
                    "`table`.`col` = 'test'",
                    false,
                    ['`table`.`col`' => "= 'test'"],
                ],
            ],
            'field type is varchar, value is string - escape string' => [
                [
                    new FieldMetadata(FIELD_TYPE_VARCHAR, MYSQLI_NUM_FLAG, (object) [
                        'name' => 'col',
                        'table' => 'table',
                        'orgtable' => 'table',
                    ]),
                ],
                ['test'],
                [
                    "`table`.`col` = 'test'",
                    false,
                    ['`table`.`col`' => "= 'test'"],
                ],
            ],
            'field type is varchar, value is string with double quote - escape string' => [
                [
                    new FieldMetadata(FIELD_TYPE_VARCHAR, MYSQLI_NUM_FLAG, (object) [
                        'name' => 'col',
                        'table' => 'table',
                        'orgtable' => 'table',
                    ]),
                ],
                ['"test"'],
                [
                    "`table`.`col` = '\\\"test\\\"'",
                    false,
                    ['`table`.`col`' => "= '\\\"test\\\"'"],
                ],
            ],
            'field type is varchar, value is string with single quote - escape string' => [
                [
                    new FieldMetadata(FIELD_TYPE_VARCHAR, MYSQLI_NUM_FLAG, (object) [
                        'name' => 'col',
                        'table' => 'table',
                        'orgtable' => 'table',
                    ]),
                ],
                ["'test'"],
                [
                    "`table`.`col` = '\'test\''",
                    false,
                    ['`table`.`col`' => "= '\'test\''"],
                ],
            ],
            'group by multiple columns and field type is mixed' => [
                [
                    new FieldMetadata(FIELD_TYPE_VARCHAR, MYSQLI_NUM_FLAG, (object) [
                        'name' => 'col',
                        'table' => 'table',
                        'orgtable' => 'table',
                    ]),
                    new FieldMetadata(FIELD_TYPE_INTEGER, MYSQLI_NUM_FLAG, (object) [
                        'name' => 'status_id',
                        'table' => 'table',
                        'orgtable' => 'table',
                    ]),
                ],
                ['test', 2],
                [
                    "`table`.`col` = 'test' AND `table`.`status_id` = 2",
                    false,
                    [
                        '`table`.`col`' => "= 'test'",
                        '`table`.`status_id`' => '= 2',
                    ],
                ],
            ],
        ];
    }

    /**
     * Test for Page Selector
     */
    public function testPageSelector(): void
    {
        $this->assertStringContainsString(
            '<select class="pageselector ajax" name="pma" >',
            Util::pageselector('pma', 3)
        );

        // If pageNow > nbTotalPage, show the pageNow number to avoid confusion
        $this->assertStringContainsString(
            '<option selected="selected" style="font-weight: bold" value="297">100</option>',
            Util::pageselector('pma', 3, 100, 50)
        );
    }

    /**
     * Test for getCharsetQueryPart
     *
     * @param string $collation Collation
     * @param string $expected  Expected Charset Query
     *
     * @dataProvider charsetQueryData
     */
    public function testGenerateCharsetQueryPart(string $collation, string $expected): void
    {
        $this->assertEquals(
            $expected,
            Util::getCharsetQueryPart($collation)
        );
    }

    /**
     * Data Provider for testgetCharsetQueryPart
     *
     * @return array test data
     */
    public function charsetQueryData(): array
    {
        return [
            [
                'a_b_c_d',
                ' CHARSET=a COLLATE a_b_c_d',
            ],
            [
                'a_',
                ' CHARSET=a COLLATE a_',
            ],
            [
                'a',
                ' CHARSET=a',
            ],
        ];
    }

    /**
     * Test for random generation
     */
    public function testGenerateRandom(): void
    {
        $this->assertEquals(32, strlen(Util::generateRandom(32)));
        $this->assertEquals(16, strlen(Util::generateRandom(16)));
    }

    public function testClearUserCache(): void
    {
        $GLOBALS['server'] = 'server';
        SessionCache::set('is_superuser', 'yes');
        $this->assertEquals('yes', $_SESSION['cache']['server_server']['is_superuser']);

        SessionCache::set('mysql_cur_user', 'mysql');
        $this->assertEquals(
            'mysql',
            $_SESSION['cache']['server_server']['mysql_cur_user']
        );

        Util::clearUserCache();
        $this->assertArrayNotHasKey('is_superuser', $_SESSION['cache']['server_server']);
        $this->assertArrayNotHasKey(
            'mysql_cur_user',
            $_SESSION['cache']['server_server']
        );
    }

    public function testCheckParameterMissing(): void
    {
        parent::setGlobalConfig();
        $_REQUEST = [];
        $GLOBALS['text_dir'] = 'ltr';
        $GLOBALS['PMA_PHP_SELF'] = Core::getenv('PHP_SELF');
        $GLOBALS['db'] = 'db';
        $GLOBALS['table'] = 'table';
        $GLOBALS['server'] = 1;
        $GLOBALS['cfg']['ServerDefault'] = 1;
        $GLOBALS['cfg']['AllowThirdPartyFraming'] = false;
        ResponseRenderer::getInstance()->setAjax(false);

        $this->expectOutputRegex('/Missing parameter: field/');

        Util::checkParameters(
            [
                'db',
                'table',
                'field',
            ]
        );
    }

    public function testCheckParameter(): void
    {
        parent::setGlobalConfig();
        $GLOBALS['cfg'] = ['ServerDefault' => 1];
        $GLOBALS['text_dir'] = 'ltr';
        $GLOBALS['PMA_PHP_SELF'] = Core::getenv('PHP_SELF');
        $GLOBALS['db'] = 'dbDatabase';
        $GLOBALS['table'] = 'tblTable';
        $GLOBALS['field'] = 'test_field';
        $GLOBALS['sql_query'] = 'SELECT * FROM tblTable;';

        $this->expectOutputString('');
        Util::checkParameters(
            [
                'db',
                'table',
                'field',
                'sql_query',
            ]
        );
    }

    /**
     * Test for Util::convertBitDefaultValue
     *
     * @param string|null $bit Value
     * @param string      $val Expected value
     *
     * @dataProvider providerConvertBitDefaultValue
     */
    public function testConvertBitDefaultValue(?string $bit, string $val): void
    {
        $this->assertEquals(
            $val,
            Util::convertBitDefaultValue($bit)
        );
    }

    /**
     * Provider for testConvertBitDefaultValue
     *
     * @return array
     */
    public function providerConvertBitDefaultValue(): array
    {
        return [
            [
                null,
                '',
            ],
            [
                "b'",
                '',
            ],
            [
                "b'01'",
                '01',
            ],
            [
                "b'010111010'",
                '010111010',
            ],
            'database name starting with b' => [
                'big database',
                'big database',
            ],
            "database name containing b'" => [
                "a b'ig database",
                "a b'ig database",
            ],
            'database name in single quotes' => [
                "'a*database*name'",
                "'a*database*name'",
            ],
            "database name with multiple b'" => [
                "b'ens datab'ase'",
                "b'ens datab'ase'",
            ],
        ];
    }

    /**
     * data provider for testEscapeMysqlWildcards and testUnescapeMysqlWildcards
     *
     * @return array
     */
    public function providerUnEscapeMysqlWildcards(): array
    {
        return [
            [
                '\_test',
                '_test',
            ],
            [
                '\_\\',
                '_\\',
            ],
            [
                '\\_\%',
                '_%',
            ],
            [
                '\\\_',
                '\_',
            ],
            [
                '\\\_\\\%',
                '\_\%',
            ],
            [
                '\_\\%\_\_\%',
                '_%__%',
            ],
            [
                '\%\_',
                '%_',
            ],
            [
                '\\\%\\\_',
                '\%\_',
            ],
        ];
    }

    /**
     * PhpMyAdmin\Util::escapeMysqlWildcards tests
     *
     * @param string $a Expected value
     * @param string $b String to escape
     *
     * @dataProvider providerUnEscapeMysqlWildcards
     */
    public function testEscapeMysqlWildcards(string $a, string $b): void
    {
        $this->assertEquals(
            $a,
            Util::escapeMysqlWildcards($b)
        );
    }

    /**
     * PhpMyAdmin\Util::unescapeMysqlWildcards tests
     *
     * @param string $a String to unescape
     * @param string $b Expected value
     *
     * @dataProvider providerUnEscapeMysqlWildcards
     */
    public function testUnescapeMysqlWildcards(string $a, string $b): void
    {
        $this->assertEquals(
            $b,
            Util::unescapeMysqlWildcards($a)
        );
    }

    /**
     * Test case for expanding strings
     *
     * @param string $in  string to evaluate
     * @param string $out expected output
     *
     * @dataProvider providerExpandUserString
     */
    public function testExpandUserString(string $in, string $out): void
    {
        parent::setGlobalConfig();
        $GLOBALS['cfg'] = [
            'Server' => [
                'host' => 'host&',
                'verbose' => 'verbose',
            ],
        ];
        $GLOBALS['db'] = 'database';
        $GLOBALS['table'] = 'table';

        $this->assertEquals(
            $out,
            Util::expandUserString($in)
        );

        $this->assertEquals(
            htmlspecialchars($out),
            Util::expandUserString(
                $in,
                'htmlspecialchars'
            )
        );
    }

    /**
     * Data provider for testExpandUserString
     *
     * @return array
     */
    public function providerExpandUserString(): array
    {
        return [
            [
                '@SERVER@',
                'host&',
            ],
            [
                '@VSERVER@',
                'verbose',
            ],
            [
                '@DATABASE@',
                'database',
            ],
            [
                '@TABLE@',
                'table',
            ],
            [
                '@IGNORE@',
                '@IGNORE@',
            ],
            [
                '@PHPMYADMIN@',
                'phpMyAdmin ' . Version::VERSION,
            ],
        ];
    }

    /**
     * Test case for parsing SHOW COLUMNS output
     *
     * @param string $in  Column specification
     * @param array  $out Expected value
     *
     * @dataProvider providerExtractColumnSpec
     */
    public function testExtractColumnSpec(string $in, array $out): void
    {
        $GLOBALS['cfg']['LimitChars'] = 1000;

        $this->assertEquals(
            $out,
            Util::extractColumnSpec($in)
        );
    }

    /**
     * Data provider for testExtractColumnSpec
     *
     * @return array
     */
    public function providerExtractColumnSpec(): array
    {
        return [
            [
                "SET('a','b')",
                [
                    'type' => 'set',
                    'print_type' => "set('a', 'b')",
                    'binary' => false,
                    'unsigned' => false,
                    'zerofill' => false,
                    'spec_in_brackets' => "'a','b'",
                    'enum_set_values' => [
                        'a',
                        'b',
                    ],
                    'attribute' => ' ',
                    'can_contain_collation' => true,
                    'displayed_type' => "set('a', 'b')",
                ],
            ],
            [
                "SET('\'a','b')",
                [
                    'type' => 'set',
                    'print_type' => "set('\'a', 'b')",
                    'binary' => false,
                    'unsigned' => false,
                    'zerofill' => false,
                    'spec_in_brackets' => "'\'a','b'",
                    'enum_set_values' => [
                        "'a",
                        'b',
                    ],
                    'attribute' => ' ',
                    'can_contain_collation' => true,
                    'displayed_type' => "set('\'a', 'b')",
                ],
            ],
            [
                "SET('''a','b')",
                [
                    'type' => 'set',
                    'print_type' => "set('''a', 'b')",
                    'binary' => false,
                    'unsigned' => false,
                    'zerofill' => false,
                    'spec_in_brackets' => "'''a','b'",
                    'enum_set_values' => [
                        "'a",
                        'b',
                    ],
                    'attribute' => ' ',
                    'can_contain_collation' => true,
                    'displayed_type' => "set('''a', 'b')",
                ],
            ],
            [
                "ENUM('a&b', 'b''c\\'d', 'e\\\\f')",
                [
                    'type' => 'enum',
                    'print_type' => "enum('a&b', 'b''c\\'d', 'e\\\\f')",
                    'binary' => false,
                    'unsigned' => false,
                    'zerofill' => false,
                    'spec_in_brackets' => "'a&b', 'b''c\\'d', 'e\\\\f'",
                    'enum_set_values' => [
                        'a&b',
                        'b\'c\'d',
                        'e\\f',
                    ],
                    'attribute' => ' ',
                    'can_contain_collation' => true,
                    'displayed_type' => "enum('a&amp;b', 'b''c\\'d', 'e\\\\f')",
                ],
            ],
            [
                'INT UNSIGNED zerofill',
                [
                    'type' => 'int',
                    'print_type' => 'int',
                    'binary' => false,
                    'unsigned' => true,
                    'zerofill' => true,
                    'spec_in_brackets' => '',
                    'enum_set_values' => [],
                    'attribute' => 'UNSIGNED ZEROFILL',
                    'can_contain_collation' => false,
                    'displayed_type' => 'int',
                ],
            ],
            [
                'VARCHAR(255)',
                [
                    'type' => 'varchar',
                    'print_type' => 'varchar(255)',
                    'binary' => false,
                    'unsigned' => false,
                    'zerofill' => false,
                    'spec_in_brackets' => '255',
                    'enum_set_values' => [],
                    'attribute' => ' ',
                    'can_contain_collation' => true,
                    'displayed_type' => 'varchar(255)',
                ],
            ],
            [
                'VARBINARY(255)',
                [
                    'type' => 'varbinary',
                    'print_type' => 'varbinary(255)',
                    'binary' => false,
                    'unsigned' => false,
                    'zerofill' => false,
                    'spec_in_brackets' => '255',
                    'enum_set_values' => [],
                    'attribute' => ' ',
                    'can_contain_collation' => false,
                    'displayed_type' => 'varbinary(255)',
                ],
            ],
            [
                'varchar(11) /*!100301 COMPRESSED*/',
                [
                    'type' => 'varchar',
                    'print_type' => 'varchar(11)',
                    'binary' => false,
                    'unsigned' => false,
                    'zerofill' => false,
                    'spec_in_brackets' => '11',
                    'enum_set_values' => [],
                    'attribute' => 'COMPRESSED=zlib',
                    'can_contain_collation' => true,
                    'displayed_type' => 'varchar(11)',
                ],
            ],
        ];
    }

    /**
     * Test for Util::extractValueFromFormattedSize
     *
     * @param int|string $size     Size
     * @param int|float  $expected Expected value (float on some cpu architectures)
     *
     * @dataProvider providerExtractValueFromFormattedSize
     */
    public function testExtractValueFromFormattedSize($size, $expected): void
    {
        $this->assertEquals(
            $expected,
            Util::extractValueFromFormattedSize($size)
        );
    }

    /**
     * Data provider for testExtractValueFromFormattedSize
     *
     * @return array
     */
    public function providerExtractValueFromFormattedSize(): array
    {
        return [
            [
                100,
                -1,
            ],
            [
                '10GB',
                10737418240,
            ],
            [
                '15MB',
                15728640,
            ],
            [
                '256K',
                262144,
            ],
        ];
    }

    /**
     * format byte test, globals are defined
     *
     * @param float|int|string $a Value to format
     * @param int              $b Sensitiveness
     * @param int              $c Number of decimals to retain
     * @param array            $e Expected value
     *
     * @dataProvider providerFormatByteDown
     */
    public function testFormatByteDown($a, int $b, int $c, array $e): void
    {
        $result = Util::formatByteDown($a, $b, $c);
        $this->assertIsArray($result);
        $result[0] = trim($result[0]);
        $this->assertSame($e, $result);
    }

    /**
     * format byte down data provider
     *
     * @return array
     */
    public function providerFormatByteDown(): array
    {
        return [
            [
                '0',
                6,
                0,
                [
                    '0',
                    __('B'),
                ],
            ],
            [
                'A4',
                6,
                0,
                [
                    '0',
                    __('B'),
                ],
            ],
            [
                10,
                2,
                2,
                [
                    '10',
                    __('B'),
                ],
            ],
            [
                100,
                2,
                0,
                [
                    '0',
                    __('KiB'),
                ],
            ],
            [
                100,
                3,
                0,
                [
                    '100',
                    __('B'),
                ],
            ],
            [
                100,
                2,
                2,
                [
                    '0.10',
                    __('KiB'),
                ],
            ],
            [
                1034,
                3,
                2,
                [
                    '1.01',
                    __('KiB'),
                ],
            ],
            [
                100233,
                3,
                3,
                [
                    '97.884',
                    __('KiB'),
                ],
            ],
            [
                '100233',
                3,
                3,
                [
                    '97.884',
                    __('KiB'),
                ],
            ],
            [
                '102400K',
                3,
                3,
                [
                    '100.000',
                    __('KiB'),
                ],
            ],
            [
                '102401K',
                3,
                3,
                [
                    '100.001',
                    __('KiB'),
                ],
            ],
            [
                '153600K',
                3,
                3,
                [
                    '150.000',
                    __('KiB'),
                ],
            ],
            [
                '153600K',
                3,
                0,
                [
                    '150',
                    __('KiB'),
                ],
            ],
            [
                102400 * 1024,
                3,
                0,
                [
                    '100',
                    __('MiB'),
                ],
            ],
            [
                2206451,
                1,
                2,
                [
                    '2.10',
                    __('MiB'),
                ],
            ],
            [
                21474836480,
                4,
                0,
                [
                    '20',
                    __('GiB'),
                ],
            ],
            [
                floatval(52) + floatval(2048),
                3,
                1,
                [
                    '2.1',
                    'KiB',
                ],
            ],
            [
                '' . (floatval(52) + floatval(2048)),
                3,
                1,
                [
                    '2.1',
                    'KiB',
                ],
            ],
        ];
    }

    /**
     * Core test for formatNumber
     *
     * @param float|int|string $a Value to format
     * @param int              $b Sensitiveness
     * @param int              $c Number of decimals to retain
     * @param string           $d Expected value
     */
    private function assertFormatNumber($a, int $b, int $c, string $d): void
    {
        $this->assertEquals(
            $d,
            (string) Util::formatNumber(
                $a,
                $b,
                $c,
                false
            )
        );
    }

    /**
     * format number test, globals are defined
     *
     * @param float|int|string $a Value to format
     * @param int              $b Sensitiveness
     * @param int              $c Number of decimals to retain
     * @param string           $d Expected value
     *
     * @dataProvider providerFormatNumber
     */
    public function testFormatNumber($a, int $b, int $c, string $d): void
    {
        $this->assertFormatNumber($a, $b, $c, $d);

        // Test with various precisions
        $old_precision = (string) ini_get('precision');
        try {
            ini_set('precision', '20');
            $this->assertFormatNumber($a, $b, $c, $d);
            ini_set('precision', '14');
            $this->assertFormatNumber($a, $b, $c, $d);
            ini_set('precision', '10');
            $this->assertFormatNumber($a, $b, $c, $d);
            ini_set('precision', '5');
            $this->assertFormatNumber($a, $b, $c, $d);
            ini_set('precision', '-1');
            $this->assertFormatNumber($a, $b, $c, $d);
        } finally {
            ini_set('precision', $old_precision);
        }

        // Test with different translations
        $translator = Loader::getInstance()->getTranslator();

        try {
            // German
            $translator->setTranslation(',', '.');
            $translator->setTranslation('.', ',');
            $expected = str_replace([',', 'X'], ['.', ','], str_replace('.', 'X', $d));
            $this->assertFormatNumber($a, $b, $c, $expected);

            // Czech
            $translator->setTranslation(',', ' ');
            $translator->setTranslation('.', ',');
            $expected = str_replace([',', 'X'], [' ', ','], str_replace('.', 'X', $d));
            $this->assertFormatNumber($a, $b, $c, $expected);
        } finally {
            // Restore
            $translator->setTranslation(',', ',');
            $translator->setTranslation('.', '.');
        }
    }

    /**
     * format number data provider
     *
     * @return array
     */
    public function providerFormatNumber(): array
    {
        return [
            [
                10,
                2,
                2,
                '10  ',
            ],
            [
                100,
                2,
                0,
                '100  ',
            ],
            [
                100,
                2,
                2,
                '100  ',
            ],
            [
                '100',
                2,
                2,
                '100  ',
            ],
            [
                -1000.454,
                4,
                2,
                '-1,000.45  ',
            ],
            [
                '-1000.454',
                4,
                2,
                '-1,000.45  ',
            ],
            [
                0.00003,
                3,
                2,
                '30 µ',
            ],
            [
                0.003,
                3,
                3,
                '3 m',
            ],
            [
                -0.003,
                6,
                0,
                '-3,000 µ',
            ],
            [
                100.98,
                0,
                2,
                '100.98',
            ],
            [
                21010101,
                0,
                2,
                '21,010,101.00',
            ],
            [
                1100000000,
                5,
                0,
                '1,100 M',
            ],
            [
                '1100000000',
                5,
                0,
                '1,100 M',
            ],
            [
                20000,
                2,
                2,
                '20 k',
            ],
            [
                20011,
                2,
                2,
                '20.01 k',
            ],
            [
                123456789,
                6,
                0,
                '123,457 k',
            ],
            [
                -123456789,
                4,
                2,
                '-123.46 M',
            ],
            [
                0,
                6,
                0,
                '0',
            ],
        ];
    }

    /**
     * Test for Util::getFormattedMaximumUploadSize
     *
     * @param int|float $size Size (float on some cpu architectures)
     * @param string    $unit Unit
     * @param string    $res  Result
     *
     * @dataProvider providerGetFormattedMaximumUploadSize
     */
    public function testGetFormattedMaximumUploadSize($size, string $unit, string $res): void
    {
        $this->assertEquals(
            '(' . __('Max: ') . $res . $unit . ')',
            Util::getFormattedMaximumUploadSize($size)
        );
    }

    /**
     * Data provider for testGetFormattedMaximumUploadSize
     *
     * @return array
     */
    public function providerGetFormattedMaximumUploadSize(): array
    {
        return [
            [
                10,
                __('B'),
                '10',
            ],
            [
                100,
                __('B'),
                '100',
            ],
            [
                1024,
                __('B'),
                '1,024',
            ],
            [
                102400,
                __('KiB'),
                '100',
            ],
            [
                10240000,
                __('MiB'),
                '10',
            ],
            [
                2147483648,
                __('MiB'),
                '2,048',
            ],
            [
                21474836480,
                __('GiB'),
                '20',
            ],
            [
                '153600K',
                __('KiB'),
                '150',
            ],
            [
                '157286400',
                __('MiB'),
                '150',
            ],
            [
                // Equals to Core::getRealSize of '102400K'
                // according to PHP FAQ on "shorthandbytes"
                102400 * 1024,
                __('MiB'),
                '100',
            ],
        ];
    }

    /**
     * Test for Util::getTitleForTarget
     *
     * @param string $target Target
     * @param string $result Expected value
     *
     * @dataProvider providerGetTitleForTarget
     */
    public function testGetTitleForTarget(string $target, string $result): void
    {
        $this->assertEquals(
            $result,
            Util::getTitleForTarget($target)
        );
    }

    /**
     * Data provider for testGetTitleForTarget
     *
     * @return array
     */
    public function providerGetTitleForTarget(): array
    {
        return [
            [
                'structure',
                __('Structure'),
            ],
            [
                'sql',
                __('SQL'),
            ],
            [
                'search',
                __('Search'),
            ],
            [
                'insert',
                __('Insert'),
            ],
            [
                'browse',
                __('Browse'),
            ],
            [
                'operations',
                __('Operations'),
            ],
        ];
    }

    /**
     * localised date test, globals are defined
     *
     * @param int    $a      Current timestamp
     * @param string $b      Format
     * @param string $e      Expected output
     * @param string $tz     Timezone to set
     * @param string $locale Locale to set
     *
     * @dataProvider providerLocalisedDate
     */
    public function testLocalisedDate(int $a, string $b, string $e, string $tz, string $locale): void
    {
        // A test case for #15830 could be added for using the php setlocale on a Windows CI
        // See https://github.com/phpmyadmin/phpmyadmin/issues/15830
        _setlocale(LC_ALL, $locale);
        $tmpTimezone = date_default_timezone_get();
        date_default_timezone_set($tz);

        $this->assertEquals(
            $e,
            Util::localisedDate($a, $b)
        );

        date_default_timezone_set($tmpTimezone);
        _setlocale(LC_ALL, 'en');
    }

    /**
     * data provider for localised date test
     *
     * @return array
     */
    public function providerLocalisedDate(): array
    {
        $hasJaTranslations = file_exists(LOCALE_PATH . '/cs/LC_MESSAGES/phpmyadmin.mo');

        return [
            [
                1227455558,
                '',
                'Nov 23, 2008 at 03:52 PM',
                'Europe/London',
                'en',
            ],
            [
                1227455558,
                '%Y-%m-%d %H:%M:%S %a',
                '2008-11-23 15:52:38 Sun',
                'Europe/London',
                'en',
            ],
            [
                1227455558,
                '%Y-%m-%d %H:%M:%S %a',
                '2008-11-23 16:52:38 Sun',
                'Europe/Paris',
                'en',
            ],
            [
                1227455558,
                '%Y-%m-%d %H:%M:%S %a',
                '2008-11-24 00:52:38 Mon',
                'Asia/Tokyo',
                'en',
            ],
            [
                1227455558,
                '%a %A %b %B',
                'Mon Mon Nov Nov',
                'Asia/Tokyo',
                'en',
            ],
            [
                1227455558,
                '%a %A %b %B %P',
                'Mon Mon Nov Nov AM',
                'Asia/Tokyo',
                'en',
            ],
            [
                1227455558,
                '%Y-%m-%d %H:%M:%S %a',
                $hasJaTranslations ? '2008-11-24 00:52:38 月' : '2008-11-24 00:52:38 Mon',
                'Asia/Tokyo',
                'ja',
            ],
            [
                1227455558,
                '%a %A %b %B',
                $hasJaTranslations ? '月 月 11 月 11 月' : 'Mon Mon Nov Nov',
                'Asia/Tokyo',
                'ja',
            ],
            [
                1227455558,
                '%a %A %b %B %P',
                $hasJaTranslations ? '月 月 11 月 11 月 午前' : 'Mon Mon Nov Nov AM',
                'Asia/Tokyo',
                'ja',
            ],
            [
                1227455558,
                '月月',
                '月月',
                'Asia/Tokyo',
                'ja',
            ],
            [
                1227455558,
                '%Y 年 2 月 %d 日 %H:%M',
                '2008 年 2 月 24 日 00:52',
                'Asia/Tokyo',
                'ja',
            ],
            [
                1227455558,
                '%Y 年 2 � %d 日 %H:%M',
                '2008 年 2 � 24 日 00:52',
                'Asia/Tokyo',
                'ja',
            ],
            [
                1617153941,
                'H:i:s Y-d-m',
                'H:i:s Y-d-m',// Not a valid strftime format
                'Europe/Paris',
                'fr',
            ],
            [
                1617153941,
                '',
                'mer. 31 mars 2021 à 03:25',// No format uses format "%B %d, %Y at %I:%M %p"
                'Europe/Paris',
                'fr',
            ],
        ];
    }

    /**
     * localised timestamp test, globals are defined
     *
     * @param int    $a Timespan in seconds
     * @param string $e Expected output
     *
     * @dataProvider providerTimespanFormat
     */
    public function testTimespanFormat(int $a, string $e): void
    {
        $GLOBALS['timespanfmt'] = '%s days, %s hours, %s minutes and %s seconds';
        $tmpTimezone = date_default_timezone_get();
        date_default_timezone_set('Europe/London');

        $this->assertEquals(
            $e,
            Util::timespanFormat($a)
        );

        date_default_timezone_set($tmpTimezone);
    }

    /**
     * data provider for localised timestamp test
     *
     * @return array
     */
    public function providerTimespanFormat(): array
    {
        return [
            [
                1258,
                '0 days, 0 hours, 20 minutes and 58 seconds',
            ],
            [
                821958,
                '9 days, 12 hours, 19 minutes and 18 seconds',
            ],
        ];
    }

    /**
     * test for generating string contains printable bit value of selected data
     *
     * @param int    $a Value
     * @param int    $b Length
     * @param string $e Expected output
     *
     * @dataProvider providerPrintableBitValue
     */
    public function testPrintableBitValue(int $a, int $b, string $e): void
    {
        $this->assertEquals(
            $e,
            Util::printableBitValue($a, $b)
        );
    }

    /**
     * data provider for printable bit value test
     *
     * @return array
     */
    public function providerPrintableBitValue(): array
    {
        return [
            [
                20131009,
                64,
                '0000000000000000000000000000000000000001001100110010110011000001',
            ],
            [
                5,
                32,
                '00000000000000000000000000000101',
            ],
        ];
    }

    /**
     * PhpMyAdmin\Util::unQuote test
     *
     * @param string $param    String
     * @param string $expected Expected output
     *
     * @dataProvider providerUnQuote
     */
    public function testUnQuote(string $param, string $expected): void
    {
        $this->assertEquals(
            $expected,
            Util::unQuote($param)
        );
    }

    /**
     * data provider for PhpMyAdmin\Util::unQuote test
     *
     * @return array
     */
    public function providerUnQuote(): array
    {
        return [
            [
                '"test\'"',
                "test'",
            ],
            [
                "'test''",
                "test'",
            ],
            [
                "`test'`",
                "test'",
            ],
            [
                "'test'test",
                "'test'test",
            ],
        ];
    }

    /**
     * PhpMyAdmin\Util::unQuote test with chosen quote
     *
     * @param string $param    String
     * @param string $expected Expected output
     *
     * @dataProvider providerUnQuoteSelectedChar
     */
    public function testUnQuoteSelectedChar(string $param, string $expected): void
    {
        $this->assertEquals(
            $expected,
            Util::unQuote($param, '"')
        );
    }

    /**
     * data provider for PhpMyAdmin\Util::unQuote test with chosen quote
     *
     * @return array
     */
    public function providerUnQuoteSelectedChar(): array
    {
        return [
            [
                '"test\'"',
                "test'",
            ],
            [
                "'test''",
                "'test''",
            ],
            [
                "`test'`",
                "`test'`",
            ],
            [
                "'test'test",
                "'test'test",
            ],
        ];
    }

    /**
     * @dataProvider providerForTestBackquote
     */
    public function testBackquote(?string $entry, string $expectedNoneOutput, string $expectedMssqlOutput): void
    {
        $this->assertSame($expectedNoneOutput, Util::backquote($entry));
        $this->assertEquals($entry, Util::backquoteCompat($entry, 'NONE', false));
        $this->assertEquals($entry, Util::backquoteCompat($entry, 'MSSQL', false));
        $this->assertSame($expectedNoneOutput, Util::backquoteCompat($entry, 'NONE'));
        $this->assertSame($expectedMssqlOutput, Util::backquoteCompat($entry, 'MSSQL'));
    }

    /**
     * @return array<int|string, string|null>[]
     */
    public function providerForTestBackquote(): array
    {
        return [
            [
                '0',
                '`0`',
                '"0"',
            ],
            [
                'test',
                '`test`',
                '"test"',
            ],
            [
                'te`st',
                '`te``st`',
                '"te`st"',
            ],
            [
                'te"st',
                '`te"st`',
                '"te\"st"',
            ],
            [
                '',
                '',
                '',
            ],
            [
                '*',
                '*',
                '*',
            ],
            [
                null,
                '',
                '',
            ],
        ];
    }

    /**
     * backquoteCompat test with forbidden words
     */
    public function testBackquoteForbidenWords(): void
    {
        foreach (Context::$KEYWORDS as $keyword => $type) {
            if ($type & Token::FLAG_KEYWORD_RESERVED) {
                $this->assertEquals(
                    '`' . $keyword . '`',
                    Util::backquoteCompat($keyword, 'NONE', false)
                );
            } else {
                $this->assertEquals(
                    $keyword,
                    Util::backquoteCompat($keyword, 'NONE', false)
                );
            }
        }
    }

    /**
     * test of generating user dir, globals are defined
     *
     * @param string $a String
     * @param string $e Expected output
     *
     * @dataProvider providerUserDir
     */
    public function testUserDir(string $a, string $e): void
    {
        $GLOBALS['cfg']['Server']['user'] = 'root';

        $this->assertEquals($e, Util::userDir($a));
    }

    /**
     * data provider for PhpMyAdmin\Util::userDir test
     *
     * @return array
     */
    public function providerUserDir(): array
    {
        return [
            [
                '/var/pma_tmp/%u/',
                '/var/pma_tmp/root/',
            ],
            [
                '/home/%u/pma',
                '/home/root/pma/',
            ],
        ];
    }

    /**
     * duplicate first newline test
     *
     * @param string $a String
     * @param string $e Expected output
     *
     * @dataProvider providerDuplicateFirstNewline
     */
    public function testDuplicateFirstNewline(string $a, string $e): void
    {
        $this->assertEquals(
            $e,
            Util::duplicateFirstNewline($a)
        );
    }

    /**
     * data provider for duplicate first newline test
     *
     * @return array
     */
    public function providerDuplicateFirstNewline(): array
    {
        return [
            [
                'test',
                'test',
            ],
            [
                "\r\ntest",
                "\n\r\ntest",
            ],
            [
                "\ntest",
                "\ntest",
            ],
            [
                "\n\r\ntest",
                "\n\r\ntest",
            ],
        ];
    }

    public function testUnsupportedDatatypes(): void
    {
        $no_support_types = [];
        $this->assertEquals(
            $no_support_types,
            Util::unsupportedDatatypes()
        );
    }

    public function testGetPageFromPosition(): void
    {
        $this->assertEquals(Util::getPageFromPosition(0, 1), 1);
        $this->assertEquals(Util::getPageFromPosition(1, 1), 2);
        $this->assertEquals(Util::getPageFromPosition(1, 2), 1);
        $this->assertEquals(Util::getPageFromPosition(1, 6), 1);
    }

    /**
     * Test for Util::isInteger
     *
     * @param bool  $expected Expected result for a given input
     * @param mixed $input    Input data to check
     *
     * @dataProvider providerIsInteger
     */
    public function testIsInteger(bool $expected, $input): void
    {
        $isInteger = Util::isInteger($input);
        $this->assertEquals($expected, $isInteger);
    }

    /**
     * Data provider for Util::isInteger test
     *
     * @return array
     */
    public function providerIsInteger(): array
    {
        return [
            [
                true,
                1000,
            ],
            [
                true,
                '1000',
            ],
            [
                false,
                1000.1,
            ],
            [
                false,
                '1000.1',
            ],
            [
                false,
                'input',
            ],
        ];
    }

    /**
     * Test for Util::getProtoFromForwardedHeader
     *
     * @param string $header The http Forwarded header
     * @param string $proto  The protocol http/https
     *
     * @dataProvider providerForwardedHeaders
     */
    public function testGetProtoFromForwardedHeader(string $header, string $proto): void
    {
        $protocolDetected = Util::getProtoFromForwardedHeader($header);
        $this->assertEquals($proto, $protocolDetected);
    }

    /**
     * Data provider for Util::getProtoFromForwardedHeader test
     *
     * @return array
     *
     * @source https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded MDN docs
     * @source https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/ Nginx docs
     */
    public function providerForwardedHeaders(): array
    {
        return [
            [
                '',
                '',
            ],
            [
                '=',
                '',
            ],
            [
                'https',
                '',
            ],
            [
                'https',
                '',
            ],
            [
                '=https',
                '',
            ],
            [
                '=http',
                '',
            ],
            [
                'For="[2001:db8:cafe::17]:4711"',
                '',
            ],
            [
                'for=192.0.2.60;proto=http;by=203.0.113.43',
                'http',
            ],
            [
                'for=192.0.2.43, for=198.51.100.17',
                '',
            ],
            [
                'for=123.34.567.89',
                '',
            ],
            [
                'for=192.0.2.43, for="[2001:db8:cafe::17]"',
                '',
            ],
            [
                'for=12.34.56.78;host=example.com;proto=https, for=23.45.67.89',
                'https',
            ],
            [
                'for=12.34.56.78, for=23.45.67.89;secret=egah2CGj55fSJFs, for=10.1.2.3',
                '',
            ],
            [
                'for=injected;by="',
                '',
            ],
            [
                'for=injected;by=", for=real',
                '',
            ],
            [
                'for=192.0.2.60;proto=http;by=203.0.113.43',
                'http',
            ],
            [
                'for=192.0.2.60;proto=htTp;by=203.0.113.43',
                'http',
            ],
            [
                'for=192.0.2.60;proto=HTTP;by=203.0.113.43',
                'http',
            ],
            [
                'for=192.0.2.60;proto= http;by=203.0.113.43',
                'http',
            ],
            [
                'for=12.34.45.67;secret="special;proto=abc;test=1";proto=http,for=23.45.67.89',
                'http',
            ],
            [
                'for=12.34.45.67;secret="special;proto=abc;test=1";proto=418,for=23.45.67.89',
                '',
            ],
            /*[ // this test case is very special and would need a different implementation
                'for=12.34.45.67;secret="special;proto=http;test=1";proto=https,for=23.45.67.89',
                'https'
            ]*/
        ];
    }

    public function testCurrentUserHasPrivilegeSkipGrantTables(): void
    {
        $dbi = $this->getMockBuilder(DatabaseInterface::class)
            ->disableOriginalConstructor()
            ->getMock();
        $dbi->expects($this->once())
            ->method('getCurrentUserAndHost')
            ->will($this->returnValue(['', '']));

        $oldDbi = $GLOBALS['dbi'];
        $GLOBALS['dbi'] = $dbi;
        $this->assertTrue(Util::currentUserHasPrivilege('EVENT'));
        $GLOBALS['dbi'] = $oldDbi;
    }

    public function testCurrentUserHasUserPrivilege(): void
    {
        $dbi = $this->getMockBuilder(DatabaseInterface::class)
            ->disableOriginalConstructor()
            ->getMock();
        $dbi->expects($this->once())
            ->method('getCurrentUserAndHost')
            ->will($this->returnValue(['groot_%', '%']));
        $dbi->expects($this->once())
            ->method('fetchValue')
            ->with(
                'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES`'
                . " WHERE GRANTEE='''groot_%''@''%''' AND PRIVILEGE_TYPE='EVENT'"
            )
            ->will($this->returnValue('EVENT'));

        $oldDbi = $GLOBALS['dbi'];
        $GLOBALS['dbi'] = $dbi;
        $this->assertTrue(Util::currentUserHasPrivilege('EVENT'));
        $GLOBALS['dbi'] = $oldDbi;
    }

    public function testCurrentUserHasNotUserPrivilege(): void
    {
        $dbi = $this->getMockBuilder(DatabaseInterface::class)
            ->disableOriginalConstructor()
            ->getMock();
        $dbi->expects($this->once())
            ->method('getCurrentUserAndHost')
            ->will($this->returnValue(['groot_%', '%']));
        $dbi->expects($this->once())
            ->method('fetchValue')
            ->with(
                'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES`'
                . " WHERE GRANTEE='''groot_%''@''%''' AND PRIVILEGE_TYPE='EVENT'"
            )
            ->will($this->returnValue(false));

        $oldDbi = $GLOBALS['dbi'];
        $GLOBALS['dbi'] = $dbi;
        $this->assertFalse(Util::currentUserHasPrivilege('EVENT'));
        $GLOBALS['dbi'] = $oldDbi;
    }

    public function testCurrentUserHasNotUserPrivilegeButDbPrivilege(): void
    {
        $dbi = $this->getMockBuilder(DatabaseInterface::class)
            ->onlyMethods(['getCurrentUserAndHost', 'fetchValue'])
            ->disableOriginalConstructor()
            ->getMock();

        $dbi->expects($this->once())
            ->method('getCurrentUserAndHost')
            ->will($this->returnValue(['groot_%', '%']));
        $dbi->expects($this->exactly(2))
            ->method('fetchValue')
            ->withConsecutive(
                [
                    'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES`'
                . " WHERE GRANTEE='''groot_%''@''%''' AND PRIVILEGE_TYPE='EVENT'",
                ],
                [
                    'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`SCHEMA_PRIVILEGES`'
                . " WHERE GRANTEE='''groot_%''@''%''' AND PRIVILEGE_TYPE='EVENT'"
                . " AND 'my_data_base' LIKE `TABLE_SCHEMA`",
                ]
            )
            ->willReturnOnConsecutiveCalls(false, 'EVENT');

        $oldDbi = $GLOBALS['dbi'];
        $GLOBALS['dbi'] = $dbi;
        $this->assertTrue(Util::currentUserHasPrivilege('EVENT', 'my_data_base'));
        $GLOBALS['dbi'] = $oldDbi;
    }

    public function testCurrentUserHasNotUserPrivilegeAndNotDbPrivilege(): void
    {
        $dbi = $this->getMockBuilder(DatabaseInterface::class)
            ->onlyMethods(['getCurrentUserAndHost', 'fetchValue'])
            ->disableOriginalConstructor()
            ->getMock();

        $dbi->expects($this->once())
            ->method('getCurrentUserAndHost')
            ->will($this->returnValue(['groot_%', '%']));
        $dbi->expects($this->exactly(2))
            ->method('fetchValue')
            ->withConsecutive(
                [
                    'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES`'
                . " WHERE GRANTEE='''groot_%''@''%''' AND PRIVILEGE_TYPE='EVENT'",
                ],
                [
                    'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`SCHEMA_PRIVILEGES`'
                . " WHERE GRANTEE='''groot_%''@''%''' AND PRIVILEGE_TYPE='EVENT'"
                . " AND 'my_data_base' LIKE `TABLE_SCHEMA`",
                ]
            )
            ->willReturnOnConsecutiveCalls(false, false);

        $oldDbi = $GLOBALS['dbi'];
        $GLOBALS['dbi'] = $dbi;
        $this->assertFalse(Util::currentUserHasPrivilege('EVENT', 'my_data_base'));
        $GLOBALS['dbi'] = $oldDbi;
    }

    public function testCurrentUserHasNotUserPrivilegeAndNotDbPrivilegeButTablePrivilege(): void
    {
        $dbi = $this->getMockBuilder(DatabaseInterface::class)
            ->onlyMethods(['getCurrentUserAndHost', 'fetchValue'])
            ->disableOriginalConstructor()
            ->getMock();

        $dbi->expects($this->once())
            ->method('getCurrentUserAndHost')
            ->will($this->returnValue(['groot_%', '%']));
        $dbi->expects($this->exactly(3))
            ->method('fetchValue')
            ->withConsecutive(
                [
                    'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES`'
                . " WHERE GRANTEE='''groot_%''@''%''' AND PRIVILEGE_TYPE='EVENT'",
                ],
                [
                    'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`SCHEMA_PRIVILEGES`'
                . " WHERE GRANTEE='''groot_%''@''%''' AND PRIVILEGE_TYPE='EVENT'"
                . " AND 'my_data_base' LIKE `TABLE_SCHEMA`",
                ],
                [
                    'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`TABLE_PRIVILEGES`'
                . " WHERE GRANTEE='''groot_%''@''%''' AND PRIVILEGE_TYPE='EVENT'"
                . " AND 'my_data_base' LIKE `TABLE_SCHEMA` AND TABLE_NAME='my_data_table'",
                ]
            )
            ->willReturnOnConsecutiveCalls(false, false, 'EVENT');

        $oldDbi = $GLOBALS['dbi'];
        $GLOBALS['dbi'] = $dbi;
        $this->assertTrue(Util::currentUserHasPrivilege('EVENT', 'my_data_base', 'my_data_table'));
        $GLOBALS['dbi'] = $oldDbi;
    }

    public function testCurrentUserHasNotUserPrivilegeAndNotDbPrivilegeAndNotTablePrivilege(): void
    {
        $dbi = $this->getMockBuilder(DatabaseInterface::class)
            ->onlyMethods(['getCurrentUserAndHost', 'fetchValue'])
            ->disableOriginalConstructor()
            ->getMock();

        $dbi->expects($this->once())
            ->method('getCurrentUserAndHost')
            ->will($this->returnValue(['groot_%', '%']));
        $dbi->expects($this->exactly(3))
            ->method('fetchValue')
            ->withConsecutive(
                [
                    'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES`'
                . " WHERE GRANTEE='''groot_%''@''%''' AND PRIVILEGE_TYPE='EVENT'",
                ],
                [
                    'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`SCHEMA_PRIVILEGES`'
                . " WHERE GRANTEE='''groot_%''@''%''' AND PRIVILEGE_TYPE='EVENT'"
                . " AND 'my_data_base' LIKE `TABLE_SCHEMA`",
                ],
                [
                    'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`TABLE_PRIVILEGES`'
                . " WHERE GRANTEE='''groot_%''@''%''' AND PRIVILEGE_TYPE='EVENT'"
                . " AND 'my_data_base' LIKE `TABLE_SCHEMA` AND TABLE_NAME='my_data_table'",
                ]
            )
            ->willReturnOnConsecutiveCalls(false, false, false);

        $oldDbi = $GLOBALS['dbi'];
        $GLOBALS['dbi'] = $dbi;
        $this->assertFalse(Util::currentUserHasPrivilege('EVENT', 'my_data_base', 'my_data_table'));
        $GLOBALS['dbi'] = $oldDbi;
    }

    /**
     * @return array[]
     */
    public function dataProviderScriptNames(): array
    {
        // target
        // location
        // function output
        return [
            [
                'structure', // Notice the typo on db_structure.php
                'databasesss',
                'index.php?route=/&lang=en', // Fallback to the base route
            ],
            [
                'db_structures.php', // Notice the typo on databases
                'database',
                'index.php?route=/&lang=en', // Fallback to the base route
            ],
            [
                'tbl_structure.php', // Support the legacy value
                'table',
                'index.php?route=/table/structure&lang=en',
            ],
            [
                'structure',
                'table',
                'index.php?route=/table/structure&lang=en',
            ],
            [
                'tbl_sql.php', // Support the legacy value
                'table',
                'index.php?route=/table/sql&lang=en',
            ],
            [
                'sql',
                'table',
                'index.php?route=/table/sql&lang=en',
            ],
            [
                'tbl_select.php', // Support the legacy value
                'table',
                'index.php?route=/table/search&lang=en',
            ],
            [
                'search',
                'table',
                'index.php?route=/table/search&lang=en',
            ],
            [
                'tbl_change.php', // Support the legacy value
                'table',
                'index.php?route=/table/change&lang=en',
            ],
            [
                'insert',
                'table',
                'index.php?route=/table/change&lang=en',
            ],
            [
                'sql.php', // Support the legacy value
                'table',
                'index.php?route=/sql&lang=en',
            ],
            [
                'browse',
                'table',
                'index.php?route=/sql&lang=en',
            ],
            [
                'db_structure.php', // Support the legacy value
                'database',
                'index.php?route=/database/structure&lang=en',
            ],
            [
                'structure',
                'database',
                'index.php?route=/database/structure&lang=en',
            ],
            [
                'db_sql.php', // Support the legacy value
                'database',
                'index.php?route=/database/sql&lang=en',
            ],
            [
                'sql',
                'database',
                'index.php?route=/database/sql&lang=en',
            ],
            [
                'db_search.php', // Support the legacy value
                'database',
                'index.php?route=/database/search&lang=en',
            ],
            [
                'search',
                'database',
                'index.php?route=/database/search&lang=en',
            ],
            [
                'db_operations.php', // Support the legacy value
                'database',
                'index.php?route=/database/operations&lang=en',
            ],
            [
                'operations',
                'database',
                'index.php?route=/database/operations&lang=en',
            ],
            [
                'index.php', // Support the legacy value
                'server',
                'index.php?route=/&lang=en',
            ],
            [
                'welcome',
                'server',
                'index.php?route=/&lang=en',
            ],
            [
                'server_databases.php', // Support the legacy value
                'server',
                'index.php?route=/server/databases&lang=en',
            ],
            [
                'databases',
                'server',
                'index.php?route=/server/databases&lang=en',
            ],
            [
                'server_status.php', // Support the legacy value
                'server',
                'index.php?route=/server/status&lang=en',
            ],
            [
                'status',
                'server',
                'index.php?route=/server/status&lang=en',
            ],
            [
                'server_variables.php', // Support the legacy value
                'server',
                'index.php?route=/server/variables&lang=en',
            ],
            [
                'variables',
                'server',
                'index.php?route=/server/variables&lang=en',
            ],
            [
                'server_privileges.php', // Support the legacy value
                'server',
                'index.php?route=/server/privileges&lang=en',
            ],
            [
                'privileges',
                'server',
                'index.php?route=/server/privileges&lang=en',
            ],
        ];
    }

    /**
     * @dataProvider dataProviderScriptNames
     */
    public function testGetScriptNameForOption(string $target, string $location, string $finalLink): void
    {
        $this->assertSame(
            $finalLink,
            Util::getScriptNameForOption($target, $location)
        );
    }

    /**
     * Tests for Util::testIsUUIDSupported() method.
     *
     * @param bool $isMariaDB True if mariadb
     * @param int  $version   Database version as integer
     * @param bool $expected  Expected Result
     *
     * @dataProvider provideForTestIsUUIDSupported
     */
    public function testIsUUIDSupported(bool $isMariaDB, int $version, bool $expected): void
    {
        $dbi = $this->getMockBuilder(DatabaseInterface::class)
            ->disableOriginalConstructor()
            ->getMock();

        $dbi->expects($this->any())
            ->method('isMariaDB')
            ->will($this->returnValue($isMariaDB));

        $dbi->expects($this->any())
            ->method('getVersion')
            ->will($this->returnValue($version));

        $oldDbi = $GLOBALS['dbi'];
        $GLOBALS['dbi'] = $dbi;
        $this->assertEquals(Util::isUUIDSupported(), $expected);
        $GLOBALS['dbi'] = $oldDbi;
    }

    /**
     * Data provider for isUUIDSupported() tests.
     *
     * @return array
     * @psalm-return array<int, array{bool, int, bool}>
     */
    public function provideForTestIsUUIDSupported(): array
    {
        return [
            [
                false,
                60100,
                false,
            ],
            [
                false,
                100700,
                false,
            ],
            [
                true,
                60100,
                false,
            ],
            [
                true,
                100700,
                true,
            ],
        ];
    }
}
