/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.text;

import static android.text.TextUtils.formatSimple;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.os.Parcel;
import android.platform.test.annotations.Presubmit;
import android.text.style.StyleSpan;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
import android.view.View;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SmallTest;

import com.google.android.collect.Lists;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * TextUtilsTest tests {@link TextUtils}.
 */
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
public class TextUtilsTest {

    @Test
    public void testBasic() {
        assertEquals("", TextUtils.concat());
        assertEquals("foo", TextUtils.concat("foo"));
        assertEquals("foobar", TextUtils.concat("foo", "bar"));
        assertEquals("foobarbaz", TextUtils.concat("foo", "bar", "baz"));

        SpannableString foo = new SpannableString("foo");
        foo.setSpan("foo", 1, 2, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);

        SpannableString bar = new SpannableString("bar");
        bar.setSpan("bar", 1, 2, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);

        SpannableString baz = new SpannableString("baz");
        baz.setSpan("baz", 1, 2, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);

        assertEquals("foo", TextUtils.concat(foo).toString());
        assertEquals("foobar", TextUtils.concat(foo, bar).toString());
        assertEquals("foobarbaz", TextUtils.concat(foo, bar, baz).toString());

        assertEquals(1, ((Spanned) TextUtils.concat(foo)).getSpanStart("foo"));

        assertEquals(1, ((Spanned) TextUtils.concat(foo, bar)).getSpanStart("foo"));
        assertEquals(4, ((Spanned) TextUtils.concat(foo, bar)).getSpanStart("bar"));

        assertEquals(1, ((Spanned) TextUtils.concat(foo, bar, baz)).getSpanStart("foo"));
        assertEquals(4, ((Spanned) TextUtils.concat(foo, bar, baz)).getSpanStart("bar"));
        assertEquals(7, ((Spanned) TextUtils.concat(foo, bar, baz)).getSpanStart("baz"));

        assertTrue(TextUtils.concat("foo", "bar") instanceof String);
        assertTrue(TextUtils.concat(foo, bar) instanceof SpannedString);
    }

    @Test
    public void testTemplateString() {
        CharSequence result;

        result = TextUtils.expandTemplate("This is a ^1 of the ^2 broadcast ^3.",
                                          "test", "emergency", "system");
        assertEquals("This is a test of the emergency broadcast system.",
                     result.toString());

        result = TextUtils.expandTemplate("^^^1^^^2^3^a^1^^b^^^c",
                                          "one", "two", "three");
        assertEquals("^one^twothree^aone^b^^c",
                     result.toString());

        result = TextUtils.expandTemplate("^");
        assertEquals("^", result.toString());

        result = TextUtils.expandTemplate("^^");
        assertEquals("^", result.toString());

        result = TextUtils.expandTemplate("^^^");
        assertEquals("^^", result.toString());

        result = TextUtils.expandTemplate("shorter ^1 values ^2.", "a", "");
        assertEquals("shorter a values .", result.toString());

        try {
            TextUtils.expandTemplate("Only ^1 value given, but ^2 used.", "foo");
            fail();
        } catch (IllegalArgumentException e) {
        }

        try {
            TextUtils.expandTemplate("^1 value given, and ^0 used.", "foo");
            fail();
        } catch (IllegalArgumentException e) {
        }

        result = TextUtils.expandTemplate("^1 value given, and ^9 used.",
                                          "one", "two", "three", "four", "five",
                                          "six", "seven", "eight", "nine");
        assertEquals("one value given, and nine used.", result.toString());

        try {
            TextUtils.expandTemplate("^1 value given, and ^10 used.",
                                     "one", "two", "three", "four", "five",
                                     "six", "seven", "eight", "nine", "ten");
            fail();
        } catch (IllegalArgumentException e) {
        }

        // putting carets in the values: expansion is not recursive.

        result = TextUtils.expandTemplate("^2", "foo", "^^");
        assertEquals("^^", result.toString());

        result = TextUtils.expandTemplate("^^2", "foo", "1");
        assertEquals("^2", result.toString());

        result = TextUtils.expandTemplate("^1", "value with ^2 in it", "foo");
        assertEquals("value with ^2 in it", result.toString());
    }

