// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.ui.modelutil;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.support.annotation.Nullable;
import android.view.View;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.ui.modelutil.PropertyModel.WritableBooleanPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableIntPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableObjectPropertyKey;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor.ViewBinder;
import org.chromium.ui.test.util.modelutil.FakeViewProvider;

/**
 * Unit tests for LazyConstructionPropertyMcp.
 */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class LazyConstructionPropertyMcpTest {
    private static final WritableBooleanPropertyKey VISIBILITY = new WritableBooleanPropertyKey();
    private static final WritableObjectPropertyKey<String> STRING_PROPERTY =
            new WritableObjectPropertyKey<>();
    private static final WritableIntPropertyKey INT_PROPERTY = new WritableIntPropertyKey();
    private static final PropertyKey[] ALL_PROPERTIES =
            new PropertyKey[] {VISIBILITY, STRING_PROPERTY, INT_PROPERTY};
    private PropertyModel mModel;
    private FakeViewProvider<View> mViewProvider;
    private @Nullable PropertyObservable.PropertyObserver<PropertyKey> mModelObserver;

    @Mock
    private View mView;
    @Mock
    private ViewBinder<PropertyModel, View, PropertyKey> mViewBinder;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mModel = new PropertyModel(ALL_PROPERTIES);
        mModel.set(VISIBILITY, false);
        mViewProvider = new FakeViewProvider<>();
        mModel.addObserver((source, propertyKey) -> {
            // Forward model changes to the model observer if it exists. It's important for the test
            // that the observer is notified before the LazyConstructionPropertyMcp.
            if (mModelObserver != null) mModelObserver.onPropertyChanged(source, propertyKey);
        });
    }

    @Test
    public void testInitialConstruction() {
        LazyConstructionPropertyMcp.create(mModel, VISIBILITY, mViewProvider, mViewBinder);
        assertFalse(mViewProvider.inflationHasStarted());

        mModel.set(VISIBILITY, true);

        assertTrue(mViewProvider.inflationHasStarted());
        mViewProvider.finishInflation(mView);
        ShadowLooper.idleMainLooper();

        verifyBind(VISIBILITY);
    }

    @Test
    public void testUpdatesBeforeInflation() {
        LazyConstructionPropertyMcp.create(mModel, VISIBILITY, mViewProvider, mViewBinder);
        mModel.set(STRING_PROPERTY, "foo");
        mModel.set(VISIBILITY, true);
        assertTrue(mViewProvider.inflationHasStarted());
        mViewProvider.finishInflation(mView);
        ShadowLooper.idleMainLooper();
        verifyBind(STRING_PROPERTY, VISIBILITY);
    }

    @Test
    public void testUpdatesWhileVisible() {
        LazyConstructionPropertyMcp.create(mModel, VISIBILITY, mViewProvider, mViewBinder);

        // Show the view and pump the looper to do the initial bind.
        mModel.set(VISIBILITY, true);
        assertTrue(mViewProvider.inflationHasStarted());
        mViewProvider.finishInflation(mView);
        ShadowLooper.idleMainLooper();
        verifyBind(VISIBILITY);
        Mockito.<ViewBinder>reset(mViewBinder);

        mModel.set(INT_PROPERTY, 42);
        verifyBind(INT_PROPERTY);

        mModel.set(VISIBILITY, false);
        verifyBind(VISIBILITY);
    }

    @Test
    public void testUpdatesWhileHidden() {
        LazyConstructionPropertyMcp.create(mModel, VISIBILITY, mViewProvider, mViewBinder);

        // Show the view and pump the looper to do the initial bind, then hide the view again.
        mModel.set(VISIBILITY, true);
        assertTrue(mViewProvider.inflationHasStarted());
        mViewProvider.finishInflation(mView);
        ShadowLooper.idleMainLooper();
        verifyBind(VISIBILITY);

        mModel.set(VISIBILITY, false);
        verify(mViewBinder, times(2)).bind(eq(mModel), eq(mView), eq(VISIBILITY));
        Mockito.<ViewBinder>reset(mViewBinder);

        // While the view is hidden, the binder should not be invoked.
        mModel.set(STRING_PROPERTY, "foo");
        mModel.set(STRING_PROPERTY, "bar");
        verify(mViewBinder, never())
                .bind(any(PropertyModel.class), any(View.class), any(PropertyKey.class));

        // When the view is shown, all pending updates should be dispatched, coalescing updates to
        // the same property.
        mModel.set(VISIBILITY, true);
        verifyBind(VISIBILITY, STRING_PROPERTY);
    }

    @Test
    public void testReentrantUpdates() {
        mModel.set(INT_PROPERTY, 0);
        LazyConstructionPropertyMcp.create(mModel, VISIBILITY, mViewProvider, mViewBinder);

        // Increase INT_PROPERTY any time visibility changes.
        mModelObserver = (source, propertyKey) -> {
            if (propertyKey != VISIBILITY) return;
            mModel.set(INT_PROPERTY, mModel.get(INT_PROPERTY) + 1);
        };

        mModel.set(VISIBILITY, true);
        mViewProvider.finishInflation(mView);
        ShadowLooper.idleMainLooper();
        verifyBind(VISIBILITY, INT_PROPERTY);
        assertThat(mModel.get(INT_PROPERTY), is(1));
        Mockito.<ViewBinder>reset(mViewBinder);

        mModel.set(VISIBILITY, false);
        verifyBind(INT_PROPERTY, VISIBILITY);
        assertThat(mModel.get(INT_PROPERTY), is(2));
    }

    private void verifyBind(PropertyKey... properties) {
        for (PropertyKey key : properties) {
            verify(mViewBinder).bind(mModel, mView, key);
        }
    }
}
