/*
 * 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.coordinatorlayout.widget;

import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.swipeUp;
import static android.support.test.espresso.matcher.ViewMatchers.withId;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.app.Instrumentation;
import android.content.Context;
import android.graphics.Rect;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.MediumTest;
import android.support.test.filters.SdkSuppress;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.widget.ImageView;

import androidx.annotation.NonNull;
import androidx.coordinatorlayout.test.R;
import androidx.coordinatorlayout.testutils.CoordinatorLayoutUtils;
import androidx.coordinatorlayout.testutils.CoordinatorLayoutUtils.DependentBehavior;
import androidx.core.view.GravityCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

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

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@MediumTest
@RunWith(AndroidJUnit4.class)
public class CoordinatorLayoutTest {
    @Rule
    public final ActivityTestRule<CoordinatorLayoutActivity> mActivityTestRule;

    private Instrumentation mInstrumentation;

    public CoordinatorLayoutTest() {
        mActivityTestRule = new ActivityTestRule<>(CoordinatorLayoutActivity.class);
    }

    @Before
    public void setup() {
        mInstrumentation = getInstrumentation();
    }

    @Test
    @SdkSuppress(minSdkVersion = 21)
    public void testSetFitSystemWindows() throws Throwable {
        final Instrumentation instrumentation = getInstrumentation();
        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
        final View view = new View(col.getContext());

        // Create a mock which calls the default impl of onApplyWindowInsets()
        final CoordinatorLayout.Behavior<View> mockBehavior =
                mock(CoordinatorLayout.Behavior.class);
        doCallRealMethod().when(mockBehavior)
                .onApplyWindowInsets(same(col), same(view), any(WindowInsetsCompat.class));

        // Assert that the CoL is currently not set to fitSystemWindows
        assertFalse(col.getFitsSystemWindows());

        // Now add a view with our mocked behavior to the CoordinatorLayout
        view.setFitsSystemWindows(true);
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                final CoordinatorLayout.LayoutParams lp = col.generateDefaultLayoutParams();
                lp.setBehavior(mockBehavior);
                col.addView(view, lp);
            }
        });
        instrumentation.waitForIdleSync();

        // Now request some insets and wait for the pass to happen
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                col.requestApplyInsets();
            }
        });
        instrumentation.waitForIdleSync();

        // Verify that onApplyWindowInsets() has not been called
        verify(mockBehavior, never())
                .onApplyWindowInsets(same(col), same(view), any(WindowInsetsCompat.class));

        // Now enable fits system windows and wait for a pass to happen
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                col.setFitsSystemWindows(true);
            }
        });
        instrumentation.waitForIdleSync();

        // Verify that onApplyWindowInsets() has been called with some insets
        verify(mockBehavior, atLeastOnce())
                .onApplyWindowInsets(same(col), same(view), any(WindowInsetsCompat.class));
    }

    @Test
    public void testLayoutChildren() throws Throwable {
        final Instrumentation instrumentation = getInstrumentation();
        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
        final View view = new View(col.getContext());
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                col.addView(view, 100, 100);
            }
        });
        instrumentation.waitForIdleSync();
        int horizontallyCentered = (col.getWidth() - view.getWidth()) / 2;
        int end = col.getWidth() - view.getWidth();
        int verticallyCentered = (col.getHeight() - view.getHeight()) / 2;
        int bottom = col.getHeight() - view.getHeight();
        final int[][] testCases = {
                // gravity, expected left, expected top
                {Gravity.NO_GRAVITY, 0, 0},
                {Gravity.LEFT, 0, 0},
                {GravityCompat.START, 0, 0},
                {Gravity.TOP, 0, 0},
                {Gravity.CENTER, horizontallyCentered, verticallyCentered},
                {Gravity.CENTER_HORIZONTAL, horizontallyCentered, 0},
                {Gravity.CENTER_VERTICAL, 0, verticallyCentered},
                {Gravity.RIGHT, end, 0},
                {GravityCompat.END, end, 0},
                {Gravity.BOTTOM, 0, bottom},
                {Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, horizontallyCentered, bottom},
                {Gravity.RIGHT | Gravity.CENTER_VERTICAL, end, verticallyCentered},
        };
        for (final int[] testCase : testCases) {
            mActivityTestRule.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    final CoordinatorLayout.LayoutParams lp =
                            (CoordinatorLayout.LayoutParams) view.getLayoutParams();
                    lp.gravity = testCase[0];
                    view.setLayoutParams(lp);
                }
            });
            instrumentation.waitForIdleSync();
            mActivityTestRule.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    assertThat("Gravity: " + testCase[0], view.getLeft(), is(testCase[1]));
                    assertThat("Gravity: " + testCase[0], view.getTop(), is(testCase[2]));
                }
            });
        }
    }

    @Test
    public void testInsetDependency() {
        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;

        final CoordinatorLayout.LayoutParams lpInsetLeft = col.generateDefaultLayoutParams();
        lpInsetLeft.insetEdge = Gravity.LEFT;

        final CoordinatorLayout.LayoutParams lpInsetRight = col.generateDefaultLayoutParams();
        lpInsetRight.insetEdge = Gravity.RIGHT;

        final CoordinatorLayout.LayoutParams lpInsetTop = col.generateDefaultLayoutParams();
        lpInsetTop.insetEdge = Gravity.TOP;

        final CoordinatorLayout.LayoutParams lpInsetBottom = col.generateDefaultLayoutParams();
        lpInsetBottom.insetEdge = Gravity.BOTTOM;

        final CoordinatorLayout.LayoutParams lpDodgeLeft = col.generateDefaultLayoutParams();
        lpDodgeLeft.dodgeInsetEdges = Gravity.LEFT;

        final CoordinatorLayout.LayoutParams lpDodgeLeftAndTop = col.generateDefaultLayoutParams();
        lpDodgeLeftAndTop.dodgeInsetEdges = Gravity.LEFT | Gravity.TOP;

        final CoordinatorLayout.LayoutParams lpDodgeAll = col.generateDefaultLayoutParams();
        lpDodgeAll.dodgeInsetEdges = Gravity.FILL;

        final View a = new View(col.getContext());
        final View b = new View(col.getContext());

        assertThat(dependsOn(lpDodgeLeft, lpInsetLeft, col, a, b), is(true));
        assertThat(dependsOn(lpDodgeLeft, lpInsetRight, col, a, b), is(false));
        assertThat(dependsOn(lpDodgeLeft, lpInsetTop, col, a, b), is(false));
        assertThat(dependsOn(lpDodgeLeft, lpInsetBottom, col, a, b), is(false));

        assertThat(dependsOn(lpDodgeLeftAndTop, lpInsetLeft, col, a, b), is(true));
        assertThat(dependsOn(lpDodgeLeftAndTop, lpInsetRight, col, a, b), is(false));
        assertThat(dependsOn(lpDodgeLeftAndTop, lpInsetTop, col, a, b), is(true));
        assertThat(dependsOn(lpDodgeLeftAndTop, lpInsetBottom, col, a, b), is(false));

        assertThat(dependsOn(lpDodgeAll, lpInsetLeft, col, a, b), is(true));
        assertThat(dependsOn(lpDodgeAll, lpInsetRight, col, a, b), is(true));
        assertThat(dependsOn(lpDodgeAll, lpInsetTop, col, a, b), is(true));
        assertThat(dependsOn(lpDodgeAll, lpInsetBottom, col, a, b), is(true));

        assertThat(dependsOn(lpInsetLeft, lpDodgeLeft, col, a, b), is(false));
    }

    private static boolean dependsOn(CoordinatorLayout.LayoutParams lpChild,
            CoordinatorLayout.LayoutParams lpDependency, CoordinatorLayout col,
            View child, View dependency) {
        child.setLayoutParams(lpChild);
        dependency.setLayoutParams(lpDependency);
        return lpChild.dependsOn(col, child, dependency);
    }

    @Test
    public void testInsetEdge() throws Throwable {
        final Instrumentation instrumentation = getInstrumentation();
        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;

        final View insetView = new View(col.getContext());
        final View dodgeInsetView = new View(col.getContext());
        final AtomicInteger originalTop = new AtomicInteger();

        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                CoordinatorLayout.LayoutParams lpInsetView = col.generateDefaultLayoutParams();
                lpInsetView.width = CoordinatorLayout.LayoutParams.MATCH_PARENT;
                lpInsetView.height = 100;
                lpInsetView.gravity = Gravity.TOP | Gravity.LEFT;
                lpInsetView.insetEdge = Gravity.TOP;
                col.addView(insetView, lpInsetView);
                insetView.setBackgroundColor(0xFF0000FF);

                CoordinatorLayout.LayoutParams lpDodgeInsetView = col.generateDefaultLayoutParams();
                lpDodgeInsetView.width = 100;
                lpDodgeInsetView.height = 100;
                lpDodgeInsetView.gravity = Gravity.TOP | Gravity.LEFT;
                lpDodgeInsetView.dodgeInsetEdges = Gravity.TOP;
                col.addView(dodgeInsetView, lpDodgeInsetView);
                dodgeInsetView.setBackgroundColor(0xFFFF0000);
            }
        });
        instrumentation.waitForIdleSync();
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                List<View> dependencies = col.getDependencies(dodgeInsetView);
                assertThat(dependencies.size(), is(1));
                assertThat(dependencies.get(0), is(insetView));

                // Move the insetting view
                originalTop.set(dodgeInsetView.getTop());
                assertThat(originalTop.get(), is(insetView.getBottom()));
                ViewCompat.offsetTopAndBottom(insetView, 123);
            }
        });
        instrumentation.waitForIdleSync();
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // Confirm that the dodging view was moved by the same size
                assertThat(dodgeInsetView.getTop() - originalTop.get(), is(123));
            }
        });
    }

    @Test
    public void testDependentViewChanged() throws Throwable {
        final Instrumentation instrumentation = getInstrumentation();
        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;

        // Add two views, A & B, where B depends on A
        final View viewA = new View(col.getContext());
        final CoordinatorLayout.LayoutParams lpA = col.generateDefaultLayoutParams();
        lpA.width = 100;
        lpA.height = 100;

        final View viewB = new View(col.getContext());
        final CoordinatorLayout.LayoutParams lpB = col.generateDefaultLayoutParams();
        lpB.width = 100;
        lpB.height = 100;
        final CoordinatorLayout.Behavior behavior =
                spy(new DependentBehavior(viewA));
        lpB.setBehavior(behavior);

        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                col.addView(viewA, lpA);
                col.addView(viewB, lpB);
            }
        });
        instrumentation.waitForIdleSync();

        // Reset the Behavior since onDependentViewChanged may have already been called as part of
        // any layout/draw passes already
        reset(behavior);

        // Now offset view A
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                ViewCompat.offsetLeftAndRight(viewA, 20);
                ViewCompat.offsetTopAndBottom(viewA, 20);
            }
        });
        instrumentation.waitForIdleSync();

        // And assert that view B's Behavior was called appropriately
        verify(behavior, times(1)).onDependentViewChanged(col, viewB, viewA);
    }

    @Test
    public void testDependentViewRemoved() throws Throwable {
        final Instrumentation instrumentation = getInstrumentation();
        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;

        // Add two views, A & B, where B depends on A
        final View viewA = new View(col.getContext());
        final View viewB = new View(col.getContext());
        final CoordinatorLayout.LayoutParams lpB = col.generateDefaultLayoutParams();
        final CoordinatorLayout.Behavior behavior =
                spy(new DependentBehavior(viewA));
        lpB.setBehavior(behavior);

        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                col.addView(viewA);
                col.addView(viewB, lpB);
            }
        });
        instrumentation.waitForIdleSync();

        // Now remove view A
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                col.removeView(viewA);
            }
        });

        // And assert that View B's Behavior was called appropriately
        verify(behavior, times(1)).onDependentViewRemoved(col, viewB, viewA);
    }

    @Test
    public void testGetDependenciesAfterDependentViewRemoved() throws Throwable {
        final Instrumentation instrumentation = getInstrumentation();
        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;

        // Add two views, A & B, where B depends on A
        final View viewA = new View(col.getContext());
        final View viewB = new View(col.getContext());
        final CoordinatorLayout.LayoutParams lpB = col.generateDefaultLayoutParams();
        final CoordinatorLayout.Behavior behavior =
                new CoordinatorLayoutUtils.DependentBehavior(viewA) {
                    @Override
                    public void onDependentViewRemoved(
                            CoordinatorLayout parent, View child, View dependency) {
                        parent.getDependencies(child);
                    }
                };
        lpB.setBehavior(behavior);

        // Now add views
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                col.addView(viewA);
                col.addView(viewB, lpB);
            }
        });

        // Wait for a layout
        instrumentation.waitForIdleSync();

        // Now remove view A, which will trigger onDependentViewRemoved() on view B's behavior
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                col.removeView(viewA);
            }
        });
    }

    @Test
    public void testDodgeInsetBeforeLayout() throws Throwable {
        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;

        // Add a dummy view, which will be used to trigger a hierarchy change.
        final View dummy = new View(col.getContext());

        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                col.addView(dummy);
            }
        });

        // Wait for a layout.
        mInstrumentation.waitForIdleSync();

        final View dodge = new View(col.getContext());
        final CoordinatorLayout.LayoutParams lpDodge = col.generateDefaultLayoutParams();
        lpDodge.dodgeInsetEdges = Gravity.BOTTOM;
        lpDodge.setBehavior(new CoordinatorLayout.Behavior() {
            @Override
            public boolean getInsetDodgeRect(CoordinatorLayout parent, View child, Rect rect) {
                // Any non-empty rect is fine here.
                rect.set(0, 0, 10, 10);
                return true;
            }
        });

        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                col.addView(dodge, lpDodge);

                // Ensure the new view is in the list of children.
                int heightSpec = MeasureSpec.makeMeasureSpec(col.getHeight(), MeasureSpec.EXACTLY);
                int widthSpec = MeasureSpec.makeMeasureSpec(col.getWidth(), MeasureSpec.EXACTLY);
                col.measure(widthSpec, heightSpec);

                // Force a hierarchy change.
                col.removeView(dummy);
            }
        });

        // Wait for a layout.
        mInstrumentation.waitForIdleSync();
    }

    @Test
    public void testGoneViewsNotMeasuredLaidOut() throws Throwable {
        final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
        final CoordinatorLayout col = activity.mCoordinatorLayout;

        // Now create a GONE view and add it to the CoordinatorLayout
        final View imageView = new View(activity);
        imageView.setVisibility(View.GONE);
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                col.addView(imageView, 200, 200);
            }
        });
        // Wait for a layout and measure pass
        mInstrumentation.waitForIdleSync();

        // And assert that it has not been laid out
        assertFalse(imageView.getMeasuredWidth() > 0);
        assertFalse(imageView.getMeasuredHeight() > 0);
        assertFalse(ViewCompat.isLaidOut(imageView));

        // Now set the view to INVISIBLE
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                imageView.setVisibility(View.INVISIBLE);
            }
        });
        // Wait for a layout and measure pass
        mInstrumentation.waitForIdleSync();

        // And assert that it has been laid out
        assertTrue(imageView.getMeasuredWidth() > 0);
        assertTrue(imageView.getMeasuredHeight() > 0);
        assertTrue(ViewCompat.isLaidOut(imageView));
    }

    @Test
    public void testNestedScrollingDispatchesToBehavior() throws Throwable {
        final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
        final CoordinatorLayout col = activity.mCoordinatorLayout;

        // Now create a view and add it to the CoordinatorLayout with the spy behavior,
        // along with a NestedScrollView
        final ImageView imageView = new ImageView(activity);
        final CoordinatorLayout.Behavior behavior = spy(new NestedScrollingBehavior());
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                LayoutInflater.from(activity).inflate(R.layout.include_nestedscrollview, col, true);

                CoordinatorLayout.LayoutParams clp = new CoordinatorLayout.LayoutParams(200, 200);
                clp.setBehavior(behavior);
                col.addView(imageView, clp);
            }
        });

        // Now vertically swipe up on the NSV, causing nested scrolling to occur
        onView(withId(R.id.nested_scrollview)).perform(swipeUp());

        // Verify that the Behavior's onStartNestedScroll was called once
        verify(behavior, times(1)).onStartNestedScroll(
                eq(col), // parent
                eq(imageView), // child
                any(View.class), // target
                any(View.class), // direct child target
                any(int.class)); // axes

        // Verify that the Behavior's onNestedScrollAccepted was called once
        verify(behavior, times(1)).onNestedScrollAccepted(
                eq(col), // parent
                eq(imageView), // child
                any(View.class), // target
                any(View.class), // direct child target
                any(int.class)); // axes

        // Verify that the Behavior's onNestedPreScroll was called at least once
        verify(behavior, atLeastOnce()).onNestedPreScroll(
                eq(col), // parent
                eq(imageView), // child
                any(View.class), // target
                any(int.class), // dx
                any(int.class), // dy
                any(int[].class)); // consumed

        // Verify that the Behavior's onNestedScroll was called at least once
        verify(behavior, atLeastOnce()).onNestedScroll(
                eq(col), // parent
                eq(imageView), // child
                any(View.class), // target
                any(int.class), // dx consumed
                any(int.class), // dy consumed
                any(int.class), // dx unconsumed
                any(int.class)); // dy unconsumed

        // Verify that the Behavior's onStopNestedScroll was called once
        verify(behavior, times(1)).onStopNestedScroll(
                eq(col), // parent
                eq(imageView), // child
                any(View.class)); // target
    }

    @Test
    public void testNestedScrollingDispatchingToBehaviorWithGoneView() throws Throwable {
        final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
        final CoordinatorLayout col = activity.mCoordinatorLayout;

        // Now create a GONE view and add it to the CoordinatorLayout with the spy behavior,
        // along with a NestedScrollView
        final ImageView imageView = new ImageView(activity);
        imageView.setVisibility(View.GONE);
        final CoordinatorLayout.Behavior behavior = spy(new NestedScrollingBehavior());
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                LayoutInflater.from(activity).inflate(R.layout.include_nestedscrollview, col, true);

                CoordinatorLayout.LayoutParams clp = new CoordinatorLayout.LayoutParams(200, 200);
                clp.setBehavior(behavior);
                col.addView(imageView, clp);
            }
        });

        // Now vertically swipe up on the NSV, causing nested scrolling to occur
        onView(withId(R.id.nested_scrollview)).perform(swipeUp());

        // Verify that the Behavior's onStartNestedScroll was not called
        verify(behavior, never()).onStartNestedScroll(
                eq(col), // parent
                eq(imageView), // child
                any(View.class), // target
                any(View.class), // direct child target
                any(int.class)); // axes

        // Verify that the Behavior's onNestedScrollAccepted was not called
        verify(behavior, never()).onNestedScrollAccepted(
                eq(col), // parent
                eq(imageView), // child
                any(View.class), // target
                any(View.class), // direct child target
                any(int.class)); // axes

        // Verify that the Behavior's onNestedPreScroll was not called
        verify(behavior, never()).onNestedPreScroll(
                eq(col), // parent
                eq(imageView), // child
                any(View.class), // target
                any(int.class), // dx
                any(int.class), // dy
                any(int[].class)); // consumed

        // Verify that the Behavior's onNestedScroll was not called
        verify(behavior, never()).onNestedScroll(
                eq(col), // parent
                eq(imageView), // child
                any(View.class), // target
                any(int.class), // dx consumed
                any(int.class), // dy consumed
                any(int.class), // dx unconsumed
                any(int.class)); // dy unconsumed

        // Verify that the Behavior's onStopNestedScroll was not called
        verify(behavior, never()).onStopNestedScroll(
                eq(col), // parent
                eq(imageView), // child
                any(View.class)); // target
    }

    @Test
    public void testNestedScrollingTriggeringDependentViewChanged() throws Throwable {
        final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
        final CoordinatorLayout col = activity.mCoordinatorLayout;

        // First a NestedScrollView to trigger nested scrolling
        final View scrollView = LayoutInflater.from(activity).inflate(
                R.layout.include_nestedscrollview, col, false);

        // Now create a View and Behavior which depend on the scrollview
        final ImageView dependentView = new ImageView(activity);
        final CoordinatorLayout.Behavior dependentBehavior = spy(new DependentBehavior(scrollView));

        // Finally a view which accepts nested scrolling in the CoordinatorLayout
        final ImageView nestedScrollAwareView = new ImageView(activity);

        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // First add the ScrollView
                col.addView(scrollView);

                // Now add the view which depends on the scrollview
                CoordinatorLayout.LayoutParams clp = new CoordinatorLayout.LayoutParams(200, 200);
                clp.setBehavior(dependentBehavior);
                col.addView(dependentView, clp);

                // Now add the nested scrolling aware view
                clp = new CoordinatorLayout.LayoutParams(200, 200);
                clp.setBehavior(new NestedScrollingBehavior());
                col.addView(nestedScrollAwareView, clp);
            }
        });

        // Wait for any layouts, and reset the Behavior so that the call counts are 0
        getInstrumentation().waitForIdleSync();
        reset(dependentBehavior);

        // Now vertically swipe up on the NSV, causing nested scrolling to occur
        onView(withId(R.id.nested_scrollview)).perform(swipeUp());

        // Verify that the Behavior's onDependentViewChanged is not called due to the
        // nested scroll
        verify(dependentBehavior, never()).onDependentViewChanged(
                eq(col), // parent
                eq(dependentView), // child
                eq(scrollView)); // axes
    }

    @Test
    public void testDodgeInsetViewWithEmptyBounds() throws Throwable {
        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;

        // Add a view with zero height/width which is set to dodge its bounds
        final View view = new View(col.getContext());
        final CoordinatorLayout.Behavior spyBehavior = spy(new DodgeBoundsBehavior());
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                final CoordinatorLayout.LayoutParams lp = col.generateDefaultLayoutParams();
                lp.dodgeInsetEdges = Gravity.BOTTOM;
                lp.gravity = Gravity.BOTTOM;
                lp.height = 0;
                lp.width = 0;
                lp.setBehavior(spyBehavior);
                col.addView(view, lp);
            }
        });

        // Wait for a layout
        mInstrumentation.waitForIdleSync();

        // Now add an non-empty bounds inset view to the bottom of the CoordinatorLayout
        mActivityTestRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                final View dodge = new View(col.getContext());
                final CoordinatorLayout.LayoutParams lp = col.generateDefaultLayoutParams();
                lp.insetEdge = Gravity.BOTTOM;
                lp.gravity = Gravity.BOTTOM;
                lp.height = 60;
                lp.width = CoordinatorLayout.LayoutParams.MATCH_PARENT;
                col.addView(dodge, lp);
            }
        });

        // Verify that the Behavior of the view with empty bounds does not have its
        // getInsetDodgeRect() called
        verify(spyBehavior, never())
                .getInsetDodgeRect(same(col), same(view), any(Rect.class));
    }

    public static class NestedScrollingBehavior extends CoordinatorLayout.Behavior<View> {
        @Override
        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
                View directTargetChild, View target, int nestedScrollAxes) {
            // Return true so that we always accept nested scroll events
            return true;
        }
    }

    public static class DodgeBoundsBehavior extends CoordinatorLayout.Behavior<View> {
        @Override
        public boolean getInsetDodgeRect(CoordinatorLayout parent, View child, Rect rect) {
            rect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
            return true;
        }
    }

    @UiThreadTest
    @Test
    public void testAnchorDependencyGraph() throws Throwable {
        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;

        // Override hashcode because of implementation of SimpleArrayMap used in
        // DirectedAcyclicGraph used for sorting dependencies. Hashcode of anchored view has to be
        // greater than of the one it is anchored to in order to reproduce the error.
        final View anchor = createViewWithHashCode(col.getContext(), 2);
        anchor.setId(R.id.anchor);

        final View ship = createViewWithHashCode(col.getContext(), 3);
        final CoordinatorLayout.LayoutParams lp = col.generateDefaultLayoutParams();
        lp.setAnchorId(R.id.anchor);

        col.addView(anchor);
        col.addView(ship, lp);

        // Get dependencies immediately to avoid possible call to onMeasure(), since error
        // only happens on first computing of sorted dependencies.
        List<View> dependencySortedChildren = col.getDependencySortedChildren();
        assertThat(dependencySortedChildren, is(Arrays.asList(anchor, ship)));
    }

    @NonNull
    private View createViewWithHashCode(final Context context, final int hashCode) {
        return new View(context) {
            @Override
            public int hashCode() {
                return hashCode;
            }
        };
    }
}
