/*
 * Copyright (C) 2017 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 androidx.emoji.widget;

import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.withSettings;

import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.text.Editable;
import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextWatcher;
import android.text.style.QuoteSpan;

import androidx.emoji.text.EmojiSpan;

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

@SmallTest
@RunWith(AndroidJUnit4.class)
public class SpannableBuilderTest {

    private TextWatcher mWatcher;
    private Class mClass;

    @Before
    public void setup() {
        mWatcher = mock(TextWatcher.class, withSettings().extraInterfaces(SpanWatcher.class));
        mClass = mWatcher.getClass();
    }

    @Test
    public void testConstructor() {
        new SpannableBuilder(mClass);

        new SpannableBuilder(mClass, "abc");

        new SpannableBuilder(mClass, "abc", 0, 3);

        // test spannable copying? do I need it?
    }

    @Test
    public void testSubSequence() {
        final SpannableBuilder spannable = new SpannableBuilder(mClass, "abc");
        final QuoteSpan span1 = mock(QuoteSpan.class);
        final QuoteSpan span2 = mock(QuoteSpan.class);
        spannable.setSpan(span1, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        spannable.setSpan(span2, 2, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

        final CharSequence subsequence = spannable.subSequence(0, 1);
        assertNotNull(subsequence);
        assertThat(subsequence, instanceOf(SpannableBuilder.class));

        final QuoteSpan[] spans = spannable.getSpans(0, 1, QuoteSpan.class);
        assertThat(spans, arrayWithSize(1));
        assertSame(spans[0], span1);
    }

    @Test
    public void testSetAndGetSpan() {
        final SpannableBuilder spannable = new SpannableBuilder(mClass, "abcde");
        spannable.setSpan(mWatcher, 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);

        // getSpans should return the span
        Object[] spans = spannable.getSpans(0, spannable.length(), mClass);
        assertNotNull(spans);
        assertThat(spans, arrayWithSize(1));
        assertSame(mWatcher, spans[0]);

        // span attributes should be correct
        assertEquals(1, spannable.getSpanStart(mWatcher));
        assertEquals(2, spannable.getSpanEnd(mWatcher));
        assertEquals(Spanned.SPAN_INCLUSIVE_INCLUSIVE, spannable.getSpanFlags(mWatcher));

        // should remove the span
        spannable.removeSpan(mWatcher);
        spans = spannable.getSpans(0, spannable.length(), QuoteSpan.class);
        assertNotNull(spans);
        assertThat(spans, arrayWithSize(0));
    }

    @Test
    public void testNextSpanTransition() {
        final SpannableBuilder spannable = new SpannableBuilder(mClass, "abcde");
        spannable.setSpan(mWatcher, 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        final int start = spannable.nextSpanTransition(0, spannable.length(), mClass);
        assertEquals(1, start);
    }

    @Test
    public void testBlocksSpanCallbacks_forEmojiSpans() {
        final EmojiSpan span = mock(EmojiSpan.class);
        final SpannableBuilder spannable = new SpannableBuilder(mClass, "123456");
        spannable.setSpan(mWatcher, 0, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        spannable.setSpan(span, 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        reset(mWatcher);

        spannable.delete(0, 3);

        // verify that characters are deleted
        assertEquals("456", spannable.toString());
        // verify EmojiSpan is deleted
        EmojiSpan[] spans = spannable.getSpans(0, spannable.length(), EmojiSpan.class);
        assertThat(spans, arrayWithSize(0));

        // verify the call to span callbacks are blocked
        verify((SpanWatcher) mWatcher, never()).onSpanRemoved(any(Spannable.class),
                same(span), anyInt(), anyInt());
        verify((SpanWatcher) mWatcher, never()).onSpanAdded(any(Spannable.class),
                same(span), anyInt(), anyInt());
        verify((SpanWatcher) mWatcher, never()).onSpanChanged(any(Spannable.class),
                same(span), anyInt(), anyInt(), anyInt(), anyInt());

        // verify the call to TextWatcher callbacks are called
        verify(mWatcher, times(1)).beforeTextChanged(any(CharSequence.class), anyInt(),
                anyInt(), anyInt());
        verify(mWatcher, times(1)).onTextChanged(any(CharSequence.class), anyInt(), anyInt(),
                anyInt());
        verify(mWatcher, times(1)).afterTextChanged(any(Editable.class));
    }

    @Test
    public void testDoesNotBlockSpanCallbacks_forNonEmojiSpans() {
        final QuoteSpan span = mock(QuoteSpan.class);
        final SpannableBuilder spannable = new SpannableBuilder(mClass, "123456");
        spannable.setSpan(mWatcher, 0, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        spannable.setSpan(span, 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        reset(mWatcher);

        spannable.delete(0, 3);

        // verify that characters are deleted
        assertEquals("456", spannable.toString());
        // verify QuoteSpan is deleted
        QuoteSpan[] spans = spannable.getSpans(0, spannable.length(), QuoteSpan.class);
        assertThat(spans, arrayWithSize(0));

        // verify the call to span callbacks are not blocked
        verify((SpanWatcher) mWatcher, times(1)).onSpanRemoved(any(Spannable.class),
                anyObject(), anyInt(), anyInt());

        // verify the call to TextWatcher callbacks are called
        verify(mWatcher, times(1)).beforeTextChanged(any(CharSequence.class), anyInt(), anyInt(),
                anyInt());
        verify(mWatcher, times(1)).onTextChanged(any(CharSequence.class), anyInt(), anyInt(),
                anyInt());
        verify(mWatcher, times(1)).afterTextChanged(any(Editable.class));
    }

    @Test
    public void testDoesNotBlockSpanCallbacksForOtherWatchers() {
        final TextWatcher textWatcher = mock(TextWatcher.class);
        final SpanWatcher spanWatcher = mock(SpanWatcher.class);

        final EmojiSpan span = mock(EmojiSpan.class);
        final SpannableBuilder spannable = new SpannableBuilder(mClass, "123456");
        spannable.setSpan(textWatcher, 0, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        spannable.setSpan(spanWatcher, 0, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        spannable.setSpan(span, 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        reset(textWatcher);

        spannable.delete(0, 3);

        // verify that characters are deleted
        assertEquals("456", spannable.toString());
        // verify EmojiSpan is deleted
        EmojiSpan[] spans = spannable.getSpans(0, spannable.length(), EmojiSpan.class);
        assertThat(spans, arrayWithSize(0));

        // verify the call to span callbacks are blocked
        verify(spanWatcher, times(1)).onSpanRemoved(any(Spannable.class), same(span),
                anyInt(), anyInt());

        // verify the call to TextWatcher callbacks are called
        verify(textWatcher, times(1)).beforeTextChanged(any(CharSequence.class), anyInt(),
                anyInt(), anyInt());
        verify(textWatcher, times(1)).onTextChanged(any(CharSequence.class), anyInt(), anyInt(),
                anyInt());
        verify(textWatcher, times(1)).afterTextChanged(any(Editable.class));
    }
}
