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

import com.google.samples.apps.iosched.BuildConfig;
import com.google.samples.apps.iosched.InvalidEnum;
import com.google.samples.apps.iosched.R;
import com.google.samples.apps.iosched.model.TagMetadataTest;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.util.SessionsHelper;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import android.content.Context;
import android.content.CursorLoader;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.test.suitebuilder.annotation.SmallTest;

import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
@SmallTest
public class SessionDetailModelTest {

    private static final String FAKE_ID = "FAKE ID";

    private static final String FAKE_TITLE = "FAKE TITLE";

    private static final String FAKE_ROOM_ID = "FAKE ROOM ID";

    private static final String FAKE_HASHTAG = "FAKE HASHTAG";

    private static final String FAKE_SPEAKER_NAME = "FAKE SPEAKER NAME";

    private static final String FAKE_SPEAKER_IMAGE_URL = "FAKE SPEAKER IMAGE URL";

    private static final String FAKE_SPEAKER_COMPANY = "FAKE SPEAKER COMPANY";

    private static final String FAKE_SPEAKER_URL = "FAKE SPEAKER URL";

    private static final String FAKE_SPEAKER_ABSTRACT = "FAKE SPEAKER ABSTRACT";

    private static final int FAKE_COLOR = 200;

    private static final long FAKE_CURRENT_TIME_OFFSET = 0l;

    private static final int SESSION_TITLE_COLUMN_INDEX = 1;

    private static final int SESSION_COLOR_COLUMN_INDEX = 2;

    private static final int SESSION_START_COLUMN_INDEX = 3;

    private static final int SESSION_END_COLUMN_INDEX = 4;

    private static final int SESSION_IN_MY_SCHEDULE_COLUMN_INDEX = 5;

    private static final int SESSION_ROOM_ID_COLUMN_INDEX = 6;

    private static final int SESSION_HASHTAG_COLUMN_INDEX = 7;

    private static final int SPEAKER_NAME_COLUMN_INDEX = 8;

    private static final int SPEAKER_IMAGE_URL_COLUMN_INDEX = 9;

    private static final int SPEAKER_COMPANY_COLUMN_INDEX = 10;

    private static final int SPEAKER_URL_COLUMN_INDEX = 11;

    private static final int SPEAKER_PLUSONE_URL_COLUMN_INDEX = 12;

    private static final int SPEAKER_TWITTER_URL_COLUMN_INDEX = 13;

    private static final int SPEAKER_ABSTRACT_COLUMN_INDEX = 14;

    private static final long ONE_MINUTE = 1 * 60 * 1000l;

    private static final long ONE_HOUR = 1 * 60 * 60 * 1000l;

    @Mock
    private Context mMockContext;

    @Mock
    private SharedPreferences mMockSharedPreferences;

    @Mock
    private Uri mMockUri;

    @Mock
    private SessionsHelper mMockSessionsHelper;

    @Mock
    private Cursor mMockCursor;

    @Mock
    private Cursor mMockEmptyCursor;

    @Mock
    private CursorLoader mMockCursorLoader;

    private SessionDetailModel mSessionDetailModel;

    @Before
    public void setUp() {
        // Init mocks
        initMockCursors();
        initMockContextWithFakeCurrentTime();

        // Create an instance of the model.
        mSessionDetailModel = new SessionDetailModel(mMockUri, mMockContext,
                mMockSessionsHelper);
    }

    @Test
    public void readDataFromCursor_SessionQuery_SessionLoaded() {
        // Given a mock cursor with a fake session title
        initMockCursorWithTitle(mMockCursor);
        SessionDetailModel spyModel = setSpyModelForSessionLoading();

        // When ran with session query
        boolean success = spyModel.readDataFromCursor(
                mMockCursor, SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

        // Then the model has correct session title and true is returned
        assertThat(spyModel.getSessionTitle(), is(FAKE_TITLE));
        assertThat(success, is(true));
    }

    @Test
    public void readDataFromCursor_TagMetadataQuery_TagMetadataLoaded() {
        // Given a mock cursor with a fake tag
        initMockCursorWithOneTag(mMockCursor);

        // When ran with tag query
        boolean success = mSessionDetailModel.readDataFromCursor(
                mMockCursor, SessionDetailModel.SessionDetailQueryEnum.TAG_METADATA);

        // Then the tag metadata object has been created, and true is returned
        assertThat(mSessionDetailModel.getTagMetadata(), not(nullValue()));
        assertThat(success, is(true));
    }

    @Test
    public void readDataFromCursor_TagMetadataQueryWithNullCursor_ReturnsFalse() {
        // When ran with tag query and null cursor
        boolean success = mSessionDetailModel.readDataFromCursor(
                null, SessionDetailModel.SessionDetailQueryEnum.TAG_METADATA);

        // Then false is returned
        assertThat(success, is(false));
    }

    @Test
    public void readDataFromCursor_SpeakersQueryWithOneSpeaker_SpeakerLoaded() {
        // Given a mock cursor with a fake speaker
        initMockCursorWithOneSpeaker(mMockCursor);

        // When ran with speakers query
        boolean success = mSessionDetailModel.readDataFromCursor(
                mMockCursor, SessionDetailModel.SessionDetailQueryEnum.SPEAKERS);

        // Then the model has correct speakers size, and data, and true is returned
        assertThat(mSessionDetailModel.getSpeakers().size(), is(1));
        assertThat(mSessionDetailModel.getSpeakers().get(0).getName(), is(FAKE_SPEAKER_NAME));
        assertThat(mSessionDetailModel.getSpeakers().get(0).getImageUrl(),
                is(FAKE_SPEAKER_IMAGE_URL));
        assertThat(mSessionDetailModel.getSpeakers().get(0).getCompany(), is(FAKE_SPEAKER_COMPANY));
        assertThat(mSessionDetailModel.getSpeakers().get(0).getUrl(), is(FAKE_SPEAKER_URL));
        assertThat(mSessionDetailModel.getSpeakers().get(0).getAbstract(),
                is(FAKE_SPEAKER_ABSTRACT));
        assertThat(success, is(true));
    }

    @Test
    public void readDataFromCursor_SpeakersQueryWithTwoSpeakers_SpeakersLoaded() {
        // Given a mock cursor with 2 fake speakers
        initMockCursorWithTwoSpeakers(mMockCursor);

        // When ran with speakers query
        boolean success = mSessionDetailModel.readDataFromCursor(
                mMockCursor, SessionDetailModel.SessionDetailQueryEnum.SPEAKERS);

        // Then the model has correct speakers size, and data, and true is returned
        assertThat(mSessionDetailModel.getSpeakers().size(), is(2));
        assertThat(mSessionDetailModel.getSpeakers().get(0).getName(), is(FAKE_SPEAKER_NAME));
        assertThat(mSessionDetailModel.getSpeakers().get(0).getImageUrl(),
                is(FAKE_SPEAKER_IMAGE_URL));
        assertThat(mSessionDetailModel.getSpeakers().get(0).getCompany(), is(FAKE_SPEAKER_COMPANY));
        assertThat(mSessionDetailModel.getSpeakers().get(0).getUrl(), is(FAKE_SPEAKER_URL));
        assertThat(mSessionDetailModel.getSpeakers().get(0).getAbstract(),
                is(FAKE_SPEAKER_ABSTRACT));
        assertThat(mSessionDetailModel.getSpeakers().get(1).getName(), is(FAKE_SPEAKER_NAME));
        assertThat(mSessionDetailModel.getSpeakers().get(1).getImageUrl(),
                is(FAKE_SPEAKER_IMAGE_URL));
        assertThat(mSessionDetailModel.getSpeakers().get(1).getCompany(), is(FAKE_SPEAKER_COMPANY));
        assertThat(mSessionDetailModel.getSpeakers().get(1).getUrl(), is(FAKE_SPEAKER_URL));
        assertThat(mSessionDetailModel.getSpeakers().get(1).getAbstract(),
                is(FAKE_SPEAKER_ABSTRACT));
        assertThat(success, is(true));
    }

    @Test
    public void readDataFromCursor_FeedbackQueryWithFeedbackAvailable_SessionHasFeedback() {
        // Given a mock cursor with data
        when(mMockCursor.getCount()).thenReturn(1);

        // When ran with feedback query
        boolean success = mSessionDetailModel.readDataFromCursor(
                mMockCursor, SessionDetailModel.SessionDetailQueryEnum.FEEDBACK);

        // Then the session has feedback and true is returned
        assertThat(mSessionDetailModel.hasFeedback(), is(true));
        assertThat(success, is(true));
    }

    @Test
    public void readDataFromCursor_FeedbackQueryWithNoFeedbackAvailable_SessionHasNoFeedback() {
        // Given a mock cursor with no data
        when(mMockCursor.getCount()).thenReturn(0);

        // When ran with feedback query
        boolean success = mSessionDetailModel.readDataFromCursor(
                mMockCursor, SessionDetailModel.SessionDetailQueryEnum.FEEDBACK);

        // Then the session has no feedback and true is returned
        assertThat(mSessionDetailModel.hasFeedback(), is(false));
        assertThat(success, is(true));
    }

    @Test
    public void readDataFromCursor_ValidQueryWithNullCursor_ReturnsFalse() {
        // When ran with session query and null cursor
        boolean success = mSessionDetailModel.readDataFromCursor(
                null, SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

        // Then false is returned
        assertThat(success, is(false));
    }

    @Test
    public void readDataFromCursor_ValidQueryWithEmptyCursor_ReturnsFalse() {
        // When ran with session query and empty cursor
        boolean success = mSessionDetailModel.readDataFromCursor(
                mMockEmptyCursor, SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

        // Then false is returned
        assertThat(success, is(false));
    }

    @Test
    public void readDataFromCursor_InvalidQuery_ReturnsFalse() {
        // When ran with invalid query and non empty cursor
        boolean success = mSessionDetailModel.readDataFromCursor(
                mMockCursor, InvalidEnum.INVALID);

        // Then false is returned
        assertThat(success, is(false));
    }

    @Test
    public void createCursorLoader_SessionQuery_ReturnsCursorLoader() {
        // Given a mock uri and mock cursor loader
        SessionDetailModel spyModel = spy(
                new SessionDetailModel(mMockUri, mMockContext, mMockSessionsHelper));
        doReturn(FAKE_ID).when(spyModel).getSessionId(mMockUri);
        doReturn(mMockCursorLoader).when(spyModel).getCursorLoaderInstance(
                any(Context.class), any(Uri.class), any(String[].class), any(String.class),
                any(String[].class), any(String.class));

        // When ran with mock uri and session query loader id
        CursorLoader createdCursorLoader =
                (CursorLoader) spyModel.createCursorLoader(
                        SessionDetailModel.SessionDetailQueryEnum.SESSIONS.getId(), mMockUri, null);

        // Then the returned cursor loader is the mock cursor loader
        assertThat(createdCursorLoader, sameInstance(mMockCursorLoader));
    }

    @Test
    public void createCursorLoader_TagMetadaQuery_ReturnsCursorLoader() {
        // Given a mock uri and mock cursor loader
        SessionDetailModel spyModel = spy(
                new SessionDetailModel(mMockUri, mMockContext, mMockSessionsHelper));
        doReturn(mMockCursorLoader).when(spyModel).getTagMetadataLoader();

        // When ran with mock uri and tag metadata query loader id
        CursorLoader createdCursorLoader =
                (CursorLoader) spyModel.createCursorLoader(
                        SessionDetailModel.SessionDetailQueryEnum.TAG_METADATA.getId(),
                        mMockUri, null);

        // Then the returned cursor loader is the mock cursor loader
        assertThat(createdCursorLoader, sameInstance(mMockCursorLoader));
    }

    @Test
    public void createCursorLoader_SpeakersQuery_ReturnsCursor() {
        // Given a mock uri and mock cursor loader
        SessionDetailModel spyModel = spy(
                new SessionDetailModel(mMockUri, mMockContext, mMockSessionsHelper));
        doReturn(mMockUri).when(spyModel).getSpeakersDirUri(any(String.class));
        doReturn(mMockCursorLoader).when(spyModel).getCursorLoaderInstance(
                any(Context.class), any(Uri.class), any(String[].class), any(String.class),
                any(String[].class), any(String.class));

        // When ran with mock uri and speakers query loader id
        CursorLoader createdCursorLoader =
                (CursorLoader) spyModel.createCursorLoader(
                        SessionDetailModel.SessionDetailQueryEnum.SPEAKERS.getId(),
                        mMockUri, null);

        // Then the returned cursor loader is the mock cursor loader
        assertThat(createdCursorLoader, sameInstance(mMockCursorLoader));
    }

    @Test
    public void createCursorLoader_FeedbackQuery_ReturnsCursor() {
        // Given a mock uri and mock cursor loader
        SessionDetailModel spyModel = spy(
                new SessionDetailModel(mMockUri, mMockContext, mMockSessionsHelper));
        doReturn(mMockUri).when(spyModel).getFeedbackUri(any(String.class));
        doReturn(mMockCursorLoader).when(spyModel).getCursorLoaderInstance(
                any(Context.class), any(Uri.class), any(String[].class), any(String.class),
                any(String[].class), any(String.class));

        // When ran with mock uri and feedback query loader id
        CursorLoader createdCursorLoader =
                (CursorLoader) spyModel.createCursorLoader(
                        SessionDetailModel.SessionDetailQueryEnum.FEEDBACK.getId(),
                        mMockUri, null);

        // Then the returned cursor loader is the mock cursor loader
        assertThat(createdCursorLoader, sameInstance(mMockCursorLoader));
    }

    @Test
    public void createCursorLoader_InvalidQuery_ReturnsNullCursor() {
        // When ran with mock uri and invalid query loader id
        CursorLoader createdCursorLoader =
                (CursorLoader) mSessionDetailModel.createCursorLoader(InvalidEnum.INVALID.getId(),
                        mMockUri, null);

        // Then the returned cursor loader is null
        assertThat(createdCursorLoader, nullValue());
    }

    @Test
    public void requestModelUpdate_StarSession_Success() {
        // Given a loaded session not in user schedule
        initMockCursorWithSessionNotInSchedule(mMockCursor);
        SessionDetailModel spyModel = setSpyModelForSessionLoading();
        spyModel.readDataFromCursor(mMockCursor,
                SessionDetailModel.SessionDetailQueryEnum.SESSIONS);
        doNothing().when(spyModel).sendAnalyticsEvent(
                any(String.class), any(String.class), any(String.class));

        // When ran with star user action
        boolean success = spyModel.requestModelUpdate(
                SessionDetailModel.SessionDetailUserActionEnum.STAR, null);

        // Then the session is in user schedule and set session starred is called with true
        verify(mMockSessionsHelper).setSessionStarred(eq(mMockUri), eq(true), anyString());
        assertThat(spyModel.isInSchedule(), is(true));
        assertThat(success, is(true));
    }

    @Test
    public void requestModelUpdate_UnstarSession_Success() {
        // Given a loaded session in user schedule
        initMockCursorWithSessionInSchedule(mMockCursor);
        SessionDetailModel spyModel = setSpyModelForSessionLoading();
        spyModel.readDataFromCursor(mMockCursor,
                SessionDetailModel.SessionDetailQueryEnum.SESSIONS);
        doNothing().when(spyModel).sendAnalyticsEvent(
                any(String.class), any(String.class), any(String.class));

        // When ran with unstar user action
        boolean success = spyModel.requestModelUpdate(
                SessionDetailModel.SessionDetailUserActionEnum.UNSTAR, null);

        // Then the session is not in user schedule and set session starred is called with false
        verify(mMockSessionsHelper).setSessionStarred(eq(mMockUri), eq(false), anyString());
        assertThat(spyModel.isInSchedule(), is(false));
        assertThat(success, is(true));
    }

    @Test
    public void requestModelUpdate_ShowMap_Success() {
        // Given a loaded session with a fake room id
        initMockCursorWithRoomId(mMockCursor);
        SessionDetailModel spyModel = setSpyModelForSessionLoading();
        spyModel.readDataFromCursor(mMockCursor,
                SessionDetailModel.SessionDetailQueryEnum.SESSIONS);
        doNothing().when(spyModel).sendAnalyticsEvent(
                any(String.class), any(String.class), any(String.class));

        // When ran with show map user action
        boolean success = spyModel.requestModelUpdate(
                SessionDetailModel.SessionDetailUserActionEnum.SHOW_MAP, null);

        // Then start map activity is called with the fake room id
        verify(mMockSessionsHelper).startMapActivity(eq(FAKE_ROOM_ID));
        assertThat(success, is(true));
    }

    @Test
    public void requestModelUpdate_ShowShare_Success() {
        // Given a loaded session with a fake title
        initMockCursorWithTitle(mMockCursor);
        SessionDetailModel spyModel = setSpyModelForSessionLoading();
        spyModel.readDataFromCursor(mMockCursor,
                SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

        // When ran with show share user action
        boolean success = spyModel.requestModelUpdate(
                SessionDetailModel.SessionDetailUserActionEnum.SHOW_SHARE, null);

        // Then share session is called with the fake title
        verify(mMockSessionsHelper).shareSession(any(Context.class), eq(R.string.share_template),
                eq(FAKE_TITLE), anyString(), anyString());
        assertThat(success, is(true));
    }

    @Test
    public void requestModelUpdate_InvalidUserAction_ReturnsFalse() {
        // When ran with an invalid user action
        boolean success = mSessionDetailModel.requestModelUpdate(InvalidEnum.INVALID, null);

        // Then false is returned
        assertThat(success, is(false));
    }

    @Test
    public void isSessionOngoing_OngoingSession_ReturnsTrue() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session in progress
            initMockCursorWithOngoingSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session is ongoing
            assertThat(spyModel.isSessionOngoing(), is(true));
        }
    }

    @Test
    public void isSessionOngoing_SessionNotStarted_ReturnsFalse() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session in future
            initMockCursorWithNotStartedSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session is not ongoing
            assertThat(spyModel.isSessionOngoing(), is(false));
        }
    }

