<?php
// StringTest.php
// Created: 2021-04-26
// Updated: 2023-07-05

declare(strict_types=1);

use PHPUnit\Framework\TestCase;
use Index\XArray;
use Index\AString;
use Index\WString;
use Index\XString;

/**
 * @covers AString
 * @covers WString
 * @covers XString
 */
final class StringTest extends TestCase {
    public function testLength(): void {
        $ascii = new AString('soup');
        $this->assertEquals(4, $ascii->getLength());
        $this->assertEquals(4, count($ascii));
        $this->assertFalse($ascii->isEmpty());

        $utf8 = new WString('ßóúþ', 'utf-8');
        $this->assertEquals(4, $utf8->getLength());
        $this->assertEquals(4, count($utf8));
        $this->assertEquals(8, $utf8->getByteCount());
        $this->assertFalse($utf8->isEmpty());

        $utf16be = new WString("\x00\xDF\x00\xF3\x00\xFA\x00\xFE", 'UTF-16BE');
        $this->assertEquals(4, $utf16be->getLength());
        $this->assertEquals(4, count($utf16be));
        $this->assertEquals(8, $utf16be->getByteCount());
        $this->assertFalse($utf16be->isEmpty());

        $utf32le = new WString("\xDF\x00\x00\x00\xF3\x00\x00\x00\xFA\x00\x00\x00\xFE\x00\x00\x00", 'UTF-32LE');
        $this->assertEquals(4, $utf32le->getLength());
        $this->assertEquals(4, count($utf32le));
        $this->assertEquals(16, $utf32le->getByteCount());
        $this->assertFalse($utf32le->isEmpty());

        $this->assertTrue($utf8->equals($utf16be));
        $this->assertTrue($utf16be->equals($utf32le));
        $this->assertTrue($utf32le->equals($utf8));
    }

    public function testToString(): void {
        $ascii = new AString('Windows XP');
        $this->assertEquals('Windows XP', (string)$ascii);

        $utf16be = new WString("\x00\xDF\x00\xF3\x00\xFA\x00\xFE", 'UTF-16BE');
        $this->assertEquals("\x00\xDF\x00\xF3\x00\xFA\x00\xFE", (string)$utf16be);

        $utf32le = $utf16be->convertEncoding('utf-32le');
        $this->assertEquals("\xDF\x00\x00\x00\xF3\x00\x00\x00\xFA\x00\x00\x00\xFE\x00\x00\x00", (string)$utf32le);
    }

    public function testArrayAccess(): void {
        $ascii = new AString('Akai Haato');

        $this->assertEquals('a', $ascii[2]);
        $this->assertEquals('H', $ascii[5]);

        $this->assertTrue(isset($ascii[6]));
        $this->assertFalse(isset($ascii[23]));

        $this->expectException(\BadMethodCallException::class);
        $ascii[4] = '_';

        $this->expectException(\BadMethodCallException::class);
        unset($ascii[7]);

        $utf16le = $ascii->toWString('utf-16le');

        $this->assertEquals('a', $utf16le[2]);
        $this->assertEquals('H', $utf16le[5]);

        $this->assertTrue(isset($utf16le[6]));
        $this->assertFalse(isset($utf16le[23]));

        $this->expectException(\BadMethodCallException::class);
        $utf16le[4] = '_';

        $this->expectException(\BadMethodCallException::class);
        unset($utf16le[7]);
    }

    public function testJsonSerialize(): void {
        $ascii = new AString('Mewow');
        $this->assertEquals('Mewow', json_decode(json_encode($ascii)));
        $this->assertEquals('Mewow', json_decode(json_encode($ascii->toWString('Shift_JIS'))));
    }

    public function testSerializable(): void {
        $ascii = new AString('Futami Mami');
        $asciiSerialised = serialize($ascii);

        $sjis = new WString("\x91\x6F\x8A\x43\x90\x5E\x94\xFC", 'Shift_JIS');
        $sjisSerialised = serialize($sjis);

        $asciiUnserialised = unserialize($asciiSerialised);
        $this->assertTrue($ascii->equals($asciiUnserialised));
        $this->assertTrue($asciiUnserialised->equals('Futami Mami'));

        $sjisUnserialised = unserialize($sjisSerialised);
        $this->assertTrue($sjis->equals($sjisUnserialised));
        $this->assertTrue($sjisUnserialised->equals(new WString('双海真美', 'utf-8')));
    }

    public function testEquals(): void {
        $ascii = new AString('Misaka Mikoto');
        $this->assertTrue($ascii->equals($ascii));
        $this->assertTrue($ascii->equals('Misaka Mikoto'));
        $this->assertTrue($ascii->equals(new AString('Misaka Mikoto')));
        $this->assertFalse($ascii->equals('Mewow'));
        $this->assertFalse($ascii->equals(new AString('Ndx Mewow')));
    }

    public function testConcat(): void {
        $ndxString1 = new AString('Akai ');
        $ndxString2 = $ndxString1->append('Haato');
        $ndxString3 = $ndxString1->append(new AString('Haato'));
        $ndxString4 = (new AString('Haato'))->prepend($ndxString1);

        $this->assertEquals('Akai Haato', (string)$ndxString2);
        $this->assertEquals('Akai Haato', (string)$ndxString3);
        $this->assertEquals('Akai Haato', (string)$ndxString4);
    }

    public function testSplit(): void {
        $ascii = new AString('Kasane Teto');
        $asciiArr = ['Kasane', 'Teto'];
        $this->assertTrue(XArray::sequenceEquals($asciiArr, $ascii->split(' ')));

        $utf16le = new WString("\x42\x30\x02\x30\x44\x30\x02\x30\x46\x30\x02\x30\x48\x30\x02\x30\x4A\x30\x02\x30", 'utf-16le');
        $utf16leArr = [
            new WString("\x42\x30", 'utf-16le'),
            new WString("\x44\x30", 'utf-16le'),
            new WString("\x46\x30", 'utf-16le'),
            new WString("\x48\x30", 'utf-16le'),
            new WString("\x4A\x30", 'utf-16le'),
            new WString("", 'utf-16le'),
        ];
        $this->assertTrue(XArray::sequenceEquals($utf16leArr, $utf16le->split(new WString("\x02\x30", 'utf-16le'))));

        $utf8 = new WString('あ。い。う。え。お。', 'utf-8');
        $utf8Arr = [
            new Wstring('あ', 'utf-8'),
            new Wstring('い', 'utf-8'),
            new Wstring('う', 'utf-8'),
            new Wstring('え', 'utf-8'),
            new Wstring('お', 'utf-8'),
            new Wstring('', 'utf-8'),
        ];
        $this->assertTrue(XArray::sequenceEquals($utf8Arr, $utf8->split(new WString('。', 'utf-8'))));
    }

    public function testChunk(): void {
        $ndxString = new AString('abcdefghijklmnopqrstuvwxyz');
        $ndxArray1 = ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz'];
        $ndxArray2 = ['ab', 'cd', 'ef', 'gh', 'ij', 'kl', 'mn', 'op', 'qr', 'st', 'uv', 'wx', 'yz'];
        $ndxArray3 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];

        $this->assertTrue(XArray::sequenceEquals($ndxString->chunk(3), $ndxArray1));
        $this->assertTrue(XArray::sequenceEquals($ndxString->chunk(2), $ndxArray2));
        $this->assertTrue(XArray::sequenceEquals($ndxString->chunk(1), $ndxArray3));

        $utf8 = new WString('君は実にばかだな', 'utf-8');
        $ndxArray4 = [
            new WString('君は実', 'utf-8'),
            new WString('にばか', 'utf-8'),
            new WString('だな', 'utf-8'),
        ];
        $ndxArray5 = [
            new WString('君は', 'utf-8'),
            new WString('実に', 'utf-8'),
            new WString('ばか', 'utf-8'),
            new WString('だな', 'utf-8'),
        ];
        $ndxArray6 = [
            new WString('君', 'utf-8'),
            new WString('は', 'utf-8'),
            new WString('実', 'utf-8'),
            new WString('に', 'utf-8'),
            new WString('ば', 'utf-8'),
            new WString('か', 'utf-8'),
            new WString('だ', 'utf-8'),
            new WString('な', 'utf-8'),
        ];

