package com.beloo.widget.chipslayoutmanager;

import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.support.annotation.UiThread;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.ViewAction;
import android.support.test.espresso.ViewInteraction;
import android.support.test.espresso.contrib.RecyclerViewActions;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;

import com.beloo.chipslayoutmanager.sample.entity.ChipsEntity;
import com.beloo.chipslayoutmanager.sample.ui.LayoutManagerFactory;
import com.beloo.chipslayoutmanager.sample.ui.ChipsFacade;
import com.beloo.chipslayoutmanager.sample.ui.TestActivity;
import com.beloo.chipslayoutmanager.sample.ui.adapter.ChipsAdapter;
import com.beloo.widget.chipslayoutmanager.util.InstrumentalUtil;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;

import com.beloo.chipslayoutmanager.sample.R;

import java.util.List;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import static com.beloo.test.util.RecyclerViewEspressoFactory.*;

/**
 */
@RunWith(AndroidJUnit4.class)
public class RowTest {

    static {
        TestActivity.isInitializeOutside = true;
    }

    @Rule
    public ActivityTestRule<TestActivity> activityTestRule = new ActivityTestRule<TestActivity>(TestActivity.class) {
        @Override
        protected void afterActivityLaunched() {
            super.afterActivityLaunched();

        }
    };

    private ChipsLayoutManager layoutManager;
    private TestActivity activity;

    private List<ChipsEntity> items;

    private ViewInteraction recyclerView;

    @Before
    public final void setUp() throws Throwable {
        MockitoAnnotations.initMocks(this);
        activity = activityTestRule.getActivity();

        LayoutManagerFactory layoutManagerFactory = new LayoutManagerFactory() {
            @Override
            public RecyclerView.LayoutManager layoutManager(Context context) {
                return retrieveLayoutManager();
            }
        };

        RecyclerView rvTest = (RecyclerView) activityTestRule.getActivity().findViewById(R.id.rvTest);
        //disable all animations
        rvTest.setItemAnimator(null);

        //set items
        ChipsFacade chipsFacade = spy(new ChipsFacade());
        items = chipsFacade.getItems();
        when(chipsFacade.getItems()).thenReturn(items);
        TestActivity.setItemsFactory(chipsFacade);

        TestActivity.setLmFactory(layoutManagerFactory);

        recyclerView = onView(withId(R.id.rvTest)).check(matches(isDisplayed()));

        onSetUp();

        activity.runOnUiThread(() -> activity.initialize());
    }

    public void onSetUp() throws Exception {}

    private RecyclerView.LayoutManager retrieveLayoutManager() {
        layoutManager = getLayoutManager();
        return layoutManager;
    }

    @UiThread
    protected ChipsLayoutManager getLayoutManager() {
        if (activityTestRule.getActivity() == null) return null;
        return ChipsLayoutManager.newBuilder(activityTestRule.getActivity())
                .setOrientation(ChipsLayoutManager.HORIZONTAL)
                .build();
    }

    ///////////////////////////////////////////////////////////////////////////
    // layouting, main feature
    ///////////////////////////////////////////////////////////////////////////

    @Test
    public void layouting_ScrollForwardOffScreenAndBackward_ItemsStayOnASamePlace() throws Exception {
        InstrumentalUtil.waitForIdle();

        //arrange
        RecyclerView rvTest = (RecyclerView) activityTestRule.getActivity().findViewById(R.id.rvTest);
        View child = getViewForPosition(rvTest, 7);
        Rect expectedViewRect = layoutManager.getCanvas().getViewRect(child);

        //act
        recyclerView.perform(scrollBy(0, 1000), scrollBy(0, -1000));
        Rect resultViewRect = layoutManager.getCanvas().getViewRect(child);

        //assert
        assertEquals(expectedViewRect, resultViewRect);
    }

    @Test
    public void layouting_ScrollForwardOnScreenAndBackward_ItemsStayOnASamePlace() throws Exception {
        InstrumentalUtil.waitForIdle();

        //arrange
        RecyclerView rvTest = (RecyclerView) activityTestRule.getActivity().findViewById(R.id.rvTest);
        View child = getViewForPosition(rvTest, 6);
        Rect expectedViewRect = layoutManager.getCanvas().getViewRect(child);

        //act
        recyclerView.perform(scrollBy(0, 250), scrollBy(0, -250));
        Rect resultViewRect = layoutManager.getCanvas().getViewRect(child);

        //assert
        assertEquals(expectedViewRect, resultViewRect);
    }

    @Test
    public void layouting_ScrollForwardAndBackward_VerifyCorrectOrder () throws Exception {
        //arrange
        //act
        recyclerView.perform(scrollBy(0, 300));
        recyclerView.perform(scrollBy(0, -300));
        InstrumentalUtil.waitForIdle();
        //assert
        recyclerView.check(matches(incrementOrder()));
    }

    ///////////////////////////////////////////////////////////////////////////
    // scroll
    ///////////////////////////////////////////////////////////////////////////

    @Test
    public void scrollBy_LMInInitialStateAndScrollForward_CorrectFirstCompletelyVisibleItem() throws Exception {
        //arrange
        InstrumentalUtil.waitForIdle();
        //act
        recyclerView.perform(scrollBy(0, 300));
        int actual = layoutManager.findFirstCompletelyVisibleItemPosition();
        //assert
        assertEquals(4, actual);
    }

    @Test
    public void scrollBy_ScrolledForwardScrollBackward_CorrectFirstCompletelyVisibleItem() throws Exception {
        //arrange
        InstrumentalUtil.waitForIdle();
        recyclerView.perform(scrollBy(0, 300));

        //act
        recyclerView.perform(scrollBy(0, -300));
        int actual = layoutManager.findFirstCompletelyVisibleItemPosition();

        //assert
        assertEquals(0, actual);
    }

    @Test
    public void scrollBy_LastItemInLastRowHasSmallSize_scrolledCompletelyToBiggestItemSize() throws Exception {
        //arrange
        {
            items.remove(39);
            items.remove(37);
        }
        activity.runOnUiThread(() -> activity.initialize());
        InstrumentalUtil.waitForIdle();

        //act
        recyclerView.perform(RecyclerViewActions.scrollToPosition(37),
                scrollBy(0, -200),
                scrollBy(0, 200));


        //assert
        recyclerView.check(matches(atPosition(36, rvPaddingMatcher())));
    }

    @Test
    public void scrollToPosition_LMInInitialState_FirstVisiblePositionsEqualsScrollingTarget() throws Exception {
        //arrange

        //act
        recyclerView.perform(RecyclerViewActions.scrollToPosition(8));
        InstrumentalUtil.waitForIdle();

        //assert
        int actual = layoutManager.findFirstCompletelyVisibleItemPosition();
        assertEquals(8, actual);
    }

    @Test
    public synchronized void smoothScrollToPosition_LMInInitialState_FirstVisiblePositionsEqualsScrollingTarget() throws Exception {
        //arrange
        InstrumentalUtil.waitForIdle();

        //act
        ViewAction scrollAction = smoothScrollToPosition(8);
        //noinspection SynchronizationOnLocalVariableOrMethodParameter
        synchronized (scrollAction) {
            recyclerView.perform(scrollAction);
            //wait for completion of SmoothScrollAction
            scrollAction.wait();
        }

        //assert
        int actual = layoutManager.findFirstCompletelyVisibleItemPosition();
        assertEquals(8, actual);
    }

    ///////////////////////////////////////////////////////////////////////////
    // find visible item
    ///////////////////////////////////////////////////////////////////////////

    @Test
    public void findFirstVisibleItem_scrolledCompletelyToItemInTheMiddle_resultCorrect() throws Exception {
        //arrange
        recyclerView.perform(RecyclerViewActions.scrollToPosition(7),
                scrollBy(0, 50));

        //act
        int actual = layoutManager.findFirstVisibleItemPosition();

        //assert
        assertEquals(6, actual);
    }

    @Test
    public void findFirstCompletelyVisibleItem_scrolledCompletelyToItemInTheMiddle_resultCorrect() throws Exception {
        //arrange
        recyclerView.perform(RecyclerViewActions.scrollToPosition(7),
                scrollBy(0, 50));

        //act
        int actual = layoutManager.findFirstCompletelyVisibleItemPosition();

        //assert
        assertEquals(7, actual);
    }

    @Test
    public void findLastVisibleItem_scrolledCompletelyToItemInTheMiddle_resultCorrect() throws Exception {
        //arrange
        recyclerView.perform(RecyclerViewActions.scrollToPosition(7));
        InstrumentalUtil.waitForIdle();

        //act
        int actual = layoutManager.findLastVisibleItemPosition();

        //assert
        assertEquals(19, actual);
    }

    @Test
    public void findLastCompletelyVisibleItem_scrolledCompletelyToItemInTheMiddle_resultCorrect() throws Exception {
        //arrange
        recyclerView.perform(RecyclerViewActions.scrollToPosition(7));
        InstrumentalUtil.waitForIdle();

        //act
        int actual = layoutManager.findLastCompletelyVisibleItemPosition();

        //assert
        assertEquals(17, actual);
    }

    ///////////////////////////////////////////////////////////////////////////
    // rotate
    ///////////////////////////////////////////////////////////////////////////

    void rotateAndWaitIdle() throws Exception {
        //arrange

        final int orientation = InstrumentationRegistry.getTargetContext()
                .getResources()
                .getConfiguration()
                .orientation;

        //act
        activityTestRule.getActivity().setRequestedOrientation(
                orientation == Configuration.ORIENTATION_PORTRAIT ?
                        ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE :
                        ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        InstrumentalUtil.waitForIdle();

        //verify no exceptions
    }

    void resetToInitialAfterRotate() throws Exception {
        activityTestRule.launchActivity(new Intent(activity, TestActivity.class));
        InstrumentalUtil.waitForIdle();
    }

    @Test
    public void rotate_LMHasItems_firstItemNotChanged() throws Exception {
        //arrange
        recyclerView.perform(RecyclerViewActions.scrollToPosition(7));
        InstrumentalUtil.waitForIdle();

        int expected = layoutManager.findFirstVisibleItemPosition();
        //act
        rotateAndWaitIdle();
        int actual = layoutManager.findFirstVisibleItemPosition();
        resetToInitialAfterRotate();

        //assert
        assertNotEquals(-1, expected);
        assertNotEquals(-1, actual);
        assertEquals("first visible positions before and after rotation doesn't match", expected, actual);
        System.out.println("first visible position = " + actual);
    }

    @Test
    public void rotate_ScrolledToEndOfItems_BottomPaddingStaySame() throws Exception {
        InstrumentalUtil.waitForIdle();
        //arrange
        RecyclerView rvTest = (RecyclerView) activityTestRule.getActivity().findViewById(R.id.rvTest);
        recyclerView.perform(RecyclerViewActions.scrollToPosition(layoutManager.getItemCount() - 1));

        InstrumentalUtil.waitForIdle();
        Thread.sleep(200);

        View child = getViewForPosition(rvTest, layoutManager.findLastVisibleItemPosition());
        double bottom = (rvTest.getY() + rvTest.getHeight()) - (child.getY() + child.getHeight());

        Log.println(Log.ASSERT, "rotateTest", "expected child padding = " + bottom);

        //act
        rotateAndWaitIdle();
        recyclerView.perform(RecyclerViewActions.scrollToPosition(layoutManager.getItemCount() - 1));
        rotateAndWaitIdle();
        rvTest = (RecyclerView) activityTestRule.getActivity().findViewById(R.id.rvTest);
        child = getViewForPosition(rvTest, layoutManager.findLastVisibleItemPosition());
        double result = (rvTest.getY() + rvTest.getHeight()) - (child.getY() + child.getHeight());
        Log.println(Log.ASSERT, "rotateTest", "result child padding = " + result);
        //reset
        resetToInitialAfterRotate();

        //assert
        assertNotEquals(0.0d, bottom, 0.01);
        assertNotEquals(0.0d, result, 0.01);
        assertEquals(bottom, result, 0.01);
    }

    @Test
    public void setAdapterTwice_ChipsLayoutManagerHaveSetToRecyclerView_NoException() throws Exception {
        //arrange
        ChipsFacade chipsFacade = spy(new ChipsFacade());
        ChipsAdapter chipsAdapter = new ChipsAdapter(chipsFacade.getItems(), null);

        //act
        recyclerView.perform(setAdapter(chipsAdapter));
        recyclerView.perform(setAdapter(chipsAdapter));
        InstrumentalUtil.waitForIdle();

        //assert
        int pos = layoutManager.findFirstVisibleItemPosition();
        assertNotEquals(RecyclerView.NO_POSITION, pos);
    }

    ///////////////////////////////////////////////////////////////////////////
    // padding
    ///////////////////////////////////////////////////////////////////////////

    @Test
    public void clipToPadding_IsTrue_paddingStaySame() throws Exception {
        //arrange
        RecyclerView rvTest = (RecyclerView) activityTestRule.getActivity().findViewById(R.id.rvTest);
        ViewAction initAction = actionDelegate((uiController, view) ->  {
            view.setClipToPadding(true);
            view.setPadding(150, 150, 150, 150);
            view.requestLayout();
        });
        recyclerView.perform(initAction);

        //act
        recyclerView.perform(RecyclerViewActions.scrollToPosition(8));

        //assert
        View view = layoutManager.getChildAt(0);
        double padding = view.getY() - rvTest.getY();
        assertTrue(padding >= 150);
    }


    @Test
    public void clipToPadding_IsFalse_paddingOfScrolledViewIsLowerThanInitial() throws Exception {
        //arrange
        ViewAction arrangeAction = actionDelegate((uiController, view) -> {
            view.setClipToPadding(false);
            view.setPadding(150, 150, 150, 150);
            view.requestLayout();
        });
        recyclerView.perform(arrangeAction);

        //act
        recyclerView.perform(RecyclerViewActions.scrollToPosition(8),
                scrollBy(0, 200));

        //assert
        View view = layoutManager.getChildAt(0);
        int padding = layoutManager.getDecoratedTop(view);
        assertTrue(padding < 0);
    }

    private ViewHolderMatcher<RecyclerView.ViewHolder> rvPaddingMatcher() {
        return new ViewHolderMatcher<RecyclerView.ViewHolder>() {
            @Override
            public boolean matches(RecyclerView parent, View itemView, RecyclerView.ViewHolder viewHolder) {
                int expectedPadding = parent.getPaddingBottom();
                int bottom = layoutManager.getDecoratedBottom(itemView);
                int parentBottom = parent.getBottom();
                int padding = parentBottom - bottom;
                assertEquals("padding of RecyclerView item doesn't equal expected padding" ,expectedPadding, padding);
                return true;
            }
        };
    }

    @Test
    public void gapsNormalization_OnLastRowDeleted_PaddingStaySame() throws Exception {
        //arrange
        recyclerView.perform(RecyclerViewActions.scrollToPosition(39));
        //act
        recyclerView.perform(actionDelegate((uiController, recyclerView) -> items.remove(39)),
                notifyItemRemovedAction(39));
        //assert
        recyclerView.check(matches(atPosition(38, rvPaddingMatcher())));
    }

    private View getViewForPosition(RecyclerView recyclerView, int position) {
        return recyclerView.findViewHolderForAdapterPosition(position).itemView;
    }
}