/*
 * Copyright 2018 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.recyclerview.widget;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.os.Build;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
@RunWith(AndroidJUnit4.class)
public class RecyclerViewCacheTest {
    TimeMockingRecyclerView mRecyclerView;
    RecyclerView.Recycler mRecycler;

    private class TimeMockingRecyclerView extends RecyclerView {
        private long mMockNanoTime = 0;

        TimeMockingRecyclerView(Context context) {
            super(context);
        }

        public void registerTimePassingMs(long ms) {
            mMockNanoTime += TimeUnit.MILLISECONDS.toNanos(ms);
        }

        @Override
        long getNanoTime() {
            return mMockNanoTime;
        }

        @Override
        public int getWindowVisibility() {
            // Pretend to be visible to avoid being filtered out
            return View.VISIBLE;
        }
    }

    @Before
    public void setup() throws Exception {
        mRecyclerView = new TimeMockingRecyclerView(getContext());
        mRecyclerView.onAttachedToWindow();
        mRecycler = mRecyclerView.mRecycler;
    }

    @After
    public void teardown() throws Exception {
        if (mRecyclerView.isAttachedToWindow()) {
            mRecyclerView.onDetachedFromWindow();
        }
        GapWorker gapWorker = GapWorker.sGapWorker.get();
        if (gapWorker != null) {
            assertTrue(gapWorker.mRecyclerViews.isEmpty());
        }
    }

    private Context getContext() {
        return InstrumentationRegistry.getContext();
    }

    private void layout(int width, int height) {
        mRecyclerView.measure(
                View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
                View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
        mRecyclerView.layout(0, 0, width, height);
    }

    @Test
    public void prefetchReusesCacheItems() {
        RecyclerView.LayoutManager prefetchingLayoutManager = new RecyclerView.LayoutManager() {
            @Override
            public RecyclerView.LayoutParams generateDefaultLayoutParams() {
                return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT);
            }

            @Override
            public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
                    LayoutPrefetchRegistry prefetchManager) {
                prefetchManager.addPosition(0, 0);
                prefetchManager.addPosition(1, 0);
                prefetchManager.addPosition(2, 0);
            }

            @Override
            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
            }
        };
        mRecyclerView.setLayoutManager(prefetchingLayoutManager);

        RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
        when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
                .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
                    @Override
                    public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
                            throws Throwable {
                        return new RecyclerView.ViewHolder(new View(getContext())) {};
                    }
                });
        when(mockAdapter.getItemCount()).thenReturn(10);
        mRecyclerView.setAdapter(mockAdapter);

        layout(320, 320);

        verify(mockAdapter, never()).onCreateViewHolder(any(ViewGroup.class), anyInt());
        verify(mockAdapter, never()).onBindViewHolder(
                any(RecyclerView.ViewHolder.class), anyInt(), any(List.class));
        assertTrue(mRecycler.mCachedViews.isEmpty());

        // Prefetch multiple times...
        for (int i = 0; i < 4; i++) {
            mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);

            // ...but should only see the same three items fetched/bound once each
            verify(mockAdapter, times(3)).onCreateViewHolder(any(ViewGroup.class), anyInt());
            verify(mockAdapter, times(3)).onBindViewHolder(
                    any(RecyclerView.ViewHolder.class), anyInt(), any(List.class));

            assertTrue(mRecycler.mCachedViews.size() == 3);
            CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 0, 1, 2);
        }
    }

    @Test
    public void prefetchItemsNotEvictedWithInserts() {
        mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3));

        RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
        when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
                .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
                    @Override
                    public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
                            throws Throwable {
                        View view = new View(getContext());
                        view.setMinimumWidth(100);
                        view.setMinimumHeight(100);
                        return new RecyclerView.ViewHolder(view) {};
                    }
                });
        when(mockAdapter.getItemCount()).thenReturn(100);
        mRecyclerView.setAdapter(mockAdapter);

        layout(300, 100);

        assertEquals(2, mRecyclerView.mRecycler.mViewCacheMax);
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
        assertEquals(5, mRecyclerView.mRecycler.mViewCacheMax);

        CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 3, 4, 5);

        // further views recycled, as though from scrolling, shouldn't evict prefetched views:
        mRecycler.recycleView(mRecycler.getViewForPosition(10));
        CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5, 10);

        mRecycler.recycleView(mRecycler.getViewForPosition(20));
        CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5, 10, 20);

        mRecycler.recycleView(mRecycler.getViewForPosition(30));
        CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5, 20, 30);

        mRecycler.recycleView(mRecycler.getViewForPosition(40));
        CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5, 30, 40);

        // After clearing the cache, the prefetch priorities should be cleared as well:
        mRecyclerView.mRecycler.recycleAndClearCachedViews();
        for (int i : new int[] {3, 4, 5, 50, 60, 70, 80, 90}) {
            mRecycler.recycleView(mRecycler.getViewForPosition(i));
        }

        // cache only contains most recent positions, no priority for previous prefetches:
        CacheUtils.verifyCacheContainsPositions(mRecyclerView, 50, 60, 70, 80, 90);
    }

    @Test
    public void prefetchItemsNotEvictedOnScroll() {
        mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3));

        // 100x100 pixel views
        RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
        when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
                .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
                    @Override
                    public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
                            throws Throwable {
                        View view = new View(getContext());
                        view.setMinimumWidth(100);
                        view.setMinimumHeight(100);
                        return new RecyclerView.ViewHolder(view) {};
                    }
                });
        when(mockAdapter.getItemCount()).thenReturn(100);
        mRecyclerView.setAdapter(mockAdapter);

        // NOTE: requested cache size must be smaller than span count so two rows cannot fit
        mRecyclerView.setItemViewCacheSize(2);

        layout(300, 150);
        mRecyclerView.scrollBy(0, 75);
        assertTrue(mRecycler.mCachedViews.isEmpty());

        // rows 0, 1, and 2 are all attached and visible. Prefetch row 3:
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);

        // row 3 is cached:
        CacheUtils.verifyCacheContainsPositions(mRecyclerView, 9, 10, 11);
        assertTrue(mRecycler.mCachedViews.size() == 3);

        // Scroll so 1 falls off (though 3 is still not on screen)
        mRecyclerView.scrollBy(0, 50);

        // row 3 is still cached, with a couple other recycled views:
        CacheUtils.verifyCacheContainsPositions(mRecyclerView, 9, 10, 11);
        assertTrue(mRecycler.mCachedViews.size() == 5);
    }

    @Test
    public void prefetchIsComputingLayout() {
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));

        // 100x100 pixel views
        RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
        when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
                .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
                    @Override
                    public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
                            throws Throwable {
                        View view = new View(getContext());
                        view.setMinimumWidth(100);
                        view.setMinimumHeight(100);
                        assertTrue(mRecyclerView.isComputingLayout());
                        return new RecyclerView.ViewHolder(view) {};
                    }
                });
        when(mockAdapter.getItemCount()).thenReturn(100);
        mRecyclerView.setAdapter(mockAdapter);

        layout(100, 100);

        verify(mockAdapter, times(1)).onCreateViewHolder(mRecyclerView, 0);

        // prefetch an item, should still observe isComputingLayout in that create
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);

        verify(mockAdapter, times(2)).onCreateViewHolder(mRecyclerView, 0);
    }

    @Test
    public void prefetchAfterOrientationChange() {
        LinearLayoutManager layout = new LinearLayoutManager(getContext(),
                LinearLayoutManager.VERTICAL, false);
        mRecyclerView.setLayoutManager(layout);

        // 100x100 pixel views
        mRecyclerView.setAdapter(new RecyclerView.Adapter() {
            @NonNull
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(
                    @NonNull ViewGroup parent, int viewType) {
                View view = new View(getContext());
                view.setMinimumWidth(100);
                view.setMinimumHeight(100);
                assertTrue(mRecyclerView.isComputingLayout());
                return new RecyclerView.ViewHolder(view) {};
            }

            @Override
            public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {}

            @Override
            public int getItemCount() {
                return 100;
            }
        });

        layout(100, 100);

        layout.setOrientation(LinearLayoutManager.HORIZONTAL);

        // Prefetch an item after changing orientation, before layout - shouldn't crash
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(1, 1);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
    }

    @Test
    public void prefetchDrag() {
        // event dispatch requires a parent
        ViewGroup parent = new FrameLayout(getContext());
        parent.addView(mRecyclerView);


        mRecyclerView.setLayoutManager(
                new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));

        // 1000x1000 pixel views
        RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(
                    @NonNull ViewGroup parent, int viewType) {
                mRecyclerView.registerTimePassingMs(5);
                View view = new View(getContext());
                view.setMinimumWidth(1000);
                view.setMinimumHeight(1000);
                return new RecyclerView.ViewHolder(view) {};
            }

            @Override
            public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
                mRecyclerView.registerTimePassingMs(5);
            }

            @Override
            public int getItemCount() {
                return 100;
            }
        };
        mRecyclerView.setAdapter(adapter);

        layout(1000, 1000);

        long time = SystemClock.uptimeMillis();
        mRecyclerView.onTouchEvent(
                MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 500, 1000, 0));

        assertEquals(0, mRecyclerView.mPrefetchRegistry.mPrefetchDx);
        assertEquals(0, mRecyclerView.mPrefetchRegistry.mPrefetchDy);

        // Consume slop
        mRecyclerView.onTouchEvent(
                MotionEvent.obtain(time, time, MotionEvent.ACTION_MOVE, 50, 500, 0));

        // move by 0,30
        mRecyclerView.onTouchEvent(
                MotionEvent.obtain(time, time, MotionEvent.ACTION_MOVE, 50, 470, 0));
        assertEquals(0, mRecyclerView.mPrefetchRegistry.mPrefetchDx);
        assertEquals(30, mRecyclerView.mPrefetchRegistry.mPrefetchDy);

        // move by 10,15
        mRecyclerView.onTouchEvent(
                MotionEvent.obtain(time, time, MotionEvent.ACTION_MOVE, 40, 455, 0));
        assertEquals(10, mRecyclerView.mPrefetchRegistry.mPrefetchDx);
        assertEquals(15, mRecyclerView.mPrefetchRegistry.mPrefetchDy);

        // move by 0,0 - IGNORED
        mRecyclerView.onTouchEvent(
                MotionEvent.obtain(time, time, MotionEvent.ACTION_MOVE, 40, 455, 0));
        assertEquals(10, mRecyclerView.mPrefetchRegistry.mPrefetchDx); // same as prev
        assertEquals(15, mRecyclerView.mPrefetchRegistry.mPrefetchDy); // same as prev
    }

    @Test
    public void prefetchItemsRespectDeadline() {
        mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3));

        // 100x100 pixel views
        RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(
                    @NonNull ViewGroup parent, int viewType) {
                mRecyclerView.registerTimePassingMs(5);
                View view = new View(getContext());
                view.setMinimumWidth(100);
                view.setMinimumHeight(100);
                return new RecyclerView.ViewHolder(view) {};
            }

            @Override
            public void onBindViewHolder(
                    @NonNull RecyclerView.ViewHolder holder, int position) {
                mRecyclerView.registerTimePassingMs(5);
            }

            @Override
            public int getItemCount() {
                return 100;
            }
        };
        mRecyclerView.setAdapter(adapter);

        layout(300, 300);

        // offset scroll so that no prefetch-able views are directly adjacent to viewport
        mRecyclerView.scrollBy(0, 50);

        assertTrue(mRecycler.mCachedViews.size() == 0);
        assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 0);

        // Should take 15 ms to inflate, bind, inflate, so give 19 to be safe
        final long deadlineNs = mRecyclerView.getNanoTime() + TimeUnit.MILLISECONDS.toNanos(19);

        // Timed prefetch
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
        mRecyclerView.mGapWorker.prefetch(deadlineNs);

        // will have enough time to inflate/bind one view, and inflate another
        assertTrue(mRecycler.mCachedViews.size() == 1);
        assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 1);
        // Note: order/view below is an implementation detail
        CacheUtils.verifyCacheContainsPositions(mRecyclerView, 12);


        // Unbounded prefetch this time
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);

        // Should finish all work
        assertTrue(mRecycler.mCachedViews.size() == 3);
        assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 0);
        CacheUtils.verifyCacheContainsPositions(mRecyclerView, 12, 13, 14);
    }

    @Test
    public void partialPrefetchAvoidsViewRecycledCallback() {
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));

        // 100x100 pixel views
        RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(
                    @NonNull ViewGroup parent, int viewType) {
                mRecyclerView.registerTimePassingMs(5);
                View view = new View(getContext());
                view.setMinimumWidth(100);
                view.setMinimumHeight(100);
                return new RecyclerView.ViewHolder(view) {};
            }

            @Override
            public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
                mRecyclerView.registerTimePassingMs(5);
            }

            @Override
            public int getItemCount() {
                return 100;
            }

            @Override
            public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
                // verify unbound view doesn't get
                assertNotEquals(RecyclerView.NO_POSITION, holder.getAdapterPosition());
            }
        };
        mRecyclerView.setAdapter(adapter);

        layout(100, 300);

        // offset scroll so that no prefetch-able views are directly adjacent to viewport
        mRecyclerView.scrollBy(0, 50);

        assertTrue(mRecycler.mCachedViews.size() == 0);
        assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 0);

        // Should take 10 ms to inflate + bind, so just give it 9 so it doesn't have time to bind
        final long deadlineNs = mRecyclerView.getNanoTime() + TimeUnit.MILLISECONDS.toNanos(9);

        // Timed prefetch
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
        mRecyclerView.mGapWorker.prefetch(deadlineNs);

        // will have enough time to inflate but not bind one view
        assertTrue(mRecycler.mCachedViews.size() == 0);
        assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 1);
        RecyclerView.ViewHolder pooledHolder = mRecyclerView.getRecycledViewPool()
                .mScrap.get(0).mScrapHeap.get(0);
        assertEquals(RecyclerView.NO_POSITION, pooledHolder.getAdapterPosition());
    }

    @Test
    public void prefetchStaggeredItemsPriority() {
        StaggeredGridLayoutManager sglm =
                new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
        mRecyclerView.setLayoutManager(sglm);

        // first view 50x100 pixels, rest are 100x100 so second column is offset
        mRecyclerView.setAdapter(new RecyclerView.Adapter() {
            @NonNull
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(
                    @NonNull ViewGroup parent, int viewType) {
                return new RecyclerView.ViewHolder(new View(getContext())) {};
            }

            @Override
            public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
                holder.itemView.setMinimumWidth(100);
                holder.itemView.setMinimumHeight(position == 0 ? 50 : 100);
            }

            @Override
            public int getItemCount() {
                return 100;
            }
        });

        layout(200, 200);

        /* Each row is 50 pixels:
         * ------------- *
         *   0   |   1   *
         *   2   |   1   *
         *   2   |   3   *
         *___4___|___3___*
         *   4   |   5   *
         *   6   |   5   *
         *      ...      *
         */
        assertEquals(5, mRecyclerView.getChildCount());
        assertEquals(0, sglm.getFirstChildPosition());
        assertEquals(4, sglm.getLastChildPosition());

        // prefetching down shows 5 at 0 pixels away, 6 at 50 pixels away
        CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, 10,
                new Integer[] {5, 0}, new Integer[] {6, 50});

        // Prefetch upward shows nothing
        CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, -10);

        mRecyclerView.scrollBy(0, 100);

        /* Each row is 50 pixels:
         * ------------- *
         *   0   |   1   *
         *___2___|___1___*
         *   2   |   3   *
         *   4   |   3   *
         *   4   |   5   *
         *___6___|___5___*
         *   6   |   7   *
         *   8   |   7   *
         *      ...      *
         */

        assertEquals(5, mRecyclerView.getChildCount());
        assertEquals(2, sglm.getFirstChildPosition());
        assertEquals(6, sglm.getLastChildPosition());

        // prefetching down shows 7 at 0 pixels away, 8 at 50 pixels away
        CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, 10,
                new Integer[] {7, 0}, new Integer[] {8, 50});

        // prefetching up shows 1 is 0 pixels away, 0 at 50 pixels away
        CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, -10,
                new Integer[] {1, 0}, new Integer[] {0, 50});
    }

    @Test
    public void prefetchStaggeredPastBoundary() {
        StaggeredGridLayoutManager sglm =
                new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
        mRecyclerView.setLayoutManager(sglm);
        mRecyclerView.setAdapter(new RecyclerView.Adapter() {
            @NonNull
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(
                    @NonNull ViewGroup parent, int viewType) {
                return new RecyclerView.ViewHolder(new View(getContext())) {};
            }

            @Override
            public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
                holder.itemView.setMinimumWidth(100);
                holder.itemView.setMinimumHeight(position == 0 ? 100 : 200);
            }

            @Override
            public int getItemCount() {
                return 2;
            }
        });

        layout(200, 100);
        mRecyclerView.scrollBy(0, 50);

        /* Each row is 50 pixels:
         * ------------- *
         *___0___|___1___*
         *   0   |   1   *
         *_______|___1___*
         *       |   1   *
         */
        assertEquals(2, mRecyclerView.getChildCount());
        assertEquals(0, sglm.getFirstChildPosition());
        assertEquals(1, sglm.getLastChildPosition());

        // prefetch upward gets nothing
        CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, -10);

        // prefetch downward gets nothing (and doesn't crash...)
        CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, 10);
    }

    @Test
    public void prefetchItemsSkipAnimations() {
        LinearLayoutManager llm = new LinearLayoutManager(getContext());
        mRecyclerView.setLayoutManager(llm);
        final int[] expandedPosition = new int[] {-1};

        final RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(
                    @NonNull ViewGroup parent, int viewType) {
                return new RecyclerView.ViewHolder(new View(parent.getContext())) {};
            }

            @Override
            public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
                int height = expandedPosition[0] == position ? 400 : 100;
                holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(200, height));
            }

            @Override
            public int getItemCount() {
                return 10;
            }
        };

        // make move duration long enough to be able to see the effects
        RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
        itemAnimator.setMoveDuration(10000);
        mRecyclerView.setAdapter(adapter);

        layout(200, 400);

        expandedPosition[0] = 1;
        // insert payload to avoid creating a new view
        adapter.notifyItemChanged(1, new Object());

        layout(200, 400);
        layout(200, 400);

        assertTrue(itemAnimator.isRunning());
        assertEquals(2, llm.getChildCount());
        assertEquals(4, mRecyclerView.getChildCount());

        // animating view should be observable as hidden, uncached...
        CacheUtils.verifyCacheDoesNotContainPositions(mRecyclerView, 2);
        assertNotNull("Animating view should be found, hidden",
                mRecyclerView.mChildHelper.findHiddenNonRemovedView(2));
        assertTrue(GapWorker.isPrefetchPositionAttached(mRecyclerView, 2));

        // ...but must not be removed for prefetch
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);

        assertEquals("Prefetch must target one view", 1, mRecyclerView.mPrefetchRegistry.mCount);
        int prefetchTarget = mRecyclerView.mPrefetchRegistry.mPrefetchArray[0];
        assertEquals("Prefetch must target view 2", 2, prefetchTarget);

        // animating view still observable as hidden, uncached
        CacheUtils.verifyCacheDoesNotContainPositions(mRecyclerView, 2);
        assertNotNull("Animating view should be found, hidden",
                mRecyclerView.mChildHelper.findHiddenNonRemovedView(2));
        assertTrue(GapWorker.isPrefetchPositionAttached(mRecyclerView, 2));

        assertTrue(itemAnimator.isRunning());
        assertEquals(2, llm.getChildCount());
        assertEquals(4, mRecyclerView.getChildCount());
    }

    @Test
    public void viewHolderFindsNestedRecyclerViews() {
        LinearLayoutManager llm = new LinearLayoutManager(getContext());
        mRecyclerView.setLayoutManager(llm);

        RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
        when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
                .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
                    @Override
                    public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
                            throws Throwable {
                        View view = new RecyclerView(getContext());
                        view.setLayoutParams(new RecyclerView.LayoutParams(100, 100));
                        return new RecyclerView.ViewHolder(view) {};
                    }
                });
        when(mockAdapter.getItemCount()).thenReturn(100);
        mRecyclerView.setAdapter(mockAdapter);

        layout(100, 200);

        verify(mockAdapter, times(2)).onCreateViewHolder(any(ViewGroup.class), anyInt());
        verify(mockAdapter, times(2)).onBindViewHolder(
                argThat(new ArgumentMatcher<RecyclerView.ViewHolder>() {
                    @Override
                    public boolean matches(RecyclerView.ViewHolder holder) {
                        return holder.itemView == holder.mNestedRecyclerView.get();
                    }
                }),
                anyInt(),
                any(List.class));
    }

    class InnerAdapter extends RecyclerView.Adapter<InnerAdapter.ViewHolder> {
        private static final int INNER_ITEM_COUNT = 20;
        int mItemsBound = 0;

        class ViewHolder extends RecyclerView.ViewHolder {
            ViewHolder(View itemView) {
                super(itemView);
            }
        }

        InnerAdapter() {}

        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            mRecyclerView.registerTimePassingMs(5);
            View view = new View(parent.getContext());
            view.setLayoutParams(new RecyclerView.LayoutParams(100, 100));
            return new ViewHolder(view);
        }

        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            mRecyclerView.registerTimePassingMs(5);
            mItemsBound++;
        }

        @Override
        public int getItemCount() {
            return INNER_ITEM_COUNT;
        }
    }

    class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {

        private boolean mReverseInner;

        class ViewHolder extends RecyclerView.ViewHolder {
            private final RecyclerView mRecyclerView;
            ViewHolder(RecyclerView itemView) {
                super(itemView);
                mRecyclerView = itemView;
            }
        }

        ArrayList<InnerAdapter> mAdapters = new ArrayList<>();
        ArrayList<Parcelable> mSavedStates = new ArrayList<>();
        RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();

        OuterAdapter() {
            this(false);
        }

        OuterAdapter(boolean reverseInner) {
            this(reverseInner, 10);
        }

        OuterAdapter(boolean reverseInner, int itemCount) {
            mReverseInner = reverseInner;
            for (int i = 0; i < itemCount; i++) {
                mAdapters.add(new InnerAdapter());
                mSavedStates.add(null);
            }
        }

        void addItem() {
            int index = getItemCount();
            mAdapters.add(new InnerAdapter());
            mSavedStates.add(null);
            notifyItemInserted(index);
        }

        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            mRecyclerView.registerTimePassingMs(5);

            RecyclerView rv = new RecyclerView(parent.getContext()) {
                @Override
                public int getWindowVisibility() {
                    // Pretend to be visible to avoid being filtered out
                    return View.VISIBLE;
                }
            };
            rv.setLayoutManager(new LinearLayoutManager(parent.getContext(),
                    LinearLayoutManager.HORIZONTAL, mReverseInner));
            rv.setRecycledViewPool(mSharedPool);
            rv.setLayoutParams(new RecyclerView.LayoutParams(200, 100));
            return new ViewHolder(rv);
        }

        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            mRecyclerView.registerTimePassingMs(5);

            // Tests may rely on bound holders not being shared between inner adapters,
            // since we force recycle here
            holder.mRecyclerView.swapAdapter(mAdapters.get(position), true);

            Parcelable savedState = mSavedStates.get(position);
            if (savedState != null) {
                holder.mRecyclerView.getLayoutManager().onRestoreInstanceState(savedState);
                mSavedStates.set(position, null);
            }
        }

        @Override
        public void onViewRecycled(@NonNull ViewHolder holder) {
            mSavedStates.set(holder.getAdapterPosition(),
                    holder.mRecyclerView.getLayoutManager().onSaveInstanceState());
        }

        @Override
        public int getItemCount() {
            return mAdapters.size();
        }
    }

    @Test
    public void nestedPrefetchSimple() {
        LinearLayoutManager llm = new LinearLayoutManager(getContext());
        assertEquals(2, llm.getInitialPrefetchItemCount());

        mRecyclerView.setLayoutManager(llm);
        mRecyclerView.setAdapter(new OuterAdapter());

        layout(200, 200);
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);

        // prefetch 2 (default)
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
        RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
        assertNotNull(holder);
        assertNotNull(holder.mNestedRecyclerView);
        RecyclerView innerView = holder.mNestedRecyclerView.get();
        CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);

        // prefetch 4
        ((LinearLayoutManager) innerView.getLayoutManager())
                .setInitialPrefetchItemCount(4);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
        CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1, 2, 3);
    }

    @Test
    public void nestedPrefetchNotClearInnerStructureChangeFlag() {
        LinearLayoutManager llm = new LinearLayoutManager(getContext());
        assertEquals(2, llm.getInitialPrefetchItemCount());

        mRecyclerView.setLayoutManager(llm);
        mRecyclerView.setAdapter(new OuterAdapter());

        layout(200, 200);
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);

        // prefetch 2 (default)
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
        RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
        assertNotNull(holder);
        assertNotNull(holder.mNestedRecyclerView);
        RecyclerView innerView = holder.mNestedRecyclerView.get();
        RecyclerView.Adapter innerAdapter = innerView.getAdapter();
        CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);
        // mStructureChanged is initially true before first layout pass.
        assertTrue(innerView.mState.mStructureChanged);
        assertTrue(innerView.hasPendingAdapterUpdates());

        // layout position 2 and clear mStructureChanged
        mRecyclerView.scrollToPosition(2);
        layout(200, 200);
        mRecyclerView.scrollToPosition(0);
        layout(200, 200);
        assertFalse(innerView.mState.mStructureChanged);
        assertFalse(innerView.hasPendingAdapterUpdates());

        // notify change on the cached innerView.
        innerAdapter.notifyDataSetChanged();
        assertTrue(innerView.mState.mStructureChanged);
        assertTrue(innerView.hasPendingAdapterUpdates());

        // prefetch again
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
        ((LinearLayoutManager) innerView.getLayoutManager())
                .setInitialPrefetchItemCount(2);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
        CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);

        // The re-prefetch is not necessary get the same inner view but we will get same Adapter
        holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
        innerView = holder.mNestedRecyclerView.get();
        assertSame(innerAdapter, innerView.getAdapter());
        // prefetch shouldn't clear the mStructureChanged flag
        assertTrue(innerView.mState.mStructureChanged);
        assertTrue(innerView.hasPendingAdapterUpdates());
    }

    @Test
    public void nestedPrefetchReverseInner() {
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        mRecyclerView.setAdapter(new OuterAdapter(/* reverseInner = */ true));

        layout(200, 200);
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);

        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
        RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);

        // anchor from right side, should see last two positions
        CacheUtils.verifyCacheContainsPrefetchedPositions(holder.mNestedRecyclerView.get(), 18, 19);
    }

    @Test
    public void nestedPrefetchOffset() {
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        mRecyclerView.setAdapter(new OuterAdapter());

        layout(200, 200);

        // Scroll top row by 5.5 items, verify positions 5, 6, 7 showing
        RecyclerView inner = (RecyclerView) mRecyclerView.getChildAt(0);
        inner.scrollBy(550, 0);
        assertEquals(5, RecyclerView.getChildViewHolderInt(inner.getChildAt(0)).mPosition);
        assertEquals(6, RecyclerView.getChildViewHolderInt(inner.getChildAt(1)).mPosition);
        assertEquals(7, RecyclerView.getChildViewHolderInt(inner.getChildAt(2)).mPosition);

        // scroll down 4 rows, up 3 so row 0 is adjacent but uncached
        mRecyclerView.scrollBy(0, 400);
        mRecyclerView.scrollBy(0, -300);

        // top row no longer present
        CacheUtils.verifyCacheDoesNotContainPositions(mRecyclerView, 0);

        // prefetch upward, and validate that we've gotten the top row with correct offsets
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, -1);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
        inner = (RecyclerView) CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 0).itemView;
        CacheUtils.verifyCacheContainsPrefetchedPositions(inner, 5, 6);

        // prefetch 4
        ((LinearLayoutManager) inner.getLayoutManager()).setInitialPrefetchItemCount(4);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
        CacheUtils.verifyCacheContainsPrefetchedPositions(inner, 5, 6, 7, 8);
    }

    @Test
    public void nestedPrefetchNotReset() {
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        OuterAdapter outerAdapter = new OuterAdapter();
        mRecyclerView.setAdapter(outerAdapter);

        layout(200, 200);

        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);

        // prefetch row 2, items 0 & 1
        assertEquals(0, outerAdapter.mAdapters.get(2).mItemsBound);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
        RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
        RecyclerView innerRecyclerView = holder.mNestedRecyclerView.get();

        assertNotNull(innerRecyclerView);
        CacheUtils.verifyCacheContainsPrefetchedPositions(innerRecyclerView, 0, 1);
        assertEquals(2, outerAdapter.mAdapters.get(2).mItemsBound);

        // new row comes on, triggers layout...
        mRecyclerView.scrollBy(0, 50);

        // ... which shouldn't require new items to be bound,
        // as prefetch has already done that work
        assertEquals(2, outerAdapter.mAdapters.get(2).mItemsBound);
    }

    static void validateRvChildrenValid(RecyclerView recyclerView, int childCount) {
        ChildHelper childHelper = recyclerView.mChildHelper;

        assertEquals(childCount, childHelper.getUnfilteredChildCount());
        for (int i = 0; i < childHelper.getUnfilteredChildCount(); i++) {
            assertFalse(recyclerView.getChildViewHolder(
                    childHelper.getUnfilteredChildAt(i)).isInvalid());
        }
    }

    @Test
    public void nestedPrefetchCacheNotTouched() {
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        OuterAdapter outerAdapter = new OuterAdapter();
        mRecyclerView.setAdapter(outerAdapter);

        layout(200, 200);
        mRecyclerView.scrollBy(0, 100);

        // item 0 is cached
        assertEquals(2, outerAdapter.mAdapters.get(0).mItemsBound);
        RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 0);
        validateRvChildrenValid(holder.mNestedRecyclerView.get(), 2);

        // try and prefetch it
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, -1);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);

        // make sure cache's inner items aren't rebound unnecessarily
        assertEquals(2, outerAdapter.mAdapters.get(0).mItemsBound);
        validateRvChildrenValid(holder.mNestedRecyclerView.get(), 2);
    }

    @Test
    public void nestedRemoveAnimatingView() {
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        OuterAdapter outerAdapter = new OuterAdapter(false, 1);
        mRecyclerView.setAdapter(outerAdapter);
        mRecyclerView.getItemAnimator().setAddDuration(TimeUnit.MILLISECONDS.toNanos(30));

        layout(200, 200);

        // Insert 3 items - only first one in viewport, so only it animates
        for (int i = 0; i < 3; i++) {
            outerAdapter.addItem();
        }
        layout(200, 200); // layout again to kick off animation


        // item 1 is animating, so scroll it out of viewport
        mRecyclerView.scrollBy(0, 200);

        // 2 items attached, 1 cached (pos 0), but item animating pos 1 not accounted for...
        assertEquals(2, mRecyclerView.mChildHelper.getUnfilteredChildCount());
        assertEquals(1, mRecycler.mCachedViews.size());
        CacheUtils.verifyCacheContainsPositions(mRecyclerView, 0);
        assertEquals(0, mRecyclerView.getRecycledViewPool().getRecycledViewCount(0));

        // until animation ends
        mRecyclerView.getItemAnimator().endAnimations();
        assertEquals(2, mRecyclerView.mChildHelper.getUnfilteredChildCount());
        assertEquals(2, mRecycler.mCachedViews.size());
        CacheUtils.verifyCacheContainsPositions(mRecyclerView, 0, 1);
        assertEquals(0, mRecyclerView.getRecycledViewPool().getRecycledViewCount(0));

        for (RecyclerView.ViewHolder viewHolder : mRecycler.mCachedViews) {
            assertNotNull(viewHolder.mNestedRecyclerView);
        }
    }

    @Test
    public void nestedExpandCacheCorrectly() {
        final int DEFAULT_CACHE_SIZE = RecyclerView.Recycler.DEFAULT_CACHE_SIZE;

        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        OuterAdapter outerAdapter = new OuterAdapter();
        mRecyclerView.setAdapter(outerAdapter);

        layout(200, 200);

        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);

        // after initial prefetch, view cache max expanded by number of inner items prefetched (2)
        RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
        RecyclerView innerView = holder.mNestedRecyclerView.get();
        assertTrue(innerView.getLayoutManager().mPrefetchMaxObservedInInitialPrefetch);
        assertEquals(2, innerView.getLayoutManager().mPrefetchMaxCountObserved);
        assertEquals(2 + DEFAULT_CACHE_SIZE, innerView.mRecycler.mViewCacheMax);

        try {
            // Note: As a hack, we not only must manually dispatch attachToWindow(), but we
            // also have to be careful to call innerView.mGapWorker below. mRecyclerView.mGapWorker
            // is registered to the wrong thread, since @setup is called on a different thread
            // from @Test. Assert this, so this test can be fixed when setup == test thread.
            assertEquals(1, mRecyclerView.mGapWorker.mRecyclerViews.size());
            assertFalse(innerView.isAttachedToWindow());
            innerView.onAttachedToWindow();

            // bring prefetch view into viewport, at which point it shouldn't have cache expanded...
            mRecyclerView.scrollBy(0, 100);
            assertFalse(innerView.getLayoutManager().mPrefetchMaxObservedInInitialPrefetch);
            assertEquals(0, innerView.getLayoutManager().mPrefetchMaxCountObserved);
            assertEquals(DEFAULT_CACHE_SIZE, innerView.mRecycler.mViewCacheMax);

            // until a valid horizontal prefetch caches an item, and expands view count by one
            innerView.mPrefetchRegistry.setPrefetchVector(1, 0);
            innerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS); // NB: must be innerView.mGapWorker
            assertFalse(innerView.getLayoutManager().mPrefetchMaxObservedInInitialPrefetch);
            assertEquals(1, innerView.getLayoutManager().mPrefetchMaxCountObserved);
            assertEquals(1 + DEFAULT_CACHE_SIZE, innerView.mRecycler.mViewCacheMax);
        } finally {
            if (innerView.isAttachedToWindow()) {
                innerView.onDetachedFromWindow();
            }
        }
    }

    /**
     * Similar to OuterAdapter above, but uses notifyDataSetChanged() instead of set/swapAdapter
     * to update data for the inner RecyclerViews when containing ViewHolder is bound.
     */
    class OuterNotifyAdapter extends RecyclerView.Adapter<OuterNotifyAdapter.ViewHolder> {
        private static final int OUTER_ITEM_COUNT = 10;

        private boolean mReverseInner;

        class ViewHolder extends RecyclerView.ViewHolder {
            private final RecyclerView mRecyclerView;
            private final InnerAdapter mAdapter;
            ViewHolder(RecyclerView itemView) {
                super(itemView);
                mRecyclerView = itemView;
                mAdapter = new InnerAdapter();
                mRecyclerView.setAdapter(mAdapter);
            }
        }

        ArrayList<Parcelable> mSavedStates = new ArrayList<>();
        RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();

        OuterNotifyAdapter() {
            this(false);
        }

        OuterNotifyAdapter(boolean reverseInner) {
            mReverseInner = reverseInner;
            for (int i = 0; i <= OUTER_ITEM_COUNT; i++) {
                mSavedStates.add(null);
            }
        }

        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            mRecyclerView.registerTimePassingMs(5);
            RecyclerView rv = new RecyclerView(parent.getContext());
            rv.setLayoutManager(new LinearLayoutManager(parent.getContext(),
                    LinearLayoutManager.HORIZONTAL, mReverseInner));
            rv.setRecycledViewPool(mSharedPool);
            rv.setLayoutParams(new RecyclerView.LayoutParams(200, 100));
            return new ViewHolder(rv);
        }

        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            mRecyclerView.registerTimePassingMs(5);
            // if we had actual data to put into our adapter, this is where we'd do it...

            // ... then notify the adapter that it has new content:
            holder.mAdapter.notifyDataSetChanged();

            Parcelable savedState = mSavedStates.get(position);
            if (savedState != null) {
                holder.mRecyclerView.getLayoutManager().onRestoreInstanceState(savedState);
                mSavedStates.set(position, null);
            }
        }

        @Override
        public void onViewRecycled(@NonNull ViewHolder holder) {
            if (holder.getAdapterPosition() >= 0) {
                mSavedStates.set(holder.getAdapterPosition(),
                        holder.mRecyclerView.getLayoutManager().onSaveInstanceState());
            }
        }

        @Override
        public int getItemCount() {
            return OUTER_ITEM_COUNT;
        }
    }

    @Test
    public void nestedPrefetchDiscardStaleChildren() {
        LinearLayoutManager llm = new LinearLayoutManager(getContext());
        assertEquals(2, llm.getInitialPrefetchItemCount());

        mRecyclerView.setLayoutManager(llm);
        OuterNotifyAdapter outerAdapter = new OuterNotifyAdapter();
        mRecyclerView.setAdapter(outerAdapter);

        // zero cache, so item we prefetch can't already be ready
        mRecyclerView.setItemViewCacheSize(0);

        // layout 3 items, then resize to 2...
        layout(200, 300);
        layout(200, 200);

        // so 1 item is evicted into the RecycledViewPool (bypassing cache)
        assertEquals(1, mRecycler.mRecyclerPool.getRecycledViewCount(0));
        assertEquals(0, mRecycler.mCachedViews.size());

        // This is a simple imitation of other behavior (namely, varied types in the outer adapter)
        // that results in the same initial state to test: items in the pool with attached children
        for (RecyclerView.ViewHolder holder : mRecycler.mRecyclerPool.mScrap.get(0).mScrapHeap) {
            // verify that children are attached and valid, since the RVs haven't been rebound
            assertNotNull(holder.mNestedRecyclerView);
            assertFalse(holder.mNestedRecyclerView.get().mDataSetHasChangedAfterLayout);
            validateRvChildrenValid(holder.mNestedRecyclerView.get(), 2);
        }

        // prefetch the outer item bind, but without enough time to do any inner binds
        final long deadlineNs = mRecyclerView.getNanoTime() + TimeUnit.MILLISECONDS.toNanos(9);
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
        mRecyclerView.mGapWorker.prefetch(deadlineNs);

        // 2 is prefetched without children
        CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 2);
        RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
        assertNotNull(holder);
        assertNotNull(holder.mNestedRecyclerView);
        assertEquals(0, holder.mNestedRecyclerView.get().mChildHelper.getUnfilteredChildCount());
        assertEquals(0, holder.mNestedRecyclerView.get().mRecycler.mCachedViews.size());

        // but if we give it more time to bind items, it'll now acquire its inner items
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
        CacheUtils.verifyCacheContainsPrefetchedPositions(holder.mNestedRecyclerView.get(), 0, 1);
    }


    @Test
    public void nestedPrefetchDiscardStalePrefetch() {
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        OuterNotifyAdapter outerAdapter = new OuterNotifyAdapter();
        mRecyclerView.setAdapter(outerAdapter);

        // zero cache, so item we prefetch can't already be ready
        mRecyclerView.setItemViewCacheSize(0);

        // layout as 2x2, starting on row index 2, with empty cache
        layout(200, 200);
        mRecyclerView.scrollBy(0, 200);

        // no views cached, or previously used (so we can trust number in mItemsBound)
        mRecycler.mRecyclerPool.clear();
        assertEquals(0, mRecycler.mRecyclerPool.getRecycledViewCount(0));
        assertEquals(0, mRecycler.mCachedViews.size());

        // prefetch the outer item and its inner children
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);

        // 4 is prefetched with 2 inner children, first two binds
        CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 4);
        RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 4);
        assertNotNull(holder);
        assertNotNull(holder.mNestedRecyclerView);
        RecyclerView innerRecyclerView = holder.mNestedRecyclerView.get();
        assertEquals(0, innerRecyclerView.mChildHelper.getUnfilteredChildCount());
        assertEquals(2, innerRecyclerView.mRecycler.mCachedViews.size());
        assertEquals(2, ((InnerAdapter) innerRecyclerView.getAdapter()).mItemsBound);

        // notify data set changed, so any previously prefetched items invalid, and re-prefetch
        innerRecyclerView.getAdapter().notifyDataSetChanged();
        mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
        mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);

        // 4 is prefetched again...
        CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 4);

        // reusing the same instance with 2 inner children...
        assertSame(holder, CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 4));
        assertSame(innerRecyclerView, holder.mNestedRecyclerView.get());
        assertEquals(0, innerRecyclerView.mChildHelper.getUnfilteredChildCount());
        assertEquals(2, innerRecyclerView.mRecycler.mCachedViews.size());

        // ... but there should be two new binds
        assertEquals(4, ((InnerAdapter) innerRecyclerView.getAdapter()).mItemsBound);
    }

    @Test
    public void setRecycledViewPool_followedByTwoSetAdapters_clearsRecycledViewPool() {
        RecyclerView.ViewHolder viewHolder = new RecyclerView.ViewHolder(new View(getContext())) {};
        viewHolder.mItemViewType = 123;
        RecyclerView.Adapter adapter = mock(RecyclerView.Adapter.class);
        RecyclerView recyclerView = new RecyclerView(getContext());
        RecyclerView.RecycledViewPool recycledViewPool = new RecyclerView.RecycledViewPool();
        recycledViewPool.putRecycledView(viewHolder);

        recyclerView.setRecycledViewPool(recycledViewPool);
        recyclerView.setAdapter(adapter);
        recyclerView.setAdapter(adapter);

        assertThat(recycledViewPool.getRecycledViewCount(123), is(equalTo(0)));
    }

    @Test
    public void setRecycledViewPool_followedByTwoSwapAdapters_doesntClearRecycledViewPool() {
        RecyclerView.ViewHolder viewHolder = new RecyclerView.ViewHolder(new View(getContext())) {};
        viewHolder.mItemViewType = 123;
        RecyclerView.Adapter adapter = mock(RecyclerView.Adapter.class);
        RecyclerView recyclerView = new RecyclerView(getContext());
        RecyclerView.RecycledViewPool recycledViewPool = new RecyclerView.RecycledViewPool();
        recycledViewPool.putRecycledView(viewHolder);

        recyclerView.setRecycledViewPool(recycledViewPool);
        recyclerView.swapAdapter(adapter, false);
        recyclerView.swapAdapter(adapter, false);

        assertThat(recycledViewPool.getRecycledViewCount(123), is(equalTo(1)));
    }
}