    @Test
    public void isSessionOngoing_SessionEnded_ReturnsFalse() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session that has ended
            initMockCursorWithEndedSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session is not ongoing
            assertThat(spyModel.isSessionOngoing(), is(false));
        }
    }

    @Test
    public void hasSessionStarted_OnGoingSession_ReturnsTrue() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session in progress
            initMockCursorWithOngoingSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session has started
            assertThat(spyModel.hasSessionStarted(), is(true));
        }
    }

    @Test
    public void hasSessionStarted_SessionNotStarted_ReturnsFalse() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session in future
            initMockCursorWithNotStartedSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session has not started
            assertThat(spyModel.hasSessionStarted(), is(false));
        }
    }

    @Test
    public void hasSessionStarted_SessionEnded_ReturnsTrue() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session that has ended
            initMockCursorWithEndedSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session has started
            assertThat(spyModel.hasSessionStarted(), is(true));
        }
    }

    @Test
    public void hasSessionEnded_SessionNotStarted_ReturnsFalse() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session in progress
            initMockCursorWithNotStartedSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session has not ended
            assertThat(spyModel.hasSessionEnded(), is(false));
        }
    }

    @Test
    public void hasSessionEnded_OngoingSession_ReturnsFalse() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session in progress
            initMockCursorWithOngoingSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session has not ended
            assertThat(spyModel.hasSessionEnded(), is(false));
        }
    }

    @Test
    public void hasSessionEnded_SessionEnded_ReturnsTrue() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session that has ended
            initMockCursorWithEndedSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session has ended
            assertThat(spyModel.hasSessionEnded(), is(true));
        }
    }

    @Test
    public void minutesSinceSessionStarted_SessionNotStarted_ReturnsCorrectMinutes() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session not started
            initMockCursorWithNotStartedSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session is ongoing
            assertThat(spyModel.minutesSinceSessionStarted(), is(0l));
        }
    }

    @Test
    public void minutesSinceSessionStarted_SessionStarted_ReturnsCorrectMinutes() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session in progress that started 1 hour ago
            initMockCursorWithOngoingSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session is ongoing
            assertThat(spyModel.minutesSinceSessionStarted(), is(60l));
        }
    }

    @Test
    public void minutesUntilSessionStarts_SessionNotStarted_ReturnsCorrectMinutes() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session starting in 1 hour
            initMockCursorWithNotStartedSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session is ongoing
            assertThat(spyModel.minutesUntilSessionStarts(), is(60l));
        }
    }

    @Test
    public void minutesUntilSessionStarts_SessionStarted_ReturnsCorrectMinutes() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session in progress
            initMockCursorWithOngoingSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session is ongoing
            assertThat(spyModel.minutesUntilSessionStarts(), is(0l));
        }
    }

    @Test
    public void isSessionReadyForFeedback_SessionNotStarted_ReturnsFalse() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session not started
            initMockCursorWithNotStartedSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session is ongoing
            assertThat(spyModel.isSessionReadyForFeedback(), is(false));
        }
    }

    @Test
    public void isSessionReadyForFeedback_OnGoingSessionJustStarted_ReturnsFalse() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session in progress that started 1 minute ago and lasts 2 hours
            initMockCursorWithJustStartedSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session is ongoing
            assertThat(spyModel.isSessionReadyForFeedback(), is(false));
        }
    }

    @Test
    public void isSessionReadyForFeedback_OnGoingSessionNearEnd_ReturnsTrue() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session in progress that will end within feedback allowed time
            initMockCursorWithSessionEndingSoon(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session is ongoing
            assertThat(spyModel.isSessionReadyForFeedback(), is(true));
        }
    }

    @Test
    public void isSessionReadyForFeedback_SessionEnded_ReturnsTrue() {
        // Only possible to mock current time in debug build
        if (BuildConfig.DEBUG) {
            // Given a mock cursor for a session that has ended
            initMockCursorWithEndedSession(mMockCursor);
            SessionDetailModel spyModel = setSpyModelForSessionLoading();

            // When session is loaded
            spyModel.readDataFromCursor(mMockCursor,
                    SessionDetailModel.SessionDetailQueryEnum.SESSIONS);

            // Then session is ongoing
            assertThat(spyModel.isSessionReadyForFeedback(), is(true));
        }
    }

    private SessionDetailModel setSpyModelForSessionLoading() {
        SessionDetailModel spyModel = spy(
                new SessionDetailModel(mMockUri, mMockContext, mMockSessionsHelper));
        doNothing().when(spyModel).formatSubtitle();
        return spyModel;
    }

    private void initMockCursors() {
        // Set non empty cursor.
        when(mMockCursor.moveToFirst()).thenReturn(true);

        // Set session color to avoid call to mock context resources.
        when(mMockCursor.getColumnIndex(ScheduleContract.Sessions.SESSION_COLOR))
                .thenReturn(SESSION_COLOR_COLUMN_INDEX);
        when(mMockCursor.getInt(SESSION_COLOR_COLUMN_INDEX)).thenReturn(FAKE_COLOR);

        // Set empty cursor.
        when(mMockEmptyCursor.moveToFirst()).thenReturn(false);
    }

    private void initMockContextWithFakeCurrentTime() {
        when(mMockContext.getSharedPreferences("mock_data", Context.MODE_PRIVATE))
                .thenReturn(mMockSharedPreferences);
        when(mMockSharedPreferences.getLong("mock_current_time", eq(anyLong())))
                .thenReturn(FAKE_CURRENT_TIME_OFFSET);
    }

    private void initMockCursorWithTitle(Cursor cursor) {
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_TITLE))
                .thenReturn(SESSION_TITLE_COLUMN_INDEX);
        when(cursor.getString(SESSION_TITLE_COLUMN_INDEX)).thenReturn(FAKE_TITLE);
    }

    private void initMockCursorWithSessionNotInSchedule(Cursor cursor) {
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE))
                .thenReturn(SESSION_IN_MY_SCHEDULE_COLUMN_INDEX);
        when(cursor.getInt(SESSION_IN_MY_SCHEDULE_COLUMN_INDEX)).thenReturn(0);
    }

    private void initMockCursorWithSessionInSchedule(Cursor cursor) {
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE))
                .thenReturn(SESSION_IN_MY_SCHEDULE_COLUMN_INDEX);
        when(cursor.getInt(SESSION_IN_MY_SCHEDULE_COLUMN_INDEX)).thenReturn(1);
    }

    private void initMockCursorWithRoomId(Cursor cursor) {
        when(cursor.getColumnIndex(ScheduleContract.Sessions.ROOM_ID))
                .thenReturn(SESSION_ROOM_ID_COLUMN_INDEX);
        when(cursor.getString(SESSION_ROOM_ID_COLUMN_INDEX)).thenReturn(FAKE_ROOM_ID);
    }

    private void initMockCursorWithHashTag(Cursor cursor) {
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_HASHTAG))
                .thenReturn(SESSION_HASHTAG_COLUMN_INDEX);
        when(cursor.getString(SESSION_HASHTAG_COLUMN_INDEX)).thenReturn(FAKE_HASHTAG);
    }

    private void initMockCursorWithNotStartedSession(Cursor cursor) {
        // Return a fake start time in future
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_START))
                .thenReturn(SESSION_START_COLUMN_INDEX);
        when(cursor.getLong(SESSION_START_COLUMN_INDEX))
                .thenReturn(FAKE_CURRENT_TIME_OFFSET + ONE_HOUR);

        // Return a fake end time in future
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_END))
                .thenReturn(SESSION_END_COLUMN_INDEX);
        when(cursor.getLong(SESSION_END_COLUMN_INDEX))
                .thenReturn(FAKE_CURRENT_TIME_OFFSET + 2 * ONE_HOUR);
    }

    private void initMockCursorWithJustStartedSession(Cursor cursor) {
        // Return a fake start time 1 minute ago
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_START))
                .thenReturn(SESSION_START_COLUMN_INDEX);
        when(cursor.getLong(SESSION_START_COLUMN_INDEX))
                .thenReturn(FAKE_CURRENT_TIME_OFFSET - ONE_MINUTE);

        // Return a fake end time in future
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_END))
                .thenReturn(SESSION_END_COLUMN_INDEX);
        when(cursor.getLong(SESSION_END_COLUMN_INDEX))
                .thenReturn(FAKE_CURRENT_TIME_OFFSET + 2 * ONE_HOUR);
    }

    private void initMockCursorWithSessionEndingSoon(Cursor cursor) {
        // Return a fake start time in past
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_START))
                .thenReturn(SESSION_START_COLUMN_INDEX);
        when(cursor.getLong(SESSION_START_COLUMN_INDEX))
                .thenReturn(FAKE_CURRENT_TIME_OFFSET - ONE_HOUR);

        // Return a fake end time within feedback before session allowed time
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_END))
                .thenReturn(SESSION_END_COLUMN_INDEX);
        when(cursor.getLong(SESSION_END_COLUMN_INDEX)).thenReturn(FAKE_CURRENT_TIME_OFFSET
                + SessionDetailConstants.FEEDBACK_MILLIS_BEFORE_SESSION_END_MS - ONE_MINUTE);
    }

    private void initMockCursorWithOngoingSession(Cursor cursor) {
        // Return a fake start time in past
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_START))
                .thenReturn(SESSION_START_COLUMN_INDEX);
        when(cursor.getLong(SESSION_START_COLUMN_INDEX))
                .thenReturn(FAKE_CURRENT_TIME_OFFSET - ONE_HOUR);

        // Return a fake end time in future
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_END))
                .thenReturn(SESSION_END_COLUMN_INDEX);
        when(cursor.getLong(SESSION_END_COLUMN_INDEX))
                .thenReturn(FAKE_CURRENT_TIME_OFFSET + ONE_HOUR);
    }

    private void initMockCursorWithEndedSession(Cursor cursor) {
        // Return a fake start time in past
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_START))
                .thenReturn(SESSION_START_COLUMN_INDEX);
        when(cursor.getLong(SESSION_START_COLUMN_INDEX))
                .thenReturn(FAKE_CURRENT_TIME_OFFSET - 2 * ONE_HOUR);

        // Return a fake end time in past
        when(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_END))
                .thenReturn(SESSION_END_COLUMN_INDEX);
        when(cursor.getLong(SESSION_END_COLUMN_INDEX))
                .thenReturn(FAKE_CURRENT_TIME_OFFSET - ONE_HOUR);
    }

    private void initMockCursorWithOneSpeaker(Cursor cursor) {
        // Return a count of 1
        when(cursor.getCount()).thenReturn(1);
        when(cursor.moveToPosition(0)).thenReturn(true);

        // Return fake speaker details
        when(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_NAME))
                .thenReturn(SPEAKER_NAME_COLUMN_INDEX);
        when(cursor.getString(SPEAKER_NAME_COLUMN_INDEX)).thenReturn(FAKE_SPEAKER_NAME);
        when(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_IMAGE_URL))
                .thenReturn(SPEAKER_IMAGE_URL_COLUMN_INDEX);
        when(cursor.getString(SPEAKER_IMAGE_URL_COLUMN_INDEX)).thenReturn(FAKE_SPEAKER_IMAGE_URL);
        when(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_COMPANY))
                .thenReturn(SPEAKER_COMPANY_COLUMN_INDEX);
        when(cursor.getString(SPEAKER_COMPANY_COLUMN_INDEX)).thenReturn(FAKE_SPEAKER_COMPANY);
        when(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_URL))
                .thenReturn(SPEAKER_URL_COLUMN_INDEX);
        when(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_PLUSONE_URL))
                .thenReturn(SPEAKER_PLUSONE_URL_COLUMN_INDEX);
        when(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_TWITTER_URL))
                .thenReturn(SPEAKER_TWITTER_URL_COLUMN_INDEX);
        when(cursor.getString(SPEAKER_URL_COLUMN_INDEX)).thenReturn(FAKE_SPEAKER_URL);
        when(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_ABSTRACT))
                .thenReturn(SPEAKER_ABSTRACT_COLUMN_INDEX);
        when(cursor.getString(SPEAKER_ABSTRACT_COLUMN_INDEX)).thenReturn(FAKE_SPEAKER_ABSTRACT);
    }

    private void initMockCursorWithTwoSpeakers(Cursor cursor) {
        // Return a count of 2
        when(cursor.getCount()).thenReturn(2);
        when(cursor.moveToPosition(0)).thenReturn(true);
        when(cursor.moveToPosition(1)).thenReturn(true);

        // Return fake speaker details
        when(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_NAME))
                .thenReturn(SPEAKER_NAME_COLUMN_INDEX);
        when(cursor.getString(SPEAKER_NAME_COLUMN_INDEX)).thenReturn(FAKE_SPEAKER_NAME);
        when(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_IMAGE_URL))
                .thenReturn(SPEAKER_IMAGE_URL_COLUMN_INDEX);
        when(cursor.getString(SPEAKER_IMAGE_URL_COLUMN_INDEX)).thenReturn(FAKE_SPEAKER_IMAGE_URL);
        when(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_COMPANY))
                .thenReturn(SPEAKER_COMPANY_COLUMN_INDEX);
        when(cursor.getString(SPEAKER_COMPANY_COLUMN_INDEX)).thenReturn(FAKE_SPEAKER_COMPANY);
        when(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_URL))
                .thenReturn(SPEAKER_URL_COLUMN_INDEX);
        when(cursor.getString(SPEAKER_URL_COLUMN_INDEX)).thenReturn(FAKE_SPEAKER_URL);
        when(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_ABSTRACT))
                .thenReturn(SPEAKER_ABSTRACT_COLUMN_INDEX);
        when(cursor.getString(SPEAKER_ABSTRACT_COLUMN_INDEX)).thenReturn(FAKE_SPEAKER_ABSTRACT);
    }

    private void initMockCursorWithOneTag(Cursor cursor) {
        TagMetadataTest.initMockCursorWithOneTag(cursor);
    }
}
