/*
 * Copyright 2015 Google Inc. All rights reserved.
 *
 * 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 com.google.samples.apps.iosched.schedule;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.intent.Intents.intended;
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent;
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasData;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

import static com.google.samples.apps.iosched.testutils.MatchersHelper.moveViewPagerToPage;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.core.IsNot.not;

import android.content.Intent;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.Espresso;
import android.support.test.espresso.IdlingResource;
import android.support.test.filters.LargeTest;
import android.support.test.filters.Suppress;
import android.support.test.runner.AndroidJUnit4;

import com.google.samples.apps.iosched.lib.R;
import com.google.samples.apps.iosched.mockdata.MyScheduleMockItems;
import com.google.samples.apps.iosched.mockdata.StubActivityContext;
import com.google.samples.apps.iosched.navigation.NavigationModel;
import com.google.samples.apps.iosched.settings.SettingsUtils;
import com.google.samples.apps.iosched.testutils.NavigationUtils;
import com.google.samples.apps.iosched.testutils.ThrottleContentObserverIdlingResource;
import com.google.samples.apps.iosched.util.RegistrationUtils;
import com.google.samples.apps.iosched.util.TimeUtils;

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

/**
 * UI tests for {@link ScheduleActivity} for when the user is attending the conference and the
 * second day of the conference starts in 3 hours.
 * <p/>
 * This should be run on devices with a narrow layout only (phones all orientation, tablets in
 * portrait mode)
 */
@RunWith(AndroidJUnit4.class)
@LargeTest
@Suppress // Can't get these to pass. No idea why since they all work
public class ScheduleActivityTest {

    /**
     * The {@link StubScheduleModel} needs a {@link android.content.Context} but at the stage it
     * is created, {@link #mActivityRule} hasn't got an {@link android.app.Activity} yet so we use
     * the instrumentation target context. However, an Actiivty Context is required by the model for
     * carrying certain actions, such as opening the session that was clicked on (which uses {@link
     * android.content.Context#startActivity(Intent)} and will not work with a non Activity context.
     * We use this {@link StubActivityContext} to later set an activity context at the start of each
     * test if needed (if the test needs to start another activity).
     */
    private StubActivityContext mActivityStubContext;

    private StubScheduleModel mStubMyScheduleModel;

//    DISABLED: Broken
//    @Rule
//    public BaseActivityTestRule<ScheduleActivity> mActivityRule =
//            new BaseActivityTestRule<ScheduleActivity>(ScheduleActivity.class) {
//
//                @Override
//                protected void beforeActivityLaunched() {
//                    prepareActivityForInPersonAttendee();
//
//
//                    // Create a stub model to simulate a user attending conference, during the
//                    // second day
//                    mActivityStubContext =
//                            new StubActivityContext(InstrumentationRegistry.getTargetContext());
//                    try {
//                        /**
//                         * {@link ScheduleModel} uses a Handler, so we need to run this on the
//                         * main thread. If we don't, we need to call {@link Looper#prepare()} but
//                         * the test runner uses the same non UI thread for setting up each test in a
//                         * test class, and therefore, upon trying to run the second test, it
//                         * complains that we call {@link Looper#prepare()} on a thread that has
//                         * already been prepared. By using the UI thread, we avoid this issue as
//                         * the UI thread is already prepared so we don't need to manually do it.
//                         */
//                        runOnUiThread(new Runnable() {
//                            @Override
//                            public void run() {
//                                mStubMyScheduleModel = new StubScheduleModel(
//                                        mActivityStubContext,
//                                        MyScheduleMockItems.getItemsForAttendeeAfter(1, false),
//                                        MyScheduleMockItems.getItemsForAttendeeBefore(2));
//                                ModelProvider.setStubModel(mStubMyScheduleModel);
//                            }
//                        });
//                    } catch (Throwable throwable) {
//                        Log.e("DEBUG", "Error running test " + throwable);
//                    }
//                }
//            };

    @Before
    public void setUp() {
        // Set up time to start of second day of conference
        TimeUtils.setCurrentTimeRelativeToStartOfSecondDayOfConference(
                InstrumentationRegistry.getTargetContext(), 0);

        // Don't show notifications for sessions as they get in the way of the UI
        SettingsUtils.setShowNotifications(InstrumentationRegistry.getTargetContext(), false);

        // Mark use as attending conference
        RegistrationUtils.setRegisteredAttendee(InstrumentationRegistry.getTargetContext(), true);
    }