    /** Fail unless text+spans contains a span 'spanName' with the given start and end. */
    private void checkContains(Spanned text, String[] spans, String spanName,
                               int start, int end) {
        for (String i: spans) {
            if (i.equals(spanName)) {
                assertEquals(start, text.getSpanStart(i));
                assertEquals(end, text.getSpanEnd(i));
                return;
            }
        }
        fail();
    }

    @Test
    public void testTemplateSpan() {
        SpannableString template;
        Spanned result;
        String[] spans;

        // ordinary replacement

        template = new SpannableString("a^1b");
        template.setSpan("before", 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        template.setSpan("during", 1, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        template.setSpan("after", 3, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        template.setSpan("during+after", 1, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        result = (Spanned) TextUtils.expandTemplate(template, "foo");
        assertEquals(5, result.length());
        spans = result.getSpans(0, result.length(), String.class);

        // value is one character longer, so span endpoints should change.
        assertEquals(4, spans.length);
        checkContains(result, spans, "before", 0, 1);
        checkContains(result, spans, "during", 1, 4);
        checkContains(result, spans, "after", 4, 5);
        checkContains(result, spans, "during+after", 1, 5);


        // replacement with empty string

        result = (Spanned) TextUtils.expandTemplate(template, "");
        assertEquals(2, result.length());
        spans = result.getSpans(0, result.length(), String.class);

        // the "during" span should disappear.
        assertEquals(3, spans.length);
        checkContains(result, spans, "before", 0, 1);
        checkContains(result, spans, "after", 1, 2);
        checkContains(result, spans, "during+after", 1, 2);
    }

    @Test
    public void testStringSplitterSimple() {
        stringSplitterTestHelper("a,b,cde", new String[] {"a", "b", "cde"});
    }

    @Test
    public void testStringSplitterEmpty() {
        stringSplitterTestHelper("", new String[] {});
    }

    @Test
    public void testStringSplitterWithLeadingEmptyString() {
        stringSplitterTestHelper(",a,b,cde", new String[] {"", "a", "b", "cde"});
    }

    @Test
    public void testStringSplitterWithInternalEmptyString() {
        stringSplitterTestHelper("a,b,,cde", new String[] {"a", "b", "", "cde"});
    }

    @Test
    public void testStringSplitterWithTrailingEmptyString() {
        // A single trailing emtpy string should be ignored.
        stringSplitterTestHelper("a,b,cde,", new String[] {"a", "b", "cde"});
    }

    private void stringSplitterTestHelper(String string, String[] expectedStrings) {
        TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(',');
        splitter.setString(string);
        List<String> strings = Lists.newArrayList();
        for (String s : splitter) {
            strings.add(s);
        }
        assertArrayEquals(expectedStrings, strings.toArray(new String[]{}));
    }

    @Test
    public void testTrim() {
        String[] strings = { "abc", " abc", "  abc", "abc ", "abc  ",
                             " abc ", "  abc  ", "\nabc\n", "\nabc", "abc\n" };

        for (String s : strings) {
            assertEquals(s.trim().length(), TextUtils.getTrimmedLength(s));
        }
    }

    @Test
    public void testRfc822TokenizerFullAddress() {
        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize("Foo Bar (something) <foo@google.com>");
        assertNotNull(tokens);
        assertEquals(1, tokens.length);
        assertEquals("foo@google.com", tokens[0].getAddress());
        assertEquals("Foo Bar", tokens[0].getName());
        assertEquals("something",tokens[0].getComment());
    }

    @Test
    public void testRfc822TokenizeItemWithError() {
        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize("\"Foo Bar\\");
        assertNotNull(tokens);
        assertEquals(1, tokens.length);
        assertEquals("Foo Bar", tokens[0].getAddress());
    }

    @Test
    public void testRfc822FindToken() {
        Rfc822Tokenizer tokenizer = new Rfc822Tokenizer();
        //                0           1         2           3         4
        //                0 1234 56789012345678901234 5678 90123456789012345
        String address = "\"Foo\" <foo@google.com>, \"Bar\" <bar@google.com>";
        assertEquals(0, tokenizer.findTokenStart(address, 21));
        assertEquals(22, tokenizer.findTokenEnd(address, 21));
        assertEquals(24, tokenizer.findTokenStart(address, 25));
        assertEquals(46, tokenizer.findTokenEnd(address, 25));
    }

    @Test
    public void testRfc822FindTokenWithError() {
        assertEquals(9, new Rfc822Tokenizer().findTokenEnd("\"Foo Bar\\", 0));
    }

    @LargeTest
    @Test
    public void testEllipsize() {
        CharSequence s1 = "The quick brown fox jumps over \u00FEhe lazy dog.";
        CharSequence s2 = new Wrapper(s1);
        Spannable s3 = new SpannableString(s1);
        s3.setSpan(new StyleSpan(0), 5, 10, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        TextPaint p = new TextPaint();
        p.setFlags(p.getFlags() & ~p.DEV_KERN_TEXT_FLAG);

        for (int i = 0; i < 100; i++) {
            for (int j = 0; j < 3; j++) {
                TextUtils.TruncateAt kind = null;

                switch (j) {
                case 0:
                    kind = TextUtils.TruncateAt.START;
                    break;

                case 1:
                    kind = TextUtils.TruncateAt.END;
                    break;

                case 2:
                    kind = TextUtils.TruncateAt.MIDDLE;
                    break;
                }

                String out1 = TextUtils.ellipsize(s1, p, i, kind).toString();
                String out2 = TextUtils.ellipsize(s2, p, i, kind).toString();
                String out3 = TextUtils.ellipsize(s3, p, i, kind).toString();

                String keep1 = TextUtils.ellipsize(s1, p, i, kind, true, null).toString();
                String keep2 = TextUtils.ellipsize(s2, p, i, kind, true, null).toString();
                String keep3 = TextUtils.ellipsize(s3, p, i, kind, true, null).toString();

                String trim1 = keep1.replace("\uFEFF", "");

                // Are all normal output strings identical?
                assertEquals("wid " + i + " pass " + j, out1, out2);
                assertEquals("wid " + i + " pass " + j, out2, out3);

                // Are preserved output strings identical?
                assertEquals("wid " + i + " pass " + j, keep1, keep2);
                assertEquals("wid " + i + " pass " + j, keep2, keep3);

                // Does trimming padding from preserved yield normal?
                assertEquals("wid " + i + " pass " + j, out1, trim1);

                // Did preserved output strings preserve length?
                assertEquals("wid " + i + " pass " + j, keep1.length(), s1.length());

                // Does the output string actually fit in the space?
                assertTrue("wid " + i + " pass " + j, p.measureText(out1) <= i);

                // Is the padded output the same width as trimmed output?
                assertTrue("wid " + i + " pass " + j, p.measureText(keep1) == p.measureText(out1));
            }
        }
    }

    @Test
    public void testEllipsize_multiCodepoint() {
        final TextPaint paint = new TextPaint();
        final float wordWidth = paint.measureText("MMMM");

        // Establish the ground rules first, for single-codepoint cases.
        final String ellipsis = "."; // one full stop character
        assertEquals(
                "MM.\uFEFF",
                TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth,
                        TextUtils.TruncateAt.END, true /* preserve length */,
                        null /* no callback */, TextDirectionHeuristics.LTR,
                        ellipsis));
        assertEquals(
                "MM.",
                TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth,
                        TextUtils.TruncateAt.END, false /* preserve length */,
                        null /* no callback */, TextDirectionHeuristics.LTR,
                        ellipsis));
        assertEquals(
                "M.",
                TextUtils.ellipsize("MM", paint, 0.45f * wordWidth,
                        TextUtils.TruncateAt.END, true /* preserve length */,
                        null /* no callback */, TextDirectionHeuristics.LTR,
                        ellipsis));
        assertEquals(
                "M.",
                TextUtils.ellipsize("MM", paint, 0.45f * wordWidth,
                        TextUtils.TruncateAt.END, false /* preserve length */,
                        null /* no callback */, TextDirectionHeuristics.LTR,
                        ellipsis));

        // Now check the differences for multi-codepoint ellipsis.
        final String longEllipsis = ".."; // two full stop characters
        assertEquals(
                "MM..",
                TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth,
                        TextUtils.TruncateAt.END, true /* preserve length */,
                        null /* no callback */, TextDirectionHeuristics.LTR,
                        longEllipsis));
        assertEquals(
                "MM..",
                TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth,
                        TextUtils.TruncateAt.END, false /* preserve length */,
                        null /* no callback */, TextDirectionHeuristics.LTR,
                        longEllipsis));
        assertEquals(
                "M\uFEFF",
                TextUtils.ellipsize("MM", paint, 0.45f * wordWidth,
                        TextUtils.TruncateAt.END, true /* preserve length */,
                        null /* no callback */, TextDirectionHeuristics.LTR,
                        longEllipsis));
        assertEquals(
                "M..",
                TextUtils.ellipsize("MM", paint, 0.45f * wordWidth,
                        TextUtils.TruncateAt.END, false /* preserve length */,
                        null /* no callback */, TextDirectionHeuristics.LTR,
                        longEllipsis));
    }

    @Test
    public void testDelimitedStringContains() {
        assertFalse(TextUtils.delimitedStringContains("", ',', null));
        assertFalse(TextUtils.delimitedStringContains(null, ',', ""));
        // Whole match
        assertTrue(TextUtils.delimitedStringContains("gps", ',', "gps"));
        // At beginning.
        assertTrue(TextUtils.delimitedStringContains("gps,gpsx,network,mock", ',', "gps"));
        assertTrue(TextUtils.delimitedStringContains("gps,network,mock", ',', "gps"));
        // In middle, both without, before & after a false match.
        assertTrue(TextUtils.delimitedStringContains("network,gps,mock", ',', "gps"));
        assertTrue(TextUtils.delimitedStringContains("network,gps,gpsx,mock", ',', "gps"));
        assertTrue(TextUtils.delimitedStringContains("network,gpsx,gps,mock", ',', "gps"));
        // At the end.
        assertTrue(TextUtils.delimitedStringContains("network,mock,gps", ',', "gps"));
        assertTrue(TextUtils.delimitedStringContains("network,mock,gpsx,gps", ',', "gps"));
        // Not present (but with a false match)
        assertFalse(TextUtils.delimitedStringContains("network,mock,gpsx", ',', "gps"));
    }

    @Test
    public void testCharSequenceCreator() {
        Parcel p = Parcel.obtain();
        CharSequence text;
        try {
            TextUtils.writeToParcel(null, p, 0);
            text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
            assertNull("null CharSequence should generate null from parcel", text);
        } finally {
            p.recycle();
        }
        p = Parcel.obtain();
        try {
            TextUtils.writeToParcel("test", p, 0);
            p.setDataPosition(0);
            text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
            assertEquals("conversion to/from parcel failed", "test", text);
        } finally {
            p.recycle();
        }
    }

    @Test
    public void testCharSequenceCreatorNull() {
        Parcel p;
        CharSequence text;
        p = Parcel.obtain();
        try {
            TextUtils.writeToParcel(null, p, 0);
            p.setDataPosition(0);
            text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
            assertNull("null CharSequence should generate null from parcel", text);
        } finally {
            p.recycle();
        }
    }

    @Test
    public void testCharSequenceCreatorSpannable() {
        Parcel p;
        CharSequence text;
        p = Parcel.obtain();
        try {
            TextUtils.writeToParcel(new SpannableString("test"), p, 0);
            p.setDataPosition(0);
            text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
            assertEquals("conversion to/from parcel failed", "test", text.toString());
        } finally {
            p.recycle();
        }
    }

    @Test
    public void testCharSequenceCreatorString() {
        Parcel p;
        CharSequence text;
        p = Parcel.obtain();
        try {
            TextUtils.writeToParcel("test", p, 0);
            p.setDataPosition(0);
            text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
            assertEquals("conversion to/from parcel failed", "test", text.toString());
        } finally {
            p.recycle();
        }
    }

    /**
     * CharSequence wrapper for testing the cases where text is copied into
     * a char array instead of working from a String or a Spanned.
     */
    private static class Wrapper implements CharSequence {
        private CharSequence mString;

        public Wrapper(CharSequence s) {
            mString = s;
        }

        @Override
        public int length() {
            return mString.length();
        }

        @Override
        public char charAt(int off) {
            return mString.charAt(off);
        }

        @Override
        public String toString() {
            return mString.toString();
        }

        @Override
        public CharSequence subSequence(int start, int end) {
            return new Wrapper(mString.subSequence(start, end));
        }
    }

    @Test
    public void testRemoveEmptySpans() {
        MockSpanned spanned = new MockSpanned();

        spanned.test();
        spanned.addSpan().test();
        spanned.addSpan().test();
        spanned.addSpan().test();
        spanned.addEmptySpan().test();
        spanned.addSpan().test();
        spanned.addEmptySpan().test();
        spanned.addEmptySpan().test();
        spanned.addSpan().test();

        spanned.clear();
        spanned.addEmptySpan().test();
        spanned.addEmptySpan().test();
        spanned.addEmptySpan().test();
        spanned.addSpan().test();
        spanned.addEmptySpan().test();
        spanned.addSpan().test();

        spanned.clear();
        spanned.addSpan().test();
        spanned.addEmptySpan().test();
        spanned.addSpan().test();
        spanned.addEmptySpan().test();
        spanned.addSpan().test();
        spanned.addSpan().test();
    }

    protected static class MockSpanned implements Spanned {

        private List<Object> allSpans = new ArrayList<Object>();
        private List<Object> nonEmptySpans = new ArrayList<Object>();

        public void clear() {
            allSpans.clear();
            nonEmptySpans.clear();
        }

        public MockSpanned addSpan() {
            Object o = new Object();
            allSpans.add(o);
            nonEmptySpans.add(o);
            return this;
        }

        public MockSpanned addEmptySpan() {
            Object o = new Object();
            allSpans.add(o);
            return this;
        }

        public void test() {
            Object[] nonEmpty = TextUtils.removeEmptySpans(allSpans.toArray(), this, Object.class);
            assertEquals("Mismatched array size", nonEmptySpans.size(), nonEmpty.length);
            for (int i=0; i<nonEmpty.length; i++) {
                assertEquals("Span differ", nonEmptySpans.get(i), nonEmpty[i]);
            }
        }

        @Override
        public char charAt(int arg0) {
            return 0;
        }

        @Override
        public int length() {
            return 0;
        }

        @Override
        public CharSequence subSequence(int arg0, int arg1) {
            return null;
        }

        @Override
        public <T> T[] getSpans(int start, int end, Class<T> type) {
            return null;
        }

        @Override
        public int getSpanStart(Object tag) {
            return 0;
        }

        @Override
        public int getSpanEnd(Object tag) {
            return nonEmptySpans.contains(tag) ? 1 : 0;
        }

        @Override
        public int getSpanFlags(Object tag) {
            return 0;
        }

        @Override
        public int nextSpanTransition(int start, int limit, Class type) {
            return 0;
        }
    }

    @Test
    public void testGetLayoutDirectionFromLocale() {
        assertEquals(View.LAYOUT_DIRECTION_LTR, TextUtils.getLayoutDirectionFromLocale(null));
        assertEquals(View.LAYOUT_DIRECTION_LTR,
                TextUtils.getLayoutDirectionFromLocale(Locale.ROOT));
        assertEquals(View.LAYOUT_DIRECTION_LTR,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("en")));
        assertEquals(View.LAYOUT_DIRECTION_LTR,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("en-US")));
        assertEquals(View.LAYOUT_DIRECTION_LTR,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az")));
        assertEquals(View.LAYOUT_DIRECTION_LTR,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az-AZ")));
        assertEquals(View.LAYOUT_DIRECTION_LTR,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az-Latn")));
        assertEquals(View.LAYOUT_DIRECTION_LTR,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("en-EG")));
        assertEquals(View.LAYOUT_DIRECTION_LTR,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("ar-Latn")));

        assertEquals(View.LAYOUT_DIRECTION_RTL,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("ar")));
        assertEquals(View.LAYOUT_DIRECTION_RTL,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("fa")));
        assertEquals(View.LAYOUT_DIRECTION_RTL,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("he")));
        assertEquals(View.LAYOUT_DIRECTION_RTL,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("iw")));
        assertEquals(View.LAYOUT_DIRECTION_RTL,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("ur")));
        assertEquals(View.LAYOUT_DIRECTION_RTL,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("dv")));
        assertEquals(View.LAYOUT_DIRECTION_RTL,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az-Arab")));
        assertEquals(View.LAYOUT_DIRECTION_RTL,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az-IR")));
        assertEquals(View.LAYOUT_DIRECTION_RTL,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("fa-US")));
        assertEquals(View.LAYOUT_DIRECTION_RTL,
                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("tr-Arab")));
    }

    @Test
    public void testToUpperCase() {
        {
            final CharSequence result = TextUtils.toUpperCase(null, "abc", false);
            assertEquals(StringBuilder.class, result.getClass());
            assertEquals("ABC", result.toString());
        }
        {
            final SpannableString str = new SpannableString("abc");
            Object span = new Object();
            str.setSpan(span, 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);

            final CharSequence result = TextUtils.toUpperCase(null, str, true /* copySpans */);
            assertEquals(SpannableStringBuilder.class, result.getClass());
            assertEquals("ABC", result.toString());
            final Spanned spanned = (Spanned) result;
            final Object[] resultSpans = spanned.getSpans(0, result.length(), Object.class);
            assertEquals(1, resultSpans.length);
            assertSame(span, resultSpans[0]);
            assertEquals(1, spanned.getSpanStart(span));
            assertEquals(2, spanned.getSpanEnd(span));
            assertEquals(Spanned.SPAN_INCLUSIVE_INCLUSIVE, spanned.getSpanFlags(span));
        }
        {
            final Locale turkish = new Locale("tr", "TR");
            final CharSequence result = TextUtils.toUpperCase(turkish, "i", false);
            assertEquals(StringBuilder.class, result.getClass());
            assertEquals("İ", result.toString());
        }
        {
            final String str = "ABC";
            assertSame(str, TextUtils.toUpperCase(null, str, false));
        }
        {
            final SpannableString str = new SpannableString("ABC");
            str.setSpan(new Object(), 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
            assertSame(str, TextUtils.toUpperCase(null, str, true /* copySpans */));
        }
    }

    // Copied from cts/tests/tests/widget/src/android/widget/cts/TextViewTest.java and modified
    // for the TextUtils.toUpperCase method.
    @Test
    public void testToUpperCase_SpansArePreserved() {
        final Locale greek = new Locale("el", "GR");
        final String lowerString = "ι\u0301ριδα";  // ίριδα with first letter decomposed
        final String upperString = "ΙΡΙΔΑ";  // uppercased
        // expected lowercase to uppercase index map
        final int[] indexMap = {0, 1, 1, 2, 3, 4, 5};
        final int flags = Spanned.SPAN_INCLUSIVE_INCLUSIVE;

        final Spannable source = new SpannableString(lowerString);
        source.setSpan(new Object(), 0, 1, flags);
        source.setSpan(new Object(), 1, 2, flags);
        source.setSpan(new Object(), 2, 3, flags);
        source.setSpan(new Object(), 3, 4, flags);
        source.setSpan(new Object(), 4, 5, flags);
        source.setSpan(new Object(), 5, 6, flags);
        source.setSpan(new Object(), 0, 2, flags);
        source.setSpan(new Object(), 1, 3, flags);
        source.setSpan(new Object(), 2, 4, flags);
        source.setSpan(new Object(), 0, 6, flags);
        final Object[] sourceSpans = source.getSpans(0, source.length(), Object.class);

        final CharSequence uppercase = TextUtils.toUpperCase(greek, source, true /* copySpans */);
        assertEquals(SpannableStringBuilder.class, uppercase.getClass());
        final Spanned result = (Spanned) uppercase;

        assertEquals(upperString, result.toString());
        final Object[] resultSpans = result.getSpans(0, result.length(), Object.class);
        assertEquals(sourceSpans.length, resultSpans.length);
        for (int i = 0; i < sourceSpans.length; i++) {
            assertSame(sourceSpans[i], resultSpans[i]);
            final Object span = sourceSpans[i];
            assertEquals(indexMap[source.getSpanStart(span)], result.getSpanStart(span));
            assertEquals(indexMap[source.getSpanEnd(span)], result.getSpanEnd(span));
            assertEquals(source.getSpanFlags(span), result.getSpanFlags(span));
        }
    }

    @Test
    public void testTrimToSize() {
        final String testString = "a\uD800\uDC00a";
        assertEquals("Should return text as it is if size is longer than length",
                testString, TextUtils.trimToSize(testString, 5));
        assertEquals("Should return text as it is if size is equal to length",
                testString, TextUtils.trimToSize(testString, 4));
        assertEquals("Should trim text",
                "a\uD800\uDC00", TextUtils.trimToSize(testString, 3));
        assertEquals("Should trim surrogate pairs if size is in the middle of a pair",
                "a", TextUtils.trimToSize(testString, 2));
        assertEquals("Should trim text",
                "a", TextUtils.trimToSize(testString, 1));
        assertEquals("Should handle null",
                null, TextUtils.trimToSize(null, 1));

        assertEquals("Should trim high surrogate if invalid surrogate",
                "a\uD800", TextUtils.trimToSize("a\uD800\uD800", 2));
        assertEquals("Should trim low surrogate if invalid surrogate",
                "a\uDC00", TextUtils.trimToSize("a\uDC00\uDC00", 2));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testTrimToSizeThrowsExceptionForNegativeSize() {
        TextUtils.trimToSize("", -1);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testTrimToSizeThrowsExceptionForZeroSize() {
        TextUtils.trimToSize("abc", 0);
    }

    @Test
    public void truncateStringForUtf8Storage() {
        assertEquals("", TextUtils.truncateStringForUtf8Storage("abc", 0));

        //================ long normal case ================
        StringBuilder builder = new StringBuilder();

        int n = 50;
        for (int i = 0; i < 2 * n; i++) {
            builder.append("哈");
        }
        String initial = builder.toString();
        String result = TextUtils.truncateStringForUtf8Storage(initial, n);

        // Result should be the beginning of initial
        assertTrue(initial.startsWith(result));

        // Result should take less than n bytes in UTF-8
        assertTrue(result.getBytes(StandardCharsets.UTF_8).length <= n);

        // result + the next codePoint should take strictly more than
        // n bytes in UTF-8
        assertTrue(initial.substring(0, initial.offsetByCodePoints(result.length(), 1))
                .getBytes(StandardCharsets.UTF_8).length > n);

        // =================== short normal case =====================
        String s = "sf\u20ACgk\u00E9ls\u00E9fg";
        result = TextUtils.truncateStringForUtf8Storage(s, 100);
        assertEquals(s, result);
    }

    @Test
    public void testTruncateInMiddleOfSurrogate() {
        StringBuilder builder = new StringBuilder();
        String beginning = "a";
        builder.append(beginning);
        builder.append(Character.toChars(0x1D11E));

        String result = TextUtils.truncateStringForUtf8Storage(builder.toString(), 3);

        // \u1D11E is a surrogate and needs 4 bytes in UTF-8. beginning == "a" uses
        // only 1 bytes in UTF8
        // As we allow only 3 bytes for the whole string, so just 2 for this
        // codePoint, there is not enough place and the string will be truncated
        // just before it
        assertEquals(beginning, result);
    }

    @Test
    public void testTruncateInMiddleOfChar() {
        StringBuilder builder = new StringBuilder();
        String beginning = "a";
        builder.append(beginning);
        builder.append(Character.toChars(0x20AC));

        String result = TextUtils.truncateStringForUtf8Storage(builder.toString(), 3);

        // Like above, \u20AC uses 3 bytes in UTF-8, with "beginning", that makes
        // 4 bytes so it is too big and should be truncated
        assertEquals(beginning, result);
    }

    @Test
    public void testTruncateSubString() {
        String test = "sdgkl;hjsl;gjhdgkljdfhglkdj";
        String sub = test.substring(10, 20);
        String res = TextUtils.truncateStringForUtf8Storage(sub, 255);
        assertEquals(sub, res);
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void truncateStringForUtf8StorageThrowsExceptionForNegativeSize() {
        TextUtils.truncateStringForUtf8Storage("abc", -1);
    }

    @Test
    public void length() {
        assertEquals(0, TextUtils.length(null));
        assertEquals(0, TextUtils.length(""));
        assertEquals(2, TextUtils.length("  "));
        assertEquals(6, TextUtils.length("Hello!"));
    }

    @Test
    public void testTrimToLengthWithEllipsis() {
        assertEquals("ABC...", TextUtils.trimToLengthWithEllipsis("ABCDEF", 3));
        assertEquals("ABC", TextUtils.trimToLengthWithEllipsis("ABC", 3));
        assertEquals("", TextUtils.trimToLengthWithEllipsis("", 3));
        assertNull(TextUtils.trimToLengthWithEllipsis(null, 3));
    }

    @Test
    public void testFormatSimple_Types() {
        assertEquals("true", formatSimple("%b", true));
        assertEquals("false", formatSimple("%b", false));
        assertEquals("true", formatSimple("%b", this));
        assertEquals("false", formatSimple("%b", new Object[] { null }));

        assertEquals("!", formatSimple("%c", '!'));

        assertEquals("42", formatSimple("%d", 42));
        assertEquals("281474976710656", formatSimple("%d", 281474976710656L));

        assertEquals("3.14159", formatSimple("%f", 3.14159));
        assertEquals("NaN", formatSimple("%f", Float.NaN));

        assertEquals("example", formatSimple("%s", "example"));
        assertEquals("null", formatSimple("%s", new Object[] { null }));

        assertEquals("2a", formatSimple("%x", 42));
        assertEquals("1000000000000", formatSimple("%x", 281474976710656L));

        assertEquals("%", formatSimple("%%"));
    }

    @Test
    public void testFormatSimple_Width() {
        assertEquals("42", formatSimple("%1d", 42));
        assertEquals("42", formatSimple("%2d", 42));
        assertEquals(" 42", formatSimple("%3d", 42));
        assertEquals("  42", formatSimple("%4d", 42));
        assertEquals("  42  42", formatSimple("%4d%4d", 42, 42));
        assertEquals(" -42", formatSimple("%4d", -42));
        assertEquals("        42", formatSimple("%10d", 42));

        assertEquals("42", formatSimple("%01d", 42));
        assertEquals("42", formatSimple("%02d", 42));
        assertEquals("042", formatSimple("%03d", 42));
        assertEquals("0042", formatSimple("%04d", 42));
        assertEquals("00420042", formatSimple("%04d%04d", 42, 42));
        assertEquals("-042", formatSimple("%04d", -42));
        assertEquals("0000000042", formatSimple("%010d", 42));
    }

    @Test
    public void testFormatSimple_Empty() {
        assertEquals("", formatSimple(""));
    }

    @Test
    public void testFormatSimple_Typical() {
        assertEquals("String foobar and %% number -42 together",
                formatSimple("String %s%s and %%%% number %d%d together", "foo", "bar", -4, 2));
    }

    @Test
    public void testFormatSimple_Advanced() {
        assertEquals("000000000000002a.ext",
                formatSimple("%016x.%s", 42, "ext"));
        assertEquals("crtcl=0x002a:intrsv=Y:grnk=0x0018:gsmry=A:example:rnk=0x0000",
                formatSimple("crtcl=0x%04x:intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
                        42, 'Y', 24, 'A', "example", 0));
    }

    @Test
    public void testFormatSimple_Mismatch() {
        try {
            formatSimple("%s");
            fail();
        } catch (IllegalArgumentException expected) {
        }
        try {
            formatSimple("%s", "foo", "bar");
            fail();
        } catch (IllegalArgumentException expected) {
        }
    }
}
