<?php

use cli\Colors;
use WP_CLI\Tests\TestCase;

class Test_Cli extends TestCase {

	function set_up() {
		// Reset enable state
		\cli\Colors::enable( null );

		// Empty the cache
		\cli\Colors::clearStringCache();
	}

	function test_string_length() {
		$this->assertEquals( \cli\Colors::length( 'x' ), 1 );
		$this->assertEquals( \cli\Colors::length( '日' ), 1 );
	}

	function test_string_width() {
		$this->assertEquals( \cli\Colors::width( 'x' ), 1 );
		$this->assertEquals( \cli\Colors::width( '日' ), 2 ); // Double-width char.
	}

	function test_encoded_string_length() {

		$this->assertEquals( \cli\Colors::length( 'hello' ), 5 );
		$this->assertEquals( \cli\Colors::length( 'óra' ), 3 );
		$this->assertEquals( \cli\Colors::length( '日本語' ), 3 );

	}

	function test_encoded_string_width() {

		$this->assertEquals( \cli\Colors::width( 'hello' ), 5 );
		$this->assertEquals( \cli\Colors::width( 'óra' ), 3 );
		$this->assertEquals( \cli\Colors::width( '日本語' ), 6 ); // 3 double-width chars.

	}

	function test_encoded_string_pad() {

		$this->assertEquals( 6, strlen( \cli\Colors::pad( 'hello', 6 ) ) );
		$this->assertEquals( 7, strlen( \cli\Colors::pad( 'óra', 6 ) ) ); // special characters take one byte
		$this->assertEquals( 9, strlen( \cli\Colors::pad( '日本語', 6 ) ) ); // each character takes two bytes
		$this->assertEquals( 17, strlen( \cli\Colors::pad( 'עִבְרִית', 6 ) ) ); // process Hebrew vowels
		$this->assertEquals( 6, strlen( \cli\Colors::pad( 'hello', 6, false, false, STR_PAD_RIGHT ) ) );
		$this->assertEquals( 7, strlen( \cli\Colors::pad( 'óra', 6, false, false, STR_PAD_LEFT ) ) ); // special characters take one byte
		$this->assertEquals( 9, strlen( \cli\Colors::pad( '日本語', 6, false, false, STR_PAD_BOTH ) ) ); // each character takes two bytes
		$this->assertSame( 4, strpos( \cli\Colors::pad( 'hello', 10, false, false, STR_PAD_RIGHT ), 'o' ) );
		$this->assertSame( 9, strpos( \cli\Colors::pad( 'hello', 10, false, false, STR_PAD_LEFT ), 'o' ) );
		$this->assertSame( 6, strpos( \cli\Colors::pad( 'hello', 10, false, false, STR_PAD_BOTH ), 'o' ) );
		$this->assertSame( 1, strpos( \cli\Colors::pad( 'hello', 10, false, false, STR_PAD_RIGHT ), 'e' ) );
		$this->assertSame( 6, strpos( \cli\Colors::pad( 'hello', 10, false, false, STR_PAD_LEFT ), 'e' ) );
		$this->assertSame( 3, strpos( \cli\Colors::pad( 'hello', 10, false, false, STR_PAD_BOTH ), 'e' ) );
	}

	function test_colorized_string_pad() {
		// Colors enabled.
		Colors::enable( true );

		$x = Colors::colorize( '%Gx%n', true ); // colorized `x` string
		$ora = Colors::colorize( "%Góra%n", true ); // colorized `óra` string

		$this->assertSame( 22, strlen( Colors::pad( $x, 11 ) ) );
		$this->assertSame( 22, strlen( Colors::pad( $x, 11, false /*pre_colorized*/ ) ) );
		$this->assertSame( 22, strlen( Colors::pad( $x, 11, true /*pre_colorized*/ ) ) );

		$this->assertSame( 23, strlen( Colors::pad( $ora, 11 ) ) ); // +1 for two-byte "ó".
		$this->assertSame( 23, strlen( Colors::pad( $ora, 11, false /*pre_colorized*/ ) ) );
		$this->assertSame( 23, strlen( Colors::pad( $ora, 11, true /*pre_colorized*/ ) ) );

		// Colors disabled.
		Colors::disable( true );
		$this->assertFalse( Colors::shouldColorize() );

		$this->assertSame( 20, strlen( Colors::pad( $x, 20 ) ) );
		$this->assertSame( 20, strlen( Colors::pad( $x, 20, false /*pre_colorized*/ ) ) );
		$this->assertSame( 31, strlen( Colors::pad( $x, 20, true /*pre_colorized*/ ) ) );

		$this->assertSame( 21, strlen( Colors::pad( $ora, 20 ) ) ); // +1 for two-byte "ó".
		$this->assertSame( 21, strlen( Colors::pad( $ora, 20, false /*pre_colorized*/ ) ) );
		$this->assertSame( 32, strlen( Colors::pad( $ora, 20, true /*pre_colorized*/ ) ) );
	}

	function test_encoded_substr() {

		$this->assertEquals( \cli\safe_substr( \cli\Colors::pad( 'hello', 6), 0, 2 ), 'he' );
		$this->assertEquals( \cli\safe_substr( \cli\Colors::pad( 'óra', 6), 0, 2 ), 'ór'  );
		$this->assertEquals( \cli\safe_substr( \cli\Colors::pad( '日本語', 6), 0, 2 ), '日本'  );

		$this->assertSame( 'el', \cli\safe_substr( Colors::pad( 'hello', 6 ), 1, 2 ) );

		$this->assertSame( 'a ', \cli\safe_substr( Colors::pad( 'óra', 6 ), 2, 2 ) );
		$this->assertSame( ' ', \cli\safe_substr( Colors::pad( 'óra', 6 ), 5, 2 ) );

		$this->assertSame( '本語', \cli\safe_substr( Colors::pad( '日本語', 8 ), 1, 2 ) );
		$this->assertSame( '語 ', \cli\safe_substr( Colors::pad( '日本語', 8 ), 2, 2 ) );
		$this->assertSame( ' ', \cli\safe_substr( Colors::pad( '日本語', 8 ), -1 ) );
		$this->assertSame( ' ', \cli\safe_substr( Colors::pad( '日本語', 8 ), -1, 2 ) );
		$this->assertSame( '語  ', \cli\safe_substr( Colors::pad( '日本語', 8 ), -3, 3 ) );
	}

	function test_various_substr() {
		// Save.
		$test_safe_substr = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' );
		if ( function_exists( 'mb_detect_order' ) ) {
			$mb_detect_order = mb_detect_order();
		}

		putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' );

		// Latin, kana, Latin, Latin combining, Thai combining, Hangul.
		$str = 'lムnöม้p를'; // 18 bytes.

		// Large string.
		$large_str_str_start = 65536 * 2;
		$large_str = str_repeat( 'a', $large_str_str_start ) . $str;
		$large_str_len = strlen( $large_str ); // 128K + 18 bytes.

		if ( \cli\can_use_icu() ) {
			putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=1' ); // Tests grapheme_substr().
			$this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) );
			$this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) );
			$this->assertSame( 'lム', \cli\safe_substr( $str, 0, 2 ) );
			$this->assertSame( 'lムn', \cli\safe_substr( $str, 0, 3 ) );
			$this->assertSame( 'lムnö', \cli\safe_substr( $str, 0, 4 ) );
			$this->assertSame( 'lムnöม้', \cli\safe_substr( $str, 0, 5 ) );
			$this->assertSame( 'lムnöม้p', \cli\safe_substr( $str, 0, 6 ) );
			$this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, 0, 7 ) );
			$this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, 0, 8 ) );
			$this->assertSame( '', \cli\safe_substr( $str, 19 ) ); // Start too large.
			$this->assertSame( '', \cli\safe_substr( $str, 19, 7 ) ); // Start too large, with length.
			$this->assertSame( '', \cli\safe_substr( $str, 8 ) ); // Start same as length.
			$this->assertSame( '', \cli\safe_substr( $str, 8, 0 ) ); // Start same as length, with zero length.
			$this->assertSame( '를', \cli\safe_substr( $str, -1 ) );
			$this->assertSame( 'p를', \cli\safe_substr( $str, -2 ) );
			$this->assertSame( 'ม้p를', \cli\safe_substr( $str, -3 ) );
			$this->assertSame( 'öม้p를', \cli\safe_substr( $str, -4 ) );
			$this->assertSame( 'öม้p', \cli\safe_substr( $str, -4, 3 ) );
			$this->assertSame( 'nö', \cli\safe_substr( $str, -5, 2 ) );
			$this->assertSame( 'ム', \cli\safe_substr( $str, -6, 1 ) );
			$this->assertSame( 'ムnöม้p를', \cli\safe_substr( $str, -6 ) );
			$this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -7 ) );
			$this->assertSame( 'lムnö', \cli\safe_substr( $str, -7, 4 ) );
			$this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -8 ) );
			$this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -9 ) ); // Negative start too large.

			$this->assertSame( $large_str, \cli\safe_substr( $large_str, 0 ) );
			$this->assertSame( '', \cli\safe_substr( $large_str, $large_str_str_start, 0 ) );
			$this->assertSame( 'l', \cli\safe_substr( $large_str, $large_str_str_start, 1 ) );
			$this->assertSame( 'lム', \cli\safe_substr( $large_str, $large_str_str_start, 2 ) );
			$this->assertSame( 'p를', \cli\safe_substr( $large_str, -2 ) );
		}

		if ( \cli\can_use_pcre_x() ) {
			putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=2' ); // Tests preg_split( '/\X/u' ).
			$this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) );
			$this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) );
			$this->assertSame( 'lム', \cli\safe_substr( $str, 0, 2 ) );
			$this->assertSame( 'lムn', \cli\safe_substr( $str, 0, 3 ) );
			$this->assertSame( 'lムnö', \cli\safe_substr( $str, 0, 4 ) );
			$this->assertSame( 'lムnöม้', \cli\safe_substr( $str, 0, 5 ) );
			$this->assertSame( 'lムnöม้p', \cli\safe_substr( $str, 0, 6 ) );
			$this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, 0, 7 ) );
			$this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, 0, 8 ) );
			$this->assertSame( '', \cli\safe_substr( $str, 19 ) ); // Start too large.
			$this->assertSame( '', \cli\safe_substr( $str, 19, 7 ) ); // Start too large, with length.
			$this->assertSame( '', \cli\safe_substr( $str, 8 ) ); // Start same as length.
			$this->assertSame( '', \cli\safe_substr( $str, 8, 0 ) ); // Start same as length, with zero length.
			$this->assertSame( '를', \cli\safe_substr( $str, -1 ) );
			$this->assertSame( 'p를', \cli\safe_substr( $str, -2 ) );
			$this->assertSame( 'ม้p를', \cli\safe_substr( $str, -3 ) );
			$this->assertSame( 'öม้p를', \cli\safe_substr( $str, -4 ) );
			$this->assertSame( 'öม้p', \cli\safe_substr( $str, -4, 3 ) );
			$this->assertSame( 'nö', \cli\safe_substr( $str, -5, 2 ) );
			$this->assertSame( 'ム', \cli\safe_substr( $str, -6, 1 ) );
			$this->assertSame( 'ムnöม้p를', \cli\safe_substr( $str, -6 ) );
			$this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -7 ) );
			$this->assertSame( 'lムnö', \cli\safe_substr( $str, -7, 4 ) );
			$this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -8 ) );
			$this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -9 ) ); // Negative start too large.

			$this->assertSame( $large_str, \cli\safe_substr( $large_str, 0 ) );
			$this->assertSame( '', \cli\safe_substr( $large_str, $large_str_str_start, 0 ) );
			$this->assertSame( 'l', \cli\safe_substr( $large_str, $large_str_str_start, 1 ) );
			$this->assertSame( 'lム', \cli\safe_substr( $large_str, $large_str_str_start, 2 ) );
			$this->assertSame( 'p를', \cli\safe_substr( $large_str, -2 ) );
		}

		if ( function_exists( 'mb_substr' ) ) {
			putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=4' ); // Tests mb_substr().
			$this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) );
			$this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) );
			$this->assertSame( 'lム', \cli\safe_substr( $str, 0, 2 ) );
			$this->assertSame( 'lムn', \cli\safe_substr( $str, 0, 3 ) );
			$this->assertSame( 'lムno', \cli\safe_substr( $str, 0, 4 ) ); // Wrong.
		}

		putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=8' ); // Tests substr().
		$this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) );
		$this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) );
		$this->assertSame( "l\xe3", \cli\safe_substr( $str, 0, 2 ) ); // Corrupt.
		$this->assertSame( '', \cli\safe_substr( $str, strlen( $str ) + 1 ) ); // Return '' not false to match behavior of other methods when `$start` > strlen.

		// Non-UTF-8 - both grapheme_substr() and preg_split( '/\X/u' ) will fail.

		putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' );

		if ( function_exists( 'mb_substr' ) && function_exists( 'mb_detect_order' ) ) {
			// Latin-1
			mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) );
			$str = "\xe0b\xe7"; // "àbç" in ISO-8859-1
			$this->assertSame( "\xe0b", \cli\safe_substr( $str, 0, 2 ) );
			$this->assertSame( "\xe0b", mb_substr( $str, 0, 2, 'ISO-8859-1' ) );
		}

		// Restore.
		putenv( false == $test_safe_substr ? 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' : "PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=$test_safe_substr" );
		if ( function_exists( 'mb_detect_order' ) ) {
			mb_detect_order( $mb_detect_order );
		}
	}

	function test_is_width_encoded_substr() {

		$this->assertSame( 'he',  \cli\safe_substr( Colors::pad( 'hello', 6 ), 0, 2, true /*is_width*/ ) );
		$this->assertSame( 'ór', \cli\safe_substr( Colors::pad( 'óra', 6 ), 0, 2, true /*is_width*/ ) );
		$this->assertSame( '日', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 2, true /*is_width*/ ) );
		$this->assertSame( '日', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 3, true /*is_width*/ ) );
		$this->assertSame( '日本', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 4, true /*is_width*/ ) );
		$this->assertSame( '日本語', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 6, true /*is_width*/ ) );
		$this->assertSame( '日本語 ', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 7, true /*is_width*/ ) );

		$this->assertSame( 'el', \cli\safe_substr( Colors::pad( 'hello', 6 ), 1, 2, true /*is_width*/ ) );

		$this->assertSame( 'a ', \cli\safe_substr( Colors::pad( 'óra', 6 ), 2, 2, true /*is_width*/ ) );
		$this->assertSame( ' ', \cli\safe_substr( Colors::pad( 'óra', 6 ), 5, 2, true /*is_width*/ ) );

		$this->assertSame( '', \cli\safe_substr( '1日4本語90', 0, 0, true /*is_width*/ ) );
		$this->assertSame( '1', \cli\safe_substr( '1日4本語90', 0, 1, true /*is_width*/ ) );
		$this->assertSame( '1', \cli\safe_substr( '1日4本語90', 0, 2, true /*is_width*/ ) );
		$this->assertSame( '1日', \cli\safe_substr( '1日4本語90', 0, 3, true /*is_width*/ ) );
		$this->assertSame( '1日4', \cli\safe_substr( '1日4本語90', 0, 4, true /*is_width*/ ) );
		$this->assertSame( '1日4', \cli\safe_substr( '1日4本語90', 0, 5, true /*is_width*/ ) );
		$this->assertSame( '1日4本', \cli\safe_substr( '1日4本語90', 0, 6, true /*is_width*/ ) );
		$this->assertSame( '1日4本', \cli\safe_substr( '1日4本語90', 0, 7, true /*is_width*/ ) );
		$this->assertSame( '1日4本語', \cli\safe_substr( '1日4本語90', 0, 8, true /*is_width*/ ) );
		$this->assertSame( '1日4本語9', \cli\safe_substr( '1日4本語90', 0, 9, true /*is_width*/ ) );
		$this->assertSame( '1日4本語90', \cli\safe_substr( '1日4本語90', 0, 10, true /*is_width*/ ) );
		$this->assertSame( '1日4本語90', \cli\safe_substr( '1日4本語90', 0, 11, true /*is_width*/ ) );

		$this->assertSame( '日', \cli\safe_substr( '1日4本語90', 1, 2, true /*is_width*/ ) );
		$this->assertSame( '日4', \cli\safe_substr( '1日4本語90', 1, 3, true /*is_width*/ ) );
		$this->assertSame( '4本語9', \cli\safe_substr( '1日4本語90', 2, 6, true /*is_width*/ ) );

		$this->assertSame( '本', \cli\safe_substr( '1日4本語90', 3, 1, true /*is_width*/ ) );
		$this->assertSame( '本', \cli\safe_substr( '1日4本語90', 3, 2, true /*is_width*/ ) );
		$this->assertSame( '本', \cli\safe_substr( '1日4本語90', 3, 3, true /*is_width*/ ) );
		$this->assertSame( '本語', \cli\safe_substr( '1日4本語90', 3, 4, true /*is_width*/ ) );
		$this->assertSame( '本語9', \cli\safe_substr( '1日4本語90', 3, 5, true /*is_width*/ ) );

		$this->assertSame( '0', \cli\safe_substr( '1日4本語90', 6, 1, true /*is_width*/ ) );
		$this->assertSame( '', \cli\safe_substr( '1日4本語90', 7, 1, true /*is_width*/ ) );
		$this->assertSame( '', \cli\safe_substr( '1日4本語90', 6, 0, true /*is_width*/ ) );

		$this->assertSame( '0', \cli\safe_substr( '1日4本語90', -1, 3, true /*is_width*/ ) );
		$this->assertSame( '90', \cli\safe_substr( '1日4本語90', -2, 3, true /*is_width*/ ) );
		$this->assertSame( '語9', \cli\safe_substr( '1日4本語90', -3, 3, true /*is_width*/ ) );
		$this->assertSame( '本語9', \cli\safe_substr( '1日4本語90', -4, 5, true /*is_width*/ ) );
	}

	function test_colorized_string_length() {
		$this->assertEquals( \cli\Colors::length( \cli\Colors::colorize( '%Gx%n', true ) ), 1 );
		$this->assertEquals( \cli\Colors::length( \cli\Colors::colorize( '%G日%n', true ) ), 1 );
	}

	function test_colorized_string_width() {
		// Colors enabled.
		Colors::enable( true );

		$x = Colors::colorize( '%Gx%n', true );
		$dw = Colors::colorize( '%G日%n', true ); // Double-width char.

		$this->assertSame( 1, Colors::width( $x ) );
		$this->assertSame( 1, Colors::width( $x, false /*pre_colorized*/ ) );
		$this->assertSame( 1, Colors::width( $x, true /*pre_colorized*/ ) );

		$this->assertSame( 2, Colors::width( $dw ) );
		$this->assertSame( 2, Colors::width( $dw, false /*pre_colorized*/ ) );
		$this->assertSame( 2, Colors::width( $dw, true /*pre_colorized*/ ) );

		// Colors disabled.
		Colors::disable( true );
		$this->assertFalse( Colors::shouldColorize() );

		$this->assertSame( 12, Colors::width( $x ) );
		$this->assertSame( 12, Colors::width( $x, false /*pre_colorized*/ ) );
		$this->assertSame( 1, Colors::width( $x, true /*pre_colorized*/ ) );

		$this->assertSame( 13, Colors::width( $dw ) );
		$this->assertSame( 13, Colors::width( $dw, false /*pre_colorized*/ ) );
		$this->assertSame( 2, Colors::width( $dw, true /*pre_colorized*/ ) );
	}

	function test_colorize_string_is_colored() {
		$original = '%Gx';
		$colorized = "\033[32;1mx";

		$this->assertEquals( \cli\Colors::colorize( $original, true ), $colorized );
	}

	function test_colorize_when_colorize_is_forced() {
		$original = '%gx%n';

		$this->assertEquals( \cli\Colors::colorize( $original, false ), 'x' );
	}

	function test_binary_string_is_converted_back_to_original_string() {
		$string            = 'x';
		$string_with_color = '%b' . $string;
		$colorized_string  = "\033[34m$string";

		// Ensure colorization is applied correctly
		$this->assertEquals( \cli\Colors::colorize( $string_with_color, true ), $colorized_string );

		// Ensure that the colorization is reverted
		$this->assertEquals( \cli\Colors::decolorize( $colorized_string ), $string );
	}

	function test_string_cache() {
		$string            = 'x';
		$string_with_color = '%k' . $string;
		$colorized_string  = "\033[30m$string";

		// Ensure colorization works
		$this->assertEquals( \cli\Colors::colorize( $string_with_color, true ), $colorized_string );

		// Test that the value was cached appropriately
		$test_cache = array(
			'passed'      => $string_with_color,
			'colorized'   => $colorized_string,
			'decolorized' => $string,
		);

		$real_cache = \cli\Colors::getStringCache();

		// Test that the cache value exists
		$this->assertTrue( isset( $real_cache[ md5( $string_with_color ) ] ) );

		// Test that the cache value is correctly set
		$this->assertEquals( $test_cache, $real_cache[ md5( $string_with_color ) ] );
	}

	function test_string_cache_colorize() {
		$string            = 'x';
		$string_with_color = '%k' . $string;
		$colorized_string  = "\033[30m$string";

		// Colors enabled.
		Colors::enable( true );

		// Ensure colorization works
		$this->assertSame( $colorized_string, Colors::colorize( $string_with_color ) );
		$this->assertSame( $colorized_string, Colors::colorize( $string_with_color ) );

		// Colors disabled.
		Colors::disable( true );
		$this->assertFalse( Colors::shouldColorize() );

		// Ensure it doesn't come from the cache.
		$this->assertSame( $string, Colors::colorize( $string_with_color ) );
		$this->assertSame( $string, Colors::colorize( $string_with_color ) );

		// Check that escaped % isn't stripped on putting into cache.
		$string = 'x%%n';
		$string_with_color = '%k' . $string;
		$this->assertSame( 'x%n', Colors::colorize( $string_with_color ) );
		$this->assertSame( 'x%n', Colors::colorize( $string_with_color ) );
	}

	function test_decolorize() {
		// Colors enabled.
		Colors::enable( true );

		$string = '%kx%%n%n';
		$colorized_string = Colors::colorize( $string );
		$both_string = '%gfoo' . $colorized_string . 'bar%%%n';

		$this->assertSame( 'x%n', Colors::decolorize( $string ) );
		$this->assertSame( 'x', Colors::decolorize( $colorized_string ) );
		$this->assertSame( 'fooxbar%', Colors::decolorize( $both_string ) );

		$this->assertSame( $string, Colors::decolorize( $string, 1 /*keep_tokens*/ ) );
		$this->assertSame( 'x%n', Colors::decolorize( $colorized_string, 1 /*keep_tokens*/ ) );
		$this->assertSame( '%gfoox%nbar%%%n', Colors::decolorize( $both_string, 1 /*keep_tokens*/ ) );

		$this->assertSame( 'x%n', Colors::decolorize( $string, 2 /*keep_encodings*/ ) );
		$this->assertSame( '[30mx[0m', Colors::decolorize( $colorized_string, 2 /*keep_encodings*/ ) );
		$this->assertSame( 'foo[30mx[0mbar%', Colors::decolorize( $both_string, 2 /*keep_encodings*/ ) );

		$this->assertSame( $string, Colors::decolorize( $string, 3 /*noop*/ ) );
		$this->assertSame( $colorized_string, Colors::decolorize( $colorized_string, 3 /*noop*/ ) );
		$this->assertSame( $both_string, Colors::decolorize( $both_string, 3 /*noop*/ ) );
	}

	function test_strwidth() {
		$this->markTestSkipped('Unknown failure');
		// Save.
		$test_strwidth = getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' );
		if ( function_exists( 'mb_detect_order' ) ) {
			$mb_detect_order = mb_detect_order();
		}

		putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' );

		// UTF-8.

		// 4 characters, one a double-width Han = 5 spacing chars, with 2 combining chars. Adapted from http://unicode.org/faq/char_combmark.html#7 (combining acute accent added after "a").
		$str = "a\xCC\x81\xE0\xA4\xA8\xE0\xA4\xBF\xE4\xBA\x9C\xF0\x90\x82\x83";

		if ( \cli\can_use_icu() ) {
			$this->assertSame( 5, \cli\strwidth( $str ) ); // Tests grapheme_strlen().
			putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=2' ); // Test preg_split( '/\X/u' ).
			$this->assertSame( 5, \cli\strwidth( $str ) );
		} else {
			$this->assertSame( 5, \cli\strwidth( $str ) ); // Tests preg_split( '/\X/u' ).
		}

		if ( function_exists( 'mb_strwidth' ) && function_exists( 'mb_detect_order' ) ) {
			putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=4' ); // Test mb_strwidth().
			mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) );
			$this->assertSame( 5, \cli\strwidth( $str ) );
		}

		putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=8' ); // Test safe_strlen().
		if ( \cli\can_use_icu() || \cli\can_use_pcre_x() ) {
			$this->assertSame( 4, \cli\strwidth( $str ) ); // safe_strlen() (correctly) does not account for double-width Han so out by 1.
		} elseif ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) {
			$this->assertSame( 4, \cli\strwidth( $str ) ); // safe_strlen() (correctly) does not account for double-width Han so out by 1.
			$this->assertSame( 6, mb_strlen( $str, 'UTF-8' ) );
		} else {
			$this->assertSame( 16, \cli\strwidth( $str ) ); // strlen() - no. of bytes.
			$this->assertSame( 16, strlen( $str ) );
		}

		// Nepali जस्ट ट॓स्ट गर्दै - 1st word: 3 spacing + 1 combining, 2nd word: 3 spacing + 2 combining, 3rd word: 3 spacing + 2 combining = 9 spacing chars + 2 spaces = 11 chars.
		$str = "\xe0\xa4\x9c\xe0\xa4\xb8\xe0\xa5\x8d\xe0\xa4\x9f \xe0\xa4\x9f\xe0\xa5\x93\xe0\xa4\xb8\xe0\xa5\x8d\xe0\xa4\x9f \xe0\xa4\x97\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa6\xe0\xa5\x88";

		putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' );

		if ( \cli\can_use_icu() ) {
			$this->assertSame( 11, \cli\strwidth( $str ) ); // Tests grapheme_strlen().
			putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=2' ); // Test preg_split( '/\X/u' ).
			$this->assertSame( 11, \cli\strwidth( $str ) );
		} else {
			$this->assertSame( 11, \cli\strwidth( $str ) ); // Tests preg_split( '/\X/u' ).
		}

		if ( function_exists( 'mb_strwidth' ) && function_exists( 'mb_detect_order' ) ) {
			putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=4' ); // Test mb_strwidth().
			mb_detect_order( array( 'UTF-8' ) );
			$this->assertSame( 11, \cli\strwidth( $str ) );
		}

		// Non-UTF-8 - both grapheme_strlen() and preg_split( '/\X/u' ) will fail.

		putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' );

		if ( function_exists( 'mb_strwidth' ) && function_exists( 'mb_detect_order' ) ) {
			// Latin-1
			mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) );
			$str = "\xe0b\xe7"; // "àbç" in ISO-8859-1
			$this->assertSame( 3, \cli\strwidth( $str ) ); // Test mb_strwidth().
			$this->assertSame( 3, mb_strwidth( $str, 'ISO-8859-1' ) );

			// Shift JIS.
			mb_detect_order( array( 'UTF-8', 'SJIS' ) );
			$str = "\x82\xb1\x82\xf1\x82\xc9\x82\xbf\x82\xcd\x90\xa2\x8a\x45!"; // "こャにちは世界!" ("Hello world!") in Shift JIS - 7 double-width chars plus Latin exclamation mark.
			$this->assertSame( 15, \cli\strwidth( $str ) ); // Test mb_strwidth().
			$this->assertSame( 15, mb_strwidth( $str, 'SJIS' ) );

			putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=8' ); // Test safe_strlen().
			if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) {
				$this->assertSame( 8, \cli\strwidth( $str ) ); // mb_strlen() - doesn't allow for double-width.
				$this->assertSame( 8, mb_strlen( $str, 'SJIS' ) );
			} else {
				$this->assertSame( 15, \cli\strwidth( $str ) ); // strlen() - no. of bytes.
				$this->assertSame( 15, strlen( $str ) );
			}
		}

		// Restore.
		putenv( false == $test_strwidth ? 'PHP_CLI_TOOLS_TEST_STRWIDTH' : "PHP_CLI_TOOLS_TEST_STRWIDTH=$test_strwidth" );
		if ( function_exists( 'mb_detect_order' ) ) {
			mb_detect_order( $mb_detect_order );
		}
	}

	function test_safe_strlen() {
		// Save.
		$test_safe_strlen = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' );
		if ( function_exists( 'mb_detect_order' ) ) {
			$mb_detect_order = mb_detect_order();
		}

		putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' );

		// UTF-8.

		// ASCII l, 3-byte kana, ASCII n, ASCII o + 2-byte combining umlaut, 6-byte Thai combining, ASCII, 3-byte Hangul. grapheme length 7, bytes 18.
		$str = 'lムnöม้p를';

		if ( \cli\can_use_icu() ) {
			putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); // Test grapheme_strlen().
			$this->assertSame( 7, \cli\safe_strlen( $str ) );
			if ( \cli\can_use_pcre_x() ) {
				putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN=2' ); // Test preg_split( '/\X/u' ).
				$this->assertSame( 7, \cli\safe_strlen( $str ) );
			}
		} elseif ( \cli\can_use_pcre_x() ) {
			$this->assertSame( 7, \cli\safe_strlen( $str ) ); // Tests preg_split( '/\X/u' ).
		} else {
			putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN=8' ); // Test strlen().
			$this->assertSame( 18, \cli\safe_strlen( $str ) ); // strlen() - no. of bytes.
			$this->assertSame( 18, strlen( $str ) );
		}

		if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) {
			putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN=4' ); // Test mb_strlen().
			mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) );
			$this->assertSame( 7, \cli\safe_strlen( $str ) );
			$this->assertSame( 9, mb_strlen( $str, 'UTF-8' ) ); // mb_strlen() - counts the 2 combining chars.
		}

		// Non-UTF-8 - both grapheme_strlen() and preg_split( '/\X/u' ) will fail.

		putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' );

		if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) {
			// Latin-1
			mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) );
			$str = "\xe0b\xe7"; // "àbç" in ISO-8859-1
			$this->assertSame( 3, \cli\safe_strlen( $str ) ); // Test mb_strlen().
			$this->assertSame( 3, mb_strlen( $str, 'ISO-8859-1' ) );
		}

		// Restore.
		putenv( false == $test_safe_strlen ? 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' : "PHP_CLI_TOOLS_TEST_SAFE_STRLEN=$test_safe_strlen" );
		if ( function_exists( 'mb_detect_order' ) ) {
			mb_detect_order( $mb_detect_order );
		}
	}
}