    @Test
    @Suppress // Test isn't deterministic when run as part of the full test suite.
    public void day2Selected() {
        // Given a current time 3 hours after the start of the second day

        // Then the second day is selected
        onView(withText(MyScheduleMockItems.SESSION_TITLE_BEFORE)).check(matches(isDisplayed()));
    }

//    DISABLED: Broken
//    @Test
//    @Suppress // Test isn't deterministic when run as part of the full test suite.
//    public void viewDay2_clickOnSession_opensSessionScreenIntentFired() {
//        mActivityStubContext.setActivityContext(mActivityRule.getActivity());
//
//        // When clicking on the session
//        onView(withText(MyScheduleMockItems.SESSION_TITLE_BEFORE)).perform(click());
//
//        // Then the intent with the session uri is fired
//        Uri expectedSessionUri =
//                ScheduleContract.Sessions.buildSessionUri(MyScheduleMockItems.SESSION_ID);
//        intended(allOf(
//                hasAction(equalTo(Intent.ACTION_VIEW)),
//                hasData(expectedSessionUri)));
//    }

//    DISABLED: Broken
//    @Test
//    @Suppress // Test isn't deterministic when run as part of the full test suite.
//    public void viewDay1_clickOnSession_opensSessionScreenIntentFired() {
//        mActivityStubContext.setActivityContext(mActivityRule.getActivity());
//
//        // When clicking on the session
//        onView(withText(MyScheduleMockItems.SESSION_TITLE_BEFORE)).perform(click());
//
//        // Then the intent with the session uri is fired
//        Uri expectedSessionUri =
//                ScheduleContract.Sessions.buildSessionUri(MyScheduleMockItems.SESSION_ID);
//        intended(allOf(
//                hasAction(equalTo(Intent.ACTION_VIEW)),
//                hasData(expectedSessionUri)));
//    }

//    DISABLED: Broken
//    @Test
//    public void viewDay1_clickOnRateSession_opensFeedbackScreenIntentFired() {
//        mActivityStubContext.setActivityContext(mActivityRule.getActivity());
//
//        // Given day 1 visible
//        showDay(1);
//
//        // When clicking on rate session
//        onView(allOf(withText(R.string.my_schedule_rate_this_session), isDisplayed()))
//                .perform(click());
//
//        // Then the intent for the feedback screen is fired
//        Uri expectedSessionUri =
//                ScheduleContract.Sessions.buildSessionUri(MyScheduleMockItems.SESSION_ID);
//        intended(allOf(
//                hasAction(equalTo(Intent.ACTION_VIEW)),
//                hasData(expectedSessionUri),
//                hasComponent(SessionFeedbackActivity.class.getName())));
//    }

//    DISABLED: Broken
//    @Test
//    @Suppress // Test isn't deterministic when run as part of the full test suite.
//    public void viewDay2_clickOnBrowseSession_opensSessionsListScreen() {
//        mActivityStubContext.setActivityContext(mActivityRule.getActivity());
//
//        // When clicking on browse sessions
//        onView(allOf(withText(R.string.browse_sessions), isDisplayed())).perform(click());
//
//        // Then the intent for the sessions list screen is fired
//        long slotStart = Config.CONFERENCE_START_MILLIS + 1 * TimeUtils.DAY
//                + MyScheduleMockItems.SESSION_AVAILABLE_SLOT_TIME_OFFSET;
//        Uri expectedTimeIntervalUri =
//                ScheduleContract.Sessions.buildUnscheduledSessionsInInterval(slotStart,
//                        slotStart + MyScheduleMockItems.SESSION_AVAILABLE_SLOT_TIME_DURATION);
//        intended(allOf(
//                hasAction(equalTo(Intent.ACTION_VIEW)),
//                hasData(expectedTimeIntervalUri)));
//    }

//    DISABLED: Broken
//    @Test
//    @Suppress // Test isn't deterministic when run as part of the full test suite.
//    public void viewDay2_clickOnTimeSlot_opensSessionsListScreen() {
//        mActivityStubContext.setActivityContext(mActivityRule.getActivity());
//
//        // When clicking on the time of a time slot
//        long slotStart = Config.CONFERENCE_START_MILLIS + 1 * TimeUtils.DAY;
//        onView(allOf(isDisplayed(), withId(R.id.start_time),
//                withText(TimeUtils.formatShortTime(mActivityStubContext, new Date(slotStart)))))
//                .perform(click());
//
//        // Then the intent for the sessions list screen is fired
//        Uri expectedTimeIntervalUri =
//                ScheduleContract.Sessions.buildUnscheduledSessionsInInterval(slotStart,
//                        slotStart + MyScheduleMockItems.SESSION_AVAILABLE_SLOT_TIME_DURATION);
//        intended(allOf(
//                hasAction(equalTo(Intent.ACTION_VIEW)),
//                hasData(expectedTimeIntervalUri)));
//    }

//    DISABLED: Broken
//    @Test
//    @Suppress // Test isn't deterministic when run as part of the full test suite.
//    public void viewDay2_clickOnMoreButton_opensSessionsListScreen() {
//        mActivityStubContext.setActivityContext(mActivityRule.getActivity());
//
//        // When clicking on the time of the more button next to a time slot
//        long slotStart = Config.CONFERENCE_START_MILLIS + 1 * TimeUtils.DAY;
//        onView(allOf(isDisplayed(), withId(R.id.more), hasSibling(
//                withText(TimeUtils.formatShortTime(mActivityStubContext, new Date(slotStart))))))
//                .perform(click());
//
//        // Then the intent for the sessions list screen is fired
//        Uri expectedTimeIntervalUri =
//                ScheduleContract.Sessions.buildUnscheduledSessionsInInterval(slotStart,
//                        slotStart + MyScheduleMockItems.SESSION_AVAILABLE_SLOT_TIME_DURATION);
//        intended(allOf(
//                hasAction(equalTo(Intent.ACTION_VIEW)),
//                hasData(expectedTimeIntervalUri)));
//    }

//    DISABLED: Broken
//    @Test
//    @Suppress // Test isn't deterministic when run as part of the full test suite.
//    public void timeSlotWithNoSessionInSchedule_MoreButton_IsNotVisible() {
//        mActivityStubContext.setActivityContext(mActivityRule.getActivity());
//
//        // More button is not visible for a time slow with no sessions in schedule
//        long slotStartWithAvailableSessionsButNoneInSchedule =
//                Config.CONFERENCE_START_MILLIS + 1 * TimeUtils.DAY +
//                        MyScheduleMockItems.SESSION_AVAILABLE_SLOT_TIME_OFFSET;
//        onView(allOf(withId(R.id.more), hasSibling(
//                withText(TimeUtils.formatShortTime(mActivityStubContext,
//                        new Date(slotStartWithAvailableSessionsButNoneInSchedule))))))
//                .check(matches(not(isDisplayed())));
//    }

//    DISABLED: Broken
//    @Test
//    @Suppress // Test isn't deterministic when run as part of the full test suite.
//    public void timeSlotWithOneSessionInSchedule_MoreButton_IsVisible() {
//        mActivityStubContext.setActivityContext(mActivityRule.getActivity());
//
//        // More button is visible for a time slow with 1 session in schedule
//        long slotStartWithOneSessionInSchedule =
//                Config.CONFERENCE_START_MILLIS +
//                        MyScheduleMockItems.SESSION_TITLE_AFTER_START_OFFSET;
//        onView(allOf(isDisplayed(), withId(R.id.more), hasSibling(
//                withText(TimeUtils.formatShortTime(mActivityStubContext,
//                        new Date(slotStartWithOneSessionInSchedule))))))
//                .check(matches(isDisplayed()));
//    }