        $this->assertTrue(XArray::sequenceEquals($utf8->chunk(3), $ndxArray4));
        $this->assertTrue(XArray::sequenceEquals($utf8->chunk(2), $ndxArray5));
        $this->assertTrue(XArray::sequenceEquals($utf8->chunk(1), $ndxArray6));
    }

    public function testTrim(): void {
        $ascii = new AString('    meow     ');
        $this->assertTrue($ascii->trim()->equals('meow'));
        $this->assertTrue($ascii->trimStart()->equals('meow     '));
        $this->assertTrue($ascii->trimEnd()->equals('    meow'));

        $utf16be = $ascii->toWString('utf-16be');
        $this->assertTrue($utf16be->trim()->equals('meow'));
        $this->assertTrue($utf16be->trimStart()->equals('meow     '));
        $this->assertTrue($utf16be->trimEnd()->equals('    meow'));

        $utf8 = new WString('    にゃ     ', 'utf-8');
        $this->assertTrue($utf8->trim()->equals(new WString('にゃ', 'utf-8')));
        $this->assertTrue($utf8->trimStart()->equals(new WString('にゃ     ', 'utf-8')));
        $this->assertTrue($utf8->trimEnd()->equals(new WString('    にゃ', 'utf-8')));
    }

    public function testCaseChange(): void {
        $stringMixed = new AString('Sometimes');
        $stringUpper = new AString('SOMETIMES');
        $stringLower = new AString('sometimes');

        $this->assertTrue($stringMixed->toUpper()->equals($stringUpper));
        $this->assertTrue($stringMixed->toLower()->equals($stringLower));
    }

    public function testEscape(): void {
        $stringDirty = new AString('<img onerror="alert(\'xss\')">');
        $stringClean = new AString('&lt;img onerror=&quot;alert(\'xss\')&quot;&gt;');

        $this->assertTrue($stringDirty->escape()->equals($stringClean));
    }

    public function testJoin(): void {
        $ndxString1 = AString::join(['flash', 'wave']);
        $ndxString2 = AString::join(['Misaka', 'Mikoto'], new AString(' '));

        $this->assertEquals('flashwave', (string)$ndxString1);
        $this->assertEquals('Misaka Mikoto', (string)$ndxString2);
    }

    public function testEmpty(): void {
        $this->assertTrue(XString::nullOrEmpty(null));
        $this->assertTrue(XString::nullOrEmpty(''));
        $this->assertTrue(XString::nullOrEmpty(new AString('')));
        $this->assertFalse(XString::nullOrEmpty('soap'));
        $this->assertFalse(XString::nullOrEmpty(new AString('boat')));
        $this->assertTrue(XString::nullOrWhitespace(''));
        $this->assertTrue(XString::nullOrWhitespace('  '));
        $this->assertTrue(XString::nullOrWhitespace(new AString('   ')));
    }

    public function testReverse(): void {
        $ascii = new AString('Futami Mami');
        $sjis = new WString("\x91\x6F\x8A\x43\x90\x5E\x94\xFC", 'Shift_JIS');

        $asciiRev = $ascii->reverse();
        $sjisRev = $sjis->reverse();

        $this->assertEquals('imaM imatuF', (string)$asciiRev);
        $this->assertEquals("\x94\xFC\x90\x5E\x8A\x43\x91\x6F", (string)$sjisRev);
    }

    public function testReplace(): void {
        $ascii = new AString('aiueo');
        $sjis = new WString("\x82\xA0\x82\xA2\x82\xA4\x82\xA6\x82\xA8", 'sjis');

        $this->assertEquals('aikeo', $ascii->replace('u', 'k'));
        $this->assertEquals('aeoeo', $ascii->replace('iu', 'eo'));

        $this->assertTrue(
            $sjis->replace(
                new WString("\x46\x30", 'utf-16le'),
                new WString("\xE3\x81\x8B", 'utf-8')
            )->equals(new WString("\x82\xA0\x82\xA2\x82\xA9\x82\xA6\x82\xA8", 'sjis'))
        );
        $this->assertTrue(
            $sjis->replace(
                new WString("\xE3\x81\x84\xE3\x81\x86", 'utf-8'),
                new WString("\x48\x30\x4A\x30", 'utf-16le')
            )->equals(new WString("\x82\xA0\x82\xA6\x82\xA8\x82\xA6\x82\xA8", 'sjis'))
        );
    }

    public function testCountUnique(): void {
        $this->assertEquals(10, XString::countUnique('iaabbccddjjefghefghi'));
        $this->assertEquals(11, (new AString('jeff has three apples'))->countUnique());
        $this->assertEquals(5, (new WString('みさかみこと'))->countUnique());
    }
}