<?php

/**
 * @copyright  Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE
 */

namespace Joomla\String\Tests;

use Joomla\String\StringHelper;
use PHPUnit\Framework\TestCase;

/**
 * Test class for StringHelper.
 */
class StringHelperTest extends TestCase
{
    /**
     * Data provider for testIncrement
     *
     * @return  \Generator
     */
    public function seedTestIncrement(): \Generator
    {
        // Note: string, style, number, expected
        yield 'First default increment' => ['title', null, 0, 'title (2)'];
        yield 'Second default increment' => ['title(2)', null, 0, 'title(3)'];
        yield 'First dash increment' => ['title', 'dash', 0, 'title-2'];
        yield 'Second dash increment' => ['title-2', 'dash', 0, 'title-3'];
        yield 'Set default increment' => ['title', null, 4, 'title (4)'];
        yield 'Unknown style fallback to default' => ['title', 'foo', 0, 'title (2)'];
    }

    /**
     * Data provider for testIs_ascii
     *
     * @return  \Generator
     */
    public function seedTestIs_ascii(): \Generator
    {
        yield ['ascii', true];
        yield ['1024', true];
        yield ['#$#@$%', true];
        yield ['áÑ', false];
        yield ['ÿ©', false];
        yield ['¡¾', false];
        yield ['÷™', false];
    }

    /**
     * Data provider for testStrpos
     *
     * @return  \Generator
     */
    public function seedTestStrpos(): \Generator
    {
        yield [3, 'missing', 'sing', 0];
        yield [false, 'missing', 'sting', 0];
        yield [4, 'missing', 'ing', 0];
        yield [10, ' объектов на карте с', 'на карте', 0];
        yield [0, 'на карте с', 'на карте', 0, 0];
        yield [false, 'на карте с', 'на каррте', 0];
        yield [false, 'на карте с', 'на карте', 2];
        yield [3, 'missing', 'sing', false];
    }

    /**
     * Data provider for testStrrpos
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestStrrpos(): \Generator
    {
        yield [3, 'missing', 'sing', 0];
        yield [false, 'missing', 'sting', 0];
        yield [4, 'missing', 'ing', 0];
        yield [10, ' объектов на карте с', 'на карте', 0];
        yield [0, 'на карте с', 'на карте', 0];
        yield [false, 'на карте с', 'на каррте', 0];
        yield [3, 'на карте с', 'карт', 2];
    }

    /**
     * Data provider for testSubstr
     *
     * @return  \Generator
     */
    public function seedTestSubstr(): \Generator
    {
        yield ['issauga', 'Mississauga', 4, false];
        yield ['на карте с', ' объектов на карте с', 10, false];
        yield ['на ка', ' объектов на карте с', 10, 5];
        yield ['те с', ' объектов на карте с', -4, false];
        yield [false, ' объектов на карте с', 99, false];
    }

    /**
     * Data provider for testStrtolower
     *
     * @return  \Generator
     */
    public function seedTestStrtolower(): \Generator
    {
        yield ['Joomla! Rocks', 'joomla! rocks'];
    }

    /**
     * Data provider for testStrtoupper
     *
     * @return  \Generator
     */
    public function seedTestStrtoupper(): \Generator
    {
        yield ['Joomla! Rocks', 'JOOMLA! ROCKS'];
    }

    /**
     * Data provider for testStrlen
     *
     * @return  \Generator
     */
    public function seedTestStrlen(): \Generator
    {
        yield ['Joomla! Rocks', 13];
    }

    /**
     * Data provider for testStr_ireplace
     *
     * @return  \Generator
     */
    public function seedTestStr_ireplace(): \Generator
    {
        yield ['Pig', 'cow', 'the pig jumped', false, 'the cow jumped'];
        yield ['Pig', 'cow', 'the pig jumped', true, 'the cow jumped'];
        yield ['Pig', 'cow', 'the pig jumped over the cow', true, 'the cow jumped over the cow'];
        yield [['PIG', 'JUMPED'], ['cow', 'hopped'], 'the pig jumped over the pig', true, 'the cow hopped over the cow'];
        yield ['шил', 'биш', 'Би шил идэй чадна', true, 'Би биш идэй чадна'];
        yield ['/', ':', '/test/slashes/', true, ':test:slashes:'];
    }

    /**
     * Data provider for testStr_split
     *
     * @return  \Generator
     */
    public function seedTestStr_split(): \Generator
    {
        yield ['string', 1, ['s', 't', 'r', 'i', 'n', 'g']];
        yield ['string', 2, ['st', 'ri', 'ng']];
        yield ['волн', 3, ['вол', 'н']];
        yield ['волн', 1, ['в', 'о', 'л', 'н']];
    }

    /**
     * Data provider for testStrcasecmp
     *
     * @return  \Generator
     */
    public function seedTestStrcasecmp(): \Generator
    {
        yield ['THIS IS STRING1', 'this is string1', false, 0];
        yield ['this is string1', 'this is string2', false, -1];
        yield ['this is string2', 'this is string1', false, 1];
        yield ['бгдпт', 'бгдпт', false, 0];
        yield ['àbc', 'abc', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], 1];
        yield ['àbc', 'bcd', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], -1];
        yield ['é', 'è', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], -1];
        yield ['É', 'é', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], 0];
        yield ['œ', 'p', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], -1];
        yield ['œ', 'n', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], 1];
    }

    /**
     * Data provider for testStrcmp
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestStrcmp(): \Generator
    {
        yield ['THIS IS STRING1', 'this is string1', false, -1];
        yield ['this is string1', 'this is string2', false, -1];
        yield ['this is string2', 'this is string1', false, 1];
        yield ['a', 'B', false, 1];
        yield ['A', 'b', false, -1];
        yield ['Àbc', 'abc', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], 1];
        yield ['Àbc', 'bcd', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], -1];
        yield ['É', 'è', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], -1];
        yield ['é', 'È', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], -1];
        yield ['Œ', 'p', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], -1];
        yield ['Œ', 'n', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], 1];
        yield ['œ', 'N', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], 1];
        yield ['œ', 'P', ['fr_FR.utf8', 'fr_FR.UTF-8', 'fr_FR.UTF-8@euro', 'French_Standard', 'french', 'fr_FR', 'fre_FR'], -1];
    }

    /**
     * Data provider for testStrcspn
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestStrcspn(): \Generator
    {
        yield ['subject <a> string <a>', '<>', false, false, 8];
        yield ['Би шил {123} идэй {456} чадна', '}{', null, false, 7];
        yield ['Би шил {123} идэй {456} чадна', '}{', 13, 10, 5];
    }

    /**
     * Data provider for testStristr
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestStristr(): \Generator
    {
        yield ['haystack', 'needle', false];
        yield ['before match, after match', 'match', 'match, after match'];
        yield ['Би шил идэй чадна', 'шил', 'шил идэй чадна'];
    }

    /**
     * Data provider for testStrrev
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestStrrev(): \Generator
    {
        yield ['abc def', 'fed cba'];
        yield ['Би шил', 'лиш иБ'];
    }

    /**
     * Data provider for testStrspn
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestStrspn(): \Generator
    {
        yield ['A321 Main Street', '0123456789', 1, 2, 2];
        yield ['321 Main Street', '0123456789', null, 2, 2];
        yield ['A321 Main Street', '0123456789', null, 10, 0];
        yield ['321 Main Street', '0123456789', null, null, 3];
        yield ['Main Street 321', '0123456789', null, -3, 0];
        yield ['321 Main Street', '0123456789', null, -13, 2];
        yield ['321 Main Street', '0123456789', null, -12, 3];
        yield ['A321 Main Street', '0123456789', 0, null, 0];
        yield ['A321 Main Street', '0123456789', 1, 10, 3];
        yield ['A321 Main Street', '0123456789', 1, null, 3];
        yield ['Би шил идэй чадна', 'Би', null, null, 2];
        yield ['чадна Би шил идэй чадна', 'Би', null, null, 0];
    }

    /**
     * Data provider for testSubstr_replace
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestSubstr_replace(): \Generator
    {
        yield ['321 Broadway Avenue', '321 Main Street', 'Broadway Avenue', 4, false];
        yield ['321 Broadway Street', '321 Main Street', 'Broadway', 4, 4];
        yield ['чадна 我能吞', 'чадна Би шил идэй чадна', '我能吞', 6, false];
        yield ['чадна 我能吞 шил идэй чадна', 'чадна Би шил идэй чадна', '我能吞', 6, 2];
    }

    /**
     * Data provider for testLtrim
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestLtrim(): \Generator
    {
        yield ['   abc def', false, 'abc def'];
        yield ['   abc def', '', '   abc def'];
        yield [' Би шил', false, 'Би шил'];
        yield ["\t\n\r\x0BБи шил", false, 'Би шил'];
        yield ["\x0B\t\n\rБи шил", "\t\n\x0B", "\rБи шил"];
        yield ["\x09Би шил\x0A", "\x09\x0A", "Би шил\x0A"];
        yield ['1234abc', '0123456789', 'abc'];
    }

    /**
     * Data provider for testRtrim
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestRtrim(): \Generator
    {
        yield ['abc def   ', false, 'abc def'];
        yield ['abc def   ', '', 'abc def   '];
        yield ['Би шил ', false, 'Би шил'];
        yield ["Би шил\t\n\r\x0B", false, 'Би шил'];
        yield ["Би шил\r\x0B\t\n", "\t\n\x0B", "Би шил\r"];
        yield ["\x09Би шил\x0A", "\x09\x0A", "\x09Би шил"];
        yield ['1234abc', 'abc', '1234'];
    }

    /**
     * Data provider for testTrim
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestTrim(): \Generator
    {
        yield ['  abc def   ', false, 'abc def'];
        yield ['  abc def   ', '', '  abc def   '];
        yield ['   Би шил ', false, 'Би шил'];
        yield ["\t\n\r\x0BБи шил\t\n\r\x0B", false, 'Би шил'];
        yield ["\x0B\t\n\rБи шил\r\x0B\t\n", "\t\n\x0B", "\rБи шил\r"];
        yield ["\x09Би шил\x0A", "\x09\x0A", "Би шил"];
        yield ['1234abc56789', '0123456789', 'abc'];
    }

    /**
     * Data provider for testUcfirst
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestUcfirst(): \Generator
    {
        yield ['george', null, null, 'George'];
        yield ['мога', null, null, 'Мога'];
        yield ['ψυχοφθόρα', null, null, 'Ψυχοφθόρα'];
        yield ['dr jekill and mister hyde', ' ', null, 'Dr Jekill And Mister Hyde'];
        yield ['dr jekill and mister hyde', ' ', '_', 'Dr_Jekill_And_Mister_Hyde'];
        yield ['dr jekill and mister hyde', ' ', '', 'DrJekillAndMisterHyde'];
    }

    /**
     * Data provider for testUcwords
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestUcwords(): \Generator
    {
        yield ['george washington', 'George Washington'];
        yield ["george\r\nwashington", "George\r\nWashington"];
        yield ['мога', 'Мога'];
        yield ['αβγ δεζ', 'Αβγ Δεζ'];
        yield ['åbc öde', 'Åbc Öde'];
    }

    /**
     * Data provider for testTranscode
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedTestTranscode(): \Generator
    {
        yield ['Åbc Öde €100', 'UTF-8', 'ISO-8859-1', "\xc5bc \xd6de EUR100"];
    }

    /**
     * Data provider for testing compliant strings
     *
     * @return  \Generator
     *
     * @since   1.0
     */
    public function seedCompliantStrings(): \Generator
    {
        yield ["\xCF\xB0", true];
        yield ["\xFBa", false];
        yield ["\xFDa", false];
        yield ["foo\xF7bar", false];
        yield ['george Мога Ž Ψυχοφθόρα ฉันกินกระจกได้ 我能吞下玻璃而不伤身体 ', true];
        yield ["\xFF ABC", false];
        yield ["0xfffd ABC", true];
        yield ['', true];
    }

    /**
     * Data provider for testUnicodeToUtf8
     *
     * @return  \Generator
     *
     * @since   1.2.0
     */
    public function seedTestUnicodeToUtf8(): \Generator
    {
        yield ["\u0422\u0435\u0441\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u044b", "Тест системы"];
        yield ["\u00dcberpr\u00fcfung der Systemumstellung", "Überprüfung der Systemumstellung"];
    }

    /**
     * Data provider for testUnicodeToUtf16
     *
     * @return  \Generator
     *
     * @since   1.2.0
     */
    public function seedTestUnicodeToUtf16(): \Generator
    {
        yield ["\u0422\u0435\u0441\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u044b", "Тест системы"];
        yield ["\u00dcberpr\u00fcfung der Systemumstellung", "Überprüfung der Systemumstellung"];
    }

    /**
     * @testdox  A string is correctly incremented
     *
     * @param   string       $string    The source string.
     * @param   string|null  $style     The the style (default|dash).
     * @param   integer      $number    If supplied, this number is used for the copy, otherwise it is the 'next' number.
     * @param   string       $expected  Expected result.
     *
     * @dataProvider  seedTestIncrement
     */
    public function testIncrement(string $string, ?string $style, int $number, string $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::increment($string, $style, $number)
        );
    }

    /**
     * @testdox  A string is checked to determine if it is ASCII
     *
     * @param   string   $string    The string to test.
     * @param   boolean  $expected  Expected result.
     *
     * @dataProvider  seedTestIs_ascii
     */
    public function testIs_ascii(string $string, bool $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::is_ascii($string)
        );
    }

    /**
     * @testdox  UTF-8 aware strpos() is performed on a string
     *
     * @param   string|boolean        $expected  Expected result
     * @param   string                $haystack  String being examined
     * @param   string                $needle    String being searched for
     * @param   integer|null|boolean  $offset    Optional, specifies the position from which the search should be performed
     *
     * @dataProvider  seedTestStrpos
     */
    public function testStrpos($expected, string $haystack, string $needle, $offset = 0)
    {
        $this->assertEquals(
            $expected,
            StringHelper::strpos($haystack, $needle, $offset)
        );
    }

    /**
     * @testdox  UTF-8 aware strrpos() is performed on a string
     *
     * @param   string|boolean        $expected  Expected result
     * @param   string                $haystack  String being examined
     * @param   string                $needle    String being searched for
     * @param   integer|null|boolean  $offset    Optional, specifies the position from which the search should be performed
     *
     * @dataProvider  seedTestStrrpos
     */
    public function testStrrpos($expected, string $haystack, string $needle, int $offset = 0)
    {
        $this->assertEquals(
            $expected,
            StringHelper::strrpos($haystack, $needle, $offset)
        );
    }

    /**
     * @testdox  UTF-8 aware substr() is performed on a string
     *
     * @param   string|boolean        $expected  Expected result
     * @param   string                $string    String being processed
     * @param   integer               $offset    Number of UTF-8 characters offset (from left)
     * @param   integer|null|boolean  $offset    Optional, specifies the position from which the search should be performed
     *
     * @dataProvider  seedTestSubstr
     */
    public function testSubstr($expected, string $string, int $start, $length = false)
    {
        $this->assertEquals(
            $expected,
            StringHelper::substr($string, $start, $length)
        );
    }

    /**
     * @testdox  UTF-8 aware strtolower() is performed on a string
     *
     * @param   string          $string    String being processed
     * @param   string|boolean  $expected  Expected result
     *
     * @dataProvider  seedTestStrtolower
     */
    public function testStrtolower(string $string, $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::strtolower($string)
        );
    }

    /**
     * @testdox  UTF-8 aware strtoupper() is performed on a string
     *
     * @param   string          $string    String being processed
     * @param   string|boolean  $expected  Expected result
     *
     * @dataProvider  seedTestStrtoupper
     */
    public function testStrtoupper($string, $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::strtoupper($string)
        );
    }

    /**
     * @testdox  UTF-8 aware strlen() is performed on a string
     *
     * @param   string          $string    String being processed
     * @param   string|boolean  $expected  Expected result
     *
     * @dataProvider  seedTestStrlen
     */
    public function testStrlen(string $string, $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::strlen($string)
        );
    }

    /**
     * @testdox  UTF-8 aware str_ireplace() is performed on a string
     *
     * @param   string                $search    String to search
     * @param   string                $replace   Existing string to replace
     * @param   string                $subject   New string to replace with
     * @param   integer|null|boolean  $count     Optional count value to be passed by reference
     * @param   string                $expected  Expected result
     *
     * @return  array
     *
     * @dataProvider  seedTestStr_ireplace
     */
    public function testStr_ireplace($search, $replace, $subject, $count, $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::str_ireplace($search, $replace, $subject, $count)
        );
    }

    /**
     * @testdox  UTF-8 aware str_split() is performed on a string
     *
     * @param   string                $string    UTF-8 encoded string to process
     * @param   integer               $splitLen  Number to characters to split string by
     * @param   array|string|boolean  $expected  Expected result
     *
     * @dataProvider  seedTestStr_split
     */
    public function testStr_split($string, $splitLen, $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::str_split($string, $splitLen)
        );
    }

    /**
     * @testdox  UTF-8 aware strcasecmp() is performed on a string
     *
     * @param   string                $string1   String 1 to compare
     * @param   string                $string2   String 2 to compare
     * @param   array|string|boolean  $locale    The locale used by strcoll or false to use classical comparison
     * @param   integer               $expected  Expected result
     *
     * @dataProvider  seedTestStrcasecmp
     */
    public function testStrcasecmp(string $string1, string $string2, $locale, int $expected)
    {
        // Convert the $locale param to a string if it is an array
        if (\is_array($locale)) {
            $locale = "'" . implode("', '", $locale) . "'";
        }

        if (substr(php_uname(), 0, 6) == 'Darwin' && $locale != false) {
            $this->markTestSkipped('Darwin bug prevents foreign conversion from working properly');
        }

        if ($locale != false && !setlocale(LC_COLLATE, $locale)) {
            $this->markTestSkipped("Locale {$locale} is not available.");
        }

        $actual = StringHelper::strcasecmp($string1, $string2, $locale);

        if ($actual != 0) {
            $actual /= abs($actual);
        }

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

    /**
     * @testdox  UTF-8 aware strcmp() is performed on a string
     *
     * @param   string   $string1   String 1 to compare
     * @param   string   $string2   String 2 to compare
     * @param   mixed    $locale    The locale used by strcoll or false to use classical comparison
     * @param   integer  $expected  Expected result
     *
     * @dataProvider  seedTestStrcmp
     */
    public function testStrcmp(string $string1, string $string2, $locale, int $expected)
    {
        // Convert the $locale param to a string if it is an array
        if (\is_array($locale)) {
            $locale = "'" . implode("', '", $locale) . "'";
        }

        if (substr(php_uname(), 0, 6) == 'Darwin' && $locale != false) {
            $this->markTestSkipped('Darwin bug prevents foreign conversion from working properly');
        }

        if ($locale != false && !setlocale(LC_COLLATE, $locale)) {
            // If the locale is not available, we can't have to transcode the string and can't reliably compare it.
            $this->markTestSkipped("Locale {$locale} is not available.");
        }

        $actual = StringHelper::strcmp($string1, $string2, $locale);

        if ($actual != 0) {
            $actual = $actual / abs($actual);
        }

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

    /**
     * @testdox  UTF-8 aware strcspn() is performed on a string
     *
     * @param   string           $haystack  The string to process
     * @param   string           $needles   The mask
     * @param   integer|boolean  $start     Optional starting character position (in characters)
     * @param   integer|boolean  $len       Optional length
     * @param   integer          $expected  Expected result
     *
     * @dataProvider  seedTestStrcspn
     */
    public function testStrcspn(string $haystack, string $needles, $start, $len, int $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::strcspn($haystack, $needles, $start, $len)
        );
    }

    /**
     * @testdox  UTF-8 aware stristr() is performed on a string
     *
     * @param   string          $haystack  The haystack
     * @param   string          $needle    The needle
     * @param   string|boolean  $expect    Expected result
     *
     * @dataProvider  seedTestStristr
     */
    public function testStristr(string $haystack, string $needle, $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::stristr($haystack, $needle)
        );
    }

    /**
     * @testdox  UTF-8 aware strrev() is performed on a string
     *
     * @param   string  $string    String to be reversed
     * @param   string  $expected  Expected result
     *
     * @dataProvider  seedTestStrrev
     */
    public function testStrrev(string $string, string $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::strrev($string)
        );
    }

    /**
     * @testdox  UTF-8 aware strspn() is performed on a string
     *
     * @param   string        $subject  The haystack
     * @param   string        $mask     The mask
     * @param   integer|null  $start    Start optional
     * @param   integer|null  $length   Length optional
     * @param   integer       $expect   Expected result
     *
     * @dataProvider  seedTestStrspn
     */
    public function testStrspn(string $subject, string $mask, $start, $length, int $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::strspn($subject, $mask, $start, $length)
        );
    }

    /**
     * @testdox  UTF-8 aware substr_replace() is performed on a string
     *
     * @param   string                $expected     Expected result
     * @param   string                $string       The haystack
     * @param   string                $replacement  The replacement string
     * @param   integer               $start        Start
     * @param   integer|boolean|null  $length       Length (optional)
     *
     * @dataProvider  seedTestSubstr_replace
     */
    public function testSubstr_replace(string $expected, string $string, string $replacement, int $start, $length)
    {
        $this->assertEquals(
            $expected,
            StringHelper::substr_replace($string, $replacement, $start, $length)
        );
    }

    /**
     * @testdox  UTF-8 aware ltrim() is performed on a string
     *
     * @param   string          $string    The string to be trimmed
     * @param   string|boolean  $charlist  The optional charlist of additional characters to trim
     * @param   string          $expected  Expected result
     *
     * @dataProvider  seedTestLtrim
     */
    public function testLtrim(string $string, $charlist, string $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::ltrim($string, $charlist)
        );
    }

    /**
     * @testdox  UTF-8 aware rtrim() is performed on a string
     *
     * @param   string          $string    The string to be trimmed
     * @param   string|boolean  $charlist  The optional charlist of additional characters to trim
     * @param   string          $expected  Expected result
     *
     * @dataProvider  seedTestRtrim
     */
    public function testRtrim(string $string, $charlist, string $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::rtrim($string, $charlist)
        );
    }

    /**
     * @testdox  UTF-8 aware trim() is performed on a string
     *
     * @param   string          $string    The string to be trimmed
     * @param   string|boolean  $charlist  The optional charlist of additional characters to trim
     * @param   string          $expected  Expected result
     *
     * @dataProvider  seedTestTrim
     */
    public function testTrim(string $string, $charlist, string $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::trim($string, $charlist)
        );
    }

    /**
     * @testdox  UTF-8 aware ucfirst() is performed on a string
     *
     * @param   string       $string        String to be processed
     * @param   string|null  $delimiter     The words delimiter (null means do not split the string)
     * @param   string|null  $newDelimiter  The new words delimiter (null means equal to $delimiter)
     * @param   string       $expected      Expected result
     *
     * @dataProvider  seedTestUcfirst
     */
    public function testUcfirst(string $string, ?string $delimiter, ?string $newDelimiter, string $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::ucfirst($string, $delimiter, $newDelimiter)
        );
    }

    /**
     * @testdox  UTF-8 aware ucwords() is performed on a string
     *
     * @param   string  $string    String to be processed
     * @param   string  $expected  Expected result
     *
     * @dataProvider  seedTestUcwords
     */
    public function testUcwords(string $string, string $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::ucwords($string)
        );
    }

    /**
     * @testdox  A string is transcoded
     *
     * @param   string       $source        The string to transcode.
     * @param   string       $fromEncoding  The source encoding.
     * @param   string       $toEncoding    The target encoding.
     * @param   string|null  $expect        Expected result.
     *
     * @dataProvider  seedTestTranscode
     */
    public function testTranscode(string $source, string $fromEncoding, string $toEncoding, ?string $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::transcode($source, $fromEncoding, $toEncoding)
        );
    }

    /**
     * @testdox  A string is tested as valid UTF-8
     *
     * @param   string   $string    UTF-8 encoded string.
     * @param   boolean  $expected  Expected result.
     *
     * @dataProvider  seedCompliantStrings
     */
    public function testValid(string $string, bool $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::valid($string)
        );
    }

    /**
     * @testdox  A string is converted from unicode to UTF-8
     *
     * @param   string  $string    Unicode string to convert
     * @param   string  $expected  Expected result
     *
     * @dataProvider  seedTestUnicodeToUtf8
     */
    public function testUnicodeToUtf8(string $string, string $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::unicode_to_utf8($string)
        );
    }

    /**
     * @testdox  A string is converted from unicode to UTF-16
     *
     * @param   string  $string    Unicode string to convert
     * @param   string  $expected  Expected result
     *
     * @dataProvider  seedTestUnicodeToUtf16
     */
    public function testUnicodeToUtf16(string $string, string $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::unicode_to_utf16($string)
        );
    }

    /**
     * @testdox  A string is checked for UTF-8 compliance
     *
     * @param   string   $string    UTF-8 string to check
     * @param   boolean  $expected  Expected result
     *
     * @dataProvider  seedCompliantStrings
     */
    public function testCompliant(string $string, bool $expected)
    {
        $this->assertEquals(
            $expected,
            StringHelper::compliant($string)
        );
    }
}