    @Test
    public void viewDay1_sessionVisible() {
        // Given day 1 visible
        showDay(1);

        // Then the session in the first day is displayed
        onView(withText(MyScheduleMockItems.SESSION_TITLE_AFTER)).check(matches(isDisplayed()));
    }

    @Test
    @Suppress // Test isn't deterministic when run as part of the full test suite.
    public void viewDay2_sessionVisible() {
        // Given day 2 visible

        // Then the session in the second day is displayed
        onView(withText(MyScheduleMockItems.SESSION_TITLE_BEFORE)).check(matches(isDisplayed()));
    }

    @Test
    public void navigation_WhenShown_CorrectItemIsSelected() {
        NavigationUtils
                .checkNavigationItemIsSelected(NavigationModel.NavigationItemEnum.MY_SCHEDULE);
    }

//    DISABLED: Broken
//    /**
//     * This test works only on phones, where the layout is the same for both orientations (ie tabs)
//     */
//    @Test
//    @Suppress // Test isn't deterministic when run as part of the full test suite.
//    public void orientationChange_RetainsDataAndCurrentTab_Flaky() {
//        // Given day 2 visible
//        showDay(2);
//        onView(withText(MyScheduleMockItems.SESSION_TITLE_BEFORE)).check(matches(isDisplayed()));
//
//        // When changing orientation
//        OrientationHelper.rotateOrientation(mActivityRule);
//
//        // Then day 2 is visible
//        onView(withText(MyScheduleMockItems.SESSION_TITLE_BEFORE)).check(matches(isDisplayed()));
//        // And day 0 is selectable and visible
//        showDay(0);
//        onView(withText(R.string.my_schedule_badgepickup)).check(matches(isDisplayed()));
//        // And day 1 is selectable and visible
//        showDay(1);
//        onView(withText(MyScheduleMockItems.SESSION_TITLE_AFTER)).check(matches(isDisplayed()));
//
//        // When changing orientation again
//        OrientationHelper.rotateOrientation(mActivityRule);
//
//        // Then day 1 is visible
//        onView(withText(MyScheduleMockItems.SESSION_TITLE_AFTER)).check(matches(isDisplayed()));
//        // And day 0 is selectable and visible
//        showDay(0);
//        onView(withText(R.string.my_schedule_badgepickup)).check(matches(isDisplayed()));
//        // And day 2 is selectable and visible
//        showDay(2);
//        onView(withText(MyScheduleMockItems.SESSION_TITLE_BEFORE)).check(matches(isDisplayed()));
//    }

    @Test
    public void newDataObtained_DataUpdated() {
        IdlingResource idlingResource = null;
        try {
            // Given initial data displayed
            showDay(2);
            onView(withText(MyScheduleMockItems.SESSION_TITLE_BEFORE))
                    .check(matches(isDisplayed()));
            onView(withText(MyScheduleMockItems.SESSION_TITLE_2)).check(doesNotExist());
            showDay(1);
            onView(withText(MyScheduleMockItems.SESSION_TITLE_AFTER)).check(matches(isDisplayed()));
            onView(withText(MyScheduleMockItems.SESSION_TITLE_1)).check(doesNotExist());

            // When new data is available
            mStubMyScheduleModel.setMockScheduleDataDay1(MyScheduleMockItems
                    .getItemsForAttendee(1, false, MyScheduleMockItems.SESSION_TITLE_1));
            mStubMyScheduleModel.setMockScheduleDataDay2(MyScheduleMockItems
                    .getItemsForAttendee(2, false, MyScheduleMockItems.SESSION_TITLE_2));
            mStubMyScheduleModel.fireContentObserver();
            // Wait for the ThrottleContentObserver to process the event
            idlingResource = new ThrottleContentObserverIdlingResource(
                    InstrumentationRegistry.getTargetContext());
            Espresso.registerIdlingResources(idlingResource);

            // Then the new data is shown
            onView(withText(MyScheduleMockItems.SESSION_TITLE_1)).check(matches(isDisplayed()));
            onView(withText(MyScheduleMockItems.SESSION_TITLE_AFTER))
                    .check(doesNotExist());
            showDay(2);
            onView(withText(MyScheduleMockItems.SESSION_TITLE_2)).check(matches(isDisplayed()));
            onView(withText(MyScheduleMockItems.SESSION_TITLE_BEFORE))
                    .check(doesNotExist());
        } finally {
            if (idlingResource != null) {
                Espresso.unregisterIdlingResources(idlingResource);
            }
        }
    }

    private void showDay(int day) {
        onView(withId(R.id.view_pager)).perform(moveViewPagerToPage(day));
    }
}