/*
 * Copyright (C) 2016 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.leanback.app.wizard;

import static org.junit.Assert.assertTrue;

import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Build;
import android.view.KeyEvent;

import androidx.leanback.app.GuidedStepFragment;
import androidx.leanback.test.R;
import androidx.leanback.widget.GuidanceStylist;
import androidx.leanback.widget.GuidedAction;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

@LargeTest
@RunWith(AndroidJUnit4.class)
public class GuidedStepAttributesTest {
    static final long TRANSITION_LENGTH = 1000;

    static final String TAG = "GuidedStepAttributesTest";

    @Rule
    public ActivityTestRule<GuidedStepAttributesTestActivity> activityTestRule =
            new ActivityTestRule<>(GuidedStepAttributesTestActivity.class, false, false);

    GuidedStepAttributesTestActivity mActivity;

    private void initActivity(Intent intent) {
        mActivity = activityTestRule.launchActivity(intent);
        try {
            Thread.sleep(2000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }

    Context mContext;
    @Before
    public void setUp() {
        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();;
    }

    public static void sendKey(int keyCode) {
        InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode);
    }

    @Test
    public void testFocusDisabledOnActions() throws Throwable {

        Intent intent = new Intent();
        Resources res = mContext.getResources();

        final int NUM_SEARCH_ACTIONS = 10;
        final List<Integer> ACTIONS_WITH_DISABLED_FOCUS = new ArrayList<>(
                Arrays.asList(1, 3, 4, 5, 8));
        final int ACTION_ID_SEARCH = 1;
        List<Integer> EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT = new ArrayList<>();

        // we will traverse actions from top to bottom and then back to the top
        for(int i = 0; i < NUM_SEARCH_ACTIONS; i++) {
            if (!ACTIONS_WITH_DISABLED_FOCUS.contains(i))
                EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT.add(i);
        }
        for(int i = EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT.size(); i-- != 0;) {
            EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT.add(EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT.get(i));
        }


        String title = "Guided Actions Focusable Test";
        String breadcrumb = "Focusable Test Demo";
        String description = "";
        GuidanceStylist.Guidance guidance = new GuidanceStylist.Guidance(title, description,
                breadcrumb, null);

        List<GuidedAction> actionList = new ArrayList<>();
        for (int i = 0; i < NUM_SEARCH_ACTIONS; i++ ) {
            actionList.add(new GuidedAction.Builder(mContext)
                    .id(ACTION_ID_SEARCH)
                    .title(res.getString(R.string.search) + "" + i)
                    .description(res.getString(R.string.search_description) + i)
                    .build()
            );
        }
        for(int action_id : ACTIONS_WITH_DISABLED_FOCUS )
            actionList.get(action_id).setFocusable(false);

        GuidedStepAttributesTestFragment.GUIDANCE = guidance;
        GuidedStepAttributesTestFragment.ACTION_LIST = actionList;

        initActivity(intent);

        int lastSelectedActionId = -1;
        int selectIndex = 0;
        GuidedStepFragment mFragment = (GuidedStepFragment) mActivity.getGuidedStepTestFragment();
        int prevSelectedActionPosition = -1;
        int nextSelectedActionPosition = mFragment.getSelectedActionPosition();
        while ( nextSelectedActionPosition != prevSelectedActionPosition ) {
            lastSelectedActionId = mFragment.getSelectedActionPosition();
            assertTrue(res.getString(R.string.focusable_test_error_message,
                    actionList.get(lastSelectedActionId).getTitle()),
                    lastSelectedActionId == EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT.get(selectIndex));
            selectIndex++;
            sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
            prevSelectedActionPosition = nextSelectedActionPosition;
            nextSelectedActionPosition = mFragment.getSelectedActionPosition();
            Thread.sleep(TRANSITION_LENGTH);
        }

        prevSelectedActionPosition = -1;
        while ( nextSelectedActionPosition != prevSelectedActionPosition ) {
            lastSelectedActionId = mFragment.getSelectedActionPosition();
            assertTrue(res.getString(R.string.focusable_test_error_message,
                    actionList.get(lastSelectedActionId).getTitle()),
                    lastSelectedActionId == EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT.get(selectIndex));
            selectIndex++;
            sendKey(KeyEvent.KEYCODE_DPAD_UP);
            prevSelectedActionPosition = nextSelectedActionPosition;
            nextSelectedActionPosition = mFragment.getSelectedActionPosition();
            Thread.sleep(TRANSITION_LENGTH);
        }

    }

    // Note: do not remove final or sRevertCallback gets null from 2nd test on!
     static final GuidedStepAttributesTestFragment.Callback sRevertCallback = new
            GuidedStepAttributesTestFragment.Callback() {
        @Override
        public void onActionClicked(GuidedStepFragment fragment, long id) {
            List<GuidedAction> allActions = fragment.getActions();
            for(int i = 1; i < allActions.size(); i++) {
                GuidedAction action = allActions.get(i);
                action.setEnabled(!action.isEnabled());
                fragment.notifyActionChanged(fragment.findActionPositionById(action.getId()));
            }
        }
    };

    /**
     * Creates a number of enabled and disable actions and tests whether the flag is correctly set
     * by clicking on each individual action and checking whether the click event is triggered.
     * @throws Throwable
     */
    @Test
    public void testDisabledActions() throws Throwable {

        Intent intent = new Intent();
        Resources res = mContext.getResources();

        final int NUM_SEARCH_ACTIONS = 10;
        final List<Integer> DISABLED_ACTIONS = new ArrayList<>(
                Arrays.asList(1, 3, 5, 7));
        final int ACTION_ID_REVERT_BUTTON = 0;
        final int ACTION_ID_SEARCH_BEGIN = ACTION_ID_REVERT_BUTTON + 1;
        int ACTION_ID_SEARCH_END = ACTION_ID_SEARCH_BEGIN;

        // sequence of clicked actions simulated in the test
        List<Integer> CLICK_SEQUENCE = new ArrayList<>();

        // Expected Clicked sequence can be different from focused ones since some of the actions
        // are disabled hence not clickable
        List<Integer> EXPECTED_FOCUSED_SEQUENCE = new ArrayList<>();
        List<Integer> EXPECTED_CLICKED_SEQUENCE = new ArrayList<>();
        // Expected actions state according to list of DISABLED_ACTIONS: false for disabled actions
        List<Boolean> EXPECTED_ACTIONS_STATE = new ArrayList<>(
                Arrays.asList(new Boolean[NUM_SEARCH_ACTIONS])
        );
        Collections.fill(EXPECTED_ACTIONS_STATE, Boolean.TRUE);

        for(int i = 0; i < NUM_SEARCH_ACTIONS; i++) {
            CLICK_SEQUENCE.add(i + 1);
        }
        for(int clickedActionId : CLICK_SEQUENCE) {
            EXPECTED_FOCUSED_SEQUENCE.add(clickedActionId);
            if (!DISABLED_ACTIONS.contains(clickedActionId - 1))
                EXPECTED_CLICKED_SEQUENCE.add(clickedActionId);
            else
                EXPECTED_CLICKED_SEQUENCE.add(-1);
        }

        String title = "Guided Actions Enabled Test";
        String breadcrumb = "Enabled Test Demo";
        String description = "";
        GuidanceStylist.Guidance guidance = new GuidanceStylist.Guidance(title, description,
                breadcrumb, null);

        List<GuidedAction> actionList = new ArrayList<>();
        actionList.add(new GuidedAction.Builder(mContext)
                .id(ACTION_ID_REVERT_BUTTON)
                .title(res.getString(R.string.invert_title))
                .description(res.getString(R.string.revert_description))
                .build()
        );

        for (int i = 0; i < NUM_SEARCH_ACTIONS; i++ ) {
            actionList.add(new GuidedAction.Builder(mContext)
                    .id(ACTION_ID_SEARCH_END++)
                    .title(res.getString(R.string.search) + "" + i)
                    .description(res.getString(R.string.search_description) + i)
                    .build()
            );
        }
        for(int action_id : DISABLED_ACTIONS ) {
            if ( action_id >= 0 && action_id < NUM_SEARCH_ACTIONS ) {
                actionList.get(action_id + 1).setEnabled(false);
                EXPECTED_ACTIONS_STATE.set(action_id, Boolean.FALSE);
            }
        }

        GuidedStepAttributesTestFragment.clear();
        GuidedStepAttributesTestFragment.GUIDANCE = guidance;
        GuidedStepAttributesTestFragment.ACTION_LIST = actionList;
        GuidedStepAttributesTestFragment.setActionClickCallback(ACTION_ID_REVERT_BUTTON,
                sRevertCallback);

        initActivity(intent);

        examineEnabledAndDisabledActions(actionList, CLICK_SEQUENCE, EXPECTED_FOCUSED_SEQUENCE,
                EXPECTED_CLICKED_SEQUENCE);
    }

    /**
     * Toggles Enabled flags in oll the actions of the prior test, and tests whether they are
     * correctly reverted.
     */
    @Test
    public void testToggleEnabledFlags() throws Throwable {

        Intent intent = new Intent();
        Resources res = mContext.getResources();

        final int NUM_SEARCH_ACTIONS = 10;
        final List<Integer> DISABLED_ACTIONS = new ArrayList<>(
                Arrays.asList(1, 3, 5, 7));
        final int ACTION_ID_REVERT_BUTTON = 0;
        final int ACTION_ID_SEARCH_BEGIN = ACTION_ID_REVERT_BUTTON + 1;
        int ACTION_ID_SEARCH_END = ACTION_ID_SEARCH_BEGIN;

        // sequence of clicked actions simulated in the test
        List<Integer> CLICK_SEQUENCE = new ArrayList<>();

        // Expected Clicked sequence can be different from focused ones since some of the actions
        // are disabled hence not clickable
        List<Integer> EXPECTED_FOCUSED_SEQUENCE = new ArrayList<>();
        List<Integer> EXPECTED_CLICKED_SEQUENCE = new ArrayList<>();
        // Expected actions state according to list of DISABLED_ACTIONS: false for disabled actions
        List<Boolean> EXPECTED_ACTIONS_STATE = new ArrayList<>(
                Arrays.asList(new Boolean[NUM_SEARCH_ACTIONS])
        );
        Collections.fill(EXPECTED_ACTIONS_STATE, Boolean.FALSE);

        for(int i = 0; i < NUM_SEARCH_ACTIONS; i++) {
            CLICK_SEQUENCE.add(i + 1);
        }
        for(int clickedActionId : CLICK_SEQUENCE) {
            EXPECTED_FOCUSED_SEQUENCE.add(clickedActionId);
            if (DISABLED_ACTIONS.contains(clickedActionId - 1))
                EXPECTED_CLICKED_SEQUENCE.add(clickedActionId);
            else
                EXPECTED_CLICKED_SEQUENCE.add(-1);
        }

        String title = "Guided Actions Enabled Test";
        String breadcrumb = "Toggle Enabled Flag Test Demo";
        String description = "";
        GuidanceStylist.Guidance guidance = new GuidanceStylist.Guidance(title, description,
                breadcrumb, null);

        List<GuidedAction> actionList = new ArrayList<>();
        actionList.add(new GuidedAction.Builder(mContext)
                .id(ACTION_ID_REVERT_BUTTON)
                .title(res.getString(R.string.invert_title))
                .description(res.getString(R.string.revert_description))
                .build()
        );

        for (int i = 0; i < NUM_SEARCH_ACTIONS; i++ ) {
            actionList.add(new GuidedAction.Builder(mContext)
                    .id(ACTION_ID_SEARCH_END++)
                    .title(res.getString(R.string.search) + "" + i)
                    .description(res.getString(R.string.search_description) + i)
                    .build()
            );
        }
        for(int action_id : DISABLED_ACTIONS ) {
            if ( action_id >= 0 && action_id < NUM_SEARCH_ACTIONS ) {
                actionList.get(action_id + 1).setEnabled(false);
                EXPECTED_ACTIONS_STATE.set(action_id, Boolean.TRUE);
            }
        }

        GuidedStepAttributesTestFragment.clear();
        GuidedStepAttributesTestFragment.GUIDANCE = guidance;
        GuidedStepAttributesTestFragment.ACTION_LIST = actionList;
        GuidedStepAttributesTestFragment.setActionClickCallback(ACTION_ID_REVERT_BUTTON,
                sRevertCallback);

        initActivity(intent);

        final GuidedStepFragment mFragment = (GuidedStepFragment)
                mActivity.getGuidedStepTestFragment();

        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mFragment.setSelectedActionPosition(0);
            }
        });
        Thread.sleep(TRANSITION_LENGTH);
        sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
        Thread.sleep(TRANSITION_LENGTH);
        examineEnabledAndDisabledActions(actionList, CLICK_SEQUENCE, EXPECTED_FOCUSED_SEQUENCE,
                EXPECTED_CLICKED_SEQUENCE);
    }

    private void examineEnabledAndDisabledActions(
            List<GuidedAction> actionList, List<Integer> CLICK_SEQUENCE,
                                List<Integer> EXPECTED_FOCUSED_SEQUENCE,
                                List<Integer> EXPECTED_CLICKED_SEQUENCE)
            throws Throwable {

        final GuidedStepFragment mFragment = (GuidedStepFragment)
                mActivity.getGuidedStepTestFragment();

        for(int i = 0; i < CLICK_SEQUENCE.size(); i++) {
            GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID =
                    GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID = -1;
            final int id = CLICK_SEQUENCE.get(i);
            mActivity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mFragment.setSelectedActionPosition(id);
                }
            });
            Thread.sleep(TRANSITION_LENGTH);

            sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
            Thread.sleep(TRANSITION_LENGTH);

            assertTrue(mContext.getResources().getString(
                    R.string.enabled_test_wrong_focus_error_message),
                    GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID
                            == EXPECTED_FOCUSED_SEQUENCE.get(i)
            );
            assertTrue(mContext.getResources().getString(
                    R.string.enabled_test_wrong_click_error_message),
                    GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID
                            == EXPECTED_CLICKED_SEQUENCE.get(i)
            );
            assertTrue(mContext.getResources().getString(
                    R.string.enabled_test_wrong_flag_error_message),
                    (GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID == -1)
                            ? !actionList.get(id).isEnabled()
                            : actionList.get(id).isEnabled()
            );
        }
    }

    @Ignore // b/263796240
    @Test
    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
    public void testCheckedActions() throws Throwable {
        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
            return; // b/262909049: Do not run this test on pre-release Android U.
        }


        Intent intent = new Intent();
        Resources res = mContext.getResources();

        final int NUM_RADIO_ACTIONS = 3;
        final int NUM_CHECK_BOX_ACTIONS = 3;
        final int INITIALLY_CHECKED_RADIO_ACTION = 0;
        final List<Integer> INITIALLY_CHECKED_CHECKBOX_ACTIONS = new ArrayList<>(
                Arrays.asList(1, 2)
        );

        List<Integer> CLICK_SEQUENCE = new ArrayList<>();
        for(int i = 0; i < NUM_RADIO_ACTIONS + NUM_CHECK_BOX_ACTIONS; i++) {
            CLICK_SEQUENCE.add(i);
        }

        List<Boolean> EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK = new ArrayList<>(
                Arrays.asList(new Boolean[CLICK_SEQUENCE.size()])
        );
        Collections.fill(EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK, Boolean.FALSE);

        // initial state of actions before any clicks happen
        EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.set(INITIALLY_CHECKED_RADIO_ACTION, true);
        for(int checkedCheckBox : INITIALLY_CHECKED_CHECKBOX_ACTIONS) {
            EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.set(NUM_RADIO_ACTIONS + checkedCheckBox, true);
        }

        String title = "Guided Actions Checked Test";
        String breadcrumb = "Checked Test Demo";
        String description = "";
        GuidanceStylist.Guidance guidance = new GuidanceStylist.Guidance(title, description,
                breadcrumb, null);

        List<GuidedAction> actionList = new ArrayList<>();
        actionList.add(new GuidedAction.Builder(mContext)
                .title(res.getString(R.string.radio_actions_info_title))
                .description(res.getString(R.string.radio_actions_info_desc))
                .infoOnly(true)
                .enabled(true)
                .focusable(false)
                .build()
        );

        int firstRadioActionIndex = actionList.size();
        for(int i = 0; i < NUM_RADIO_ACTIONS; i++) {
            actionList.add(new GuidedAction.Builder(mContext)
                    .title(res.getString(R.string.checkbox_title) + i)
                    .description(res.getString(R.string.checkbox_desc) + i)
                    .checkSetId(GuidedAction.DEFAULT_CHECK_SET_ID)
                    .build()
            );
            if (i == INITIALLY_CHECKED_RADIO_ACTION)
                actionList.get(firstRadioActionIndex + i).setChecked(true);
        }

        actionList.add(new GuidedAction.Builder(mContext)
                .title(res.getString(R.string.checkbox_actions_info_title))
                .description(res.getString(R.string.checkbox_actions_info_desc))
                .infoOnly(true)
                .enabled(true)
                .focusable(false)
                .build()
        );
        int firstCheckBoxActionIndex = actionList.size();
        for(int i = 0; i < NUM_CHECK_BOX_ACTIONS; i++) {
            actionList.add(new GuidedAction.Builder(mContext)
                    .title(res.getString(R.string.checkbox_title) + i)
                    .description(res.getString(R.string.checkbox_desc) + i)
                    .checkSetId(GuidedAction.CHECKBOX_CHECK_SET_ID)
                    .build()
            );
        }
        for(int i = 0; i < INITIALLY_CHECKED_CHECKBOX_ACTIONS.size(); i++ ) {
            actionList.get(firstCheckBoxActionIndex + INITIALLY_CHECKED_CHECKBOX_ACTIONS.get(i))
                    .setChecked(true);
        }

        GuidedStepAttributesTestFragment.GUIDANCE = guidance;
        GuidedStepAttributesTestFragment.ACTION_LIST = actionList;
        initActivity(intent);

        examineCheckedAndUncheckedActions(actionList, EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK,
                NUM_RADIO_ACTIONS, NUM_CHECK_BOX_ACTIONS);
    }

    private void updateExpectedActionsStateAfterClick(
            List<Boolean> EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK, int NUM_RADIO_ACTIONS,
            int NUM_CHECK_BOX_ACTIONS, int clickedActionIndex) {

        if (clickedActionIndex < NUM_RADIO_ACTIONS) {
            for(int i = 0; i < NUM_RADIO_ACTIONS; i++)
                EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.set(i, false);
            EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.set(clickedActionIndex, true);
        }
        else if (clickedActionIndex < NUM_RADIO_ACTIONS + NUM_CHECK_BOX_ACTIONS) {
            EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.set(clickedActionIndex,
                    !EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.get(clickedActionIndex));
        }
    }

    private void verifyIfActionsStateIsCorrect(List<GuidedAction> actionList,
            List<Boolean> EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK) {

        int actionIndex = 0;
        for(GuidedAction checkAction : actionList) {
            if (checkAction.infoOnly())
                continue;
            assertTrue("Action " + actionIndex + " is " + (!checkAction.isChecked() ? "un-" : "")
                    + "checked while it shouldn't be!", checkAction.isChecked()
                            == EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.get(actionIndex));
            actionIndex++;
        }
    }

    private void examineCheckedAndUncheckedActions(List<GuidedAction> actionList,
                                List<Boolean> EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK,
                                int NUM_RADIO_ACTIONS,
                                int NUM_CHECK_BOX_ACTIONS) throws Throwable {

        final GuidedStepFragment guidedStepCheckedFragment = (GuidedStepFragment)
                mActivity.getGuidedStepTestFragment();
        final int firstRadioActionIndex = 1;
        final int firstCheckBoxActionIndex = firstRadioActionIndex + NUM_RADIO_ACTIONS + 1;
        for(int actionId = 0; actionId < NUM_RADIO_ACTIONS; actionId++) {
            final int id = actionId;
            mActivity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    guidedStepCheckedFragment
                            .setSelectedActionPosition(firstRadioActionIndex + id);
                }
            });
            Thread.sleep(TRANSITION_LENGTH);

            sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
            Thread.sleep(TRANSITION_LENGTH);
            updateExpectedActionsStateAfterClick(EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK,
                    NUM_RADIO_ACTIONS, NUM_CHECK_BOX_ACTIONS, actionId);
            verifyIfActionsStateIsCorrect(actionList, EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK);
        }

        for(int actionId = 0; actionId < NUM_CHECK_BOX_ACTIONS; actionId++) {
            final int id = actionId;
            mActivity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    guidedStepCheckedFragment
                            .setSelectedActionPosition(firstCheckBoxActionIndex + id);
                }
            });
            Thread.sleep(TRANSITION_LENGTH);

            sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
            Thread.sleep(TRANSITION_LENGTH);
            updateExpectedActionsStateAfterClick(EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK,
                    NUM_RADIO_ACTIONS, NUM_CHECK_BOX_ACTIONS, NUM_RADIO_ACTIONS + actionId);
            verifyIfActionsStateIsCorrect(actionList, EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK);
        }
    }

    @Test
    public void testActionWithTwoSubActions() throws Throwable {
        ExpectedSubActionResult result = setUpActionsForSubActionsTest();

        final int actionPos = 0;
        final GuidedAction selectedAction = result.actionList.get(actionPos);
        List<Integer> expectedFocusedSeq = result.expectedFocusedSeq.get(actionPos);
        List<Integer> expectedClickedSeq = result.expectedClickedSeq.get(actionPos);

        traverseSubActionsAndVerifyFocusAndClickEvents(selectedAction, actionPos, expectedFocusedSeq,
                expectedClickedSeq);
    }

    @Test
    public void testActionWithOneSubAction() throws Throwable {
        ExpectedSubActionResult result = setUpActionsForSubActionsTest();

        final int actionPos = 1;
        final GuidedAction selectedAction = result.actionList.get(actionPos);
        List<Integer> expectedFocusedSeq = result.expectedFocusedSeq.get(actionPos);
        List<Integer> expectedClickedSeq = result.expectedClickedSeq.get(actionPos);

        traverseSubActionsAndVerifyFocusAndClickEvents(selectedAction, actionPos, expectedFocusedSeq,
                expectedClickedSeq);
    }

    @Test
    public void testActionWithZeroSubActions() throws Throwable {
        ExpectedSubActionResult result = setUpActionsForSubActionsTest();

        final int actionPos = 2;
        final GuidedAction selectedAction = result.actionList.get(actionPos);
        List<Integer> expectedFocusedSeq = result.expectedFocusedSeq.get(actionPos);
        List<Integer> expectedClickedSeq = result.expectedClickedSeq.get(actionPos);

        traverseSubActionsAndVerifyFocusAndClickEvents(selectedAction, actionPos, expectedFocusedSeq,
                expectedClickedSeq);
    }

    @Test
    public void testActionWithThreeSubActions() throws Throwable {
        ExpectedSubActionResult result = setUpActionsForSubActionsTest();

        final int actionPos = 3;
        final GuidedAction selectedAction = result.actionList.get(actionPos);
        List<Integer> expectedFocusedSeq = result.expectedFocusedSeq.get(actionPos);
        List<Integer> expectedClickedSeq = result.expectedClickedSeq.get(actionPos);

        traverseSubActionsAndVerifyFocusAndClickEvents(selectedAction, actionPos, expectedFocusedSeq,
                expectedClickedSeq);
    }

    /**
     * Traverses the list of sub actions of a gudied action. It also verifies the correct action
     * or sub action is focused or clicked as the traversal is performed.
     * @param selectedAction The action of interest
     * @param actionPos The position of selectedAction within the array of guidedactions
     * @param expectedFocusedSeq The actual actions IDs used as a reference to verify focused actions
     * @param expectedClickedSeq The actual action IDs used as a reference to verify clicked actions
     * @throws Throwable
     */
    private void traverseSubActionsAndVerifyFocusAndClickEvents(GuidedAction selectedAction,
                                                                int actionPos,
                                                                List<Integer> expectedFocusedSeq,
                                                                List<Integer> expectedClickedSeq)
            throws Throwable{

        final GuidedStepFragment mFragment =
                (GuidedStepFragment) mActivity.getGuidedStepTestFragment();
        int focusStep = 0, clickStep = 0;
        GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID =
                GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID = -1;


        final int pos = actionPos;
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mFragment.setSelectedActionPosition(pos);
            }
        });
        Thread.sleep(TRANSITION_LENGTH);

        if (mFragment.getSelectedActionPosition() != actionPos) {
            assertTrue(mContext.getResources().getString(
                    R.string.subaction_test_wrong_focus_error_message),
                    GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID
                            == expectedFocusedSeq.get(focusStep++)
            );
        } else {
            // If the currently focused position is the same as the position of the action of interest,
            // then GuidedStepFragment won't received onGuidedActionFocused callback. Since the first
            // element in the expectedFocusSeq is always the id of this action, we need to move focusStep
            // one step forward.
            focusStep++;
        }
        if (selectedAction.hasSubActions()) {
            // Following for loop clicks on a specific action and scrolls & clicks through
            // all its subactions
            for (int j = 0; j < selectedAction.getSubActions().size(); j++) {
                sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
                Thread.sleep(TRANSITION_LENGTH);
                assertTrue(mContext.getResources().getString(
                        R.string.subaction_test_wrong_focus_error_message),
                        GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID
                                == expectedFocusedSeq.get(focusStep++)
                );
                assertTrue(mContext.getResources().getString(
                        R.string.subaction_test_wrong_click_error_message),
                        GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID
                                == expectedClickedSeq.get(clickStep++)
                );

                for (int k = 0; k < j; k++) {
                    sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
                    Thread.sleep(TRANSITION_LENGTH);
                    assertTrue(mContext.getResources().getString(
                            R.string.subaction_test_wrong_focus_error_message),
                            GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID
                                    == expectedFocusedSeq.get(focusStep++)
                    );
                }
                sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
                Thread.sleep(TRANSITION_LENGTH);

                assertTrue(mContext.getResources().getString(
                        R.string.subaction_test_wrong_focus_error_message),
                        GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID
                                == expectedFocusedSeq.get(focusStep++)
                );
                assertTrue(mContext.getResources().getString(
                        R.string.subaction_test_wrong_click_error_message),
                        GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID
                                == expectedClickedSeq.get(clickStep++)
                );
            }
        } else {
            sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
            Thread.sleep(TRANSITION_LENGTH);
            assertTrue(mContext.getResources().getString(
                    R.string.subaction_test_wrong_focus_error_message),
                    GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID
                            == expectedFocusedSeq.get(focusStep++)
            );
            assertTrue(mContext.getResources().getString(
                    R.string.subaction_test_wrong_click_error_message),
                    GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID
                            == expectedClickedSeq.get(clickStep++)
            );
        }
    }

    static class ExpectedSubActionResult {
        List<List<Integer>> expectedFocusedSeq; // Expected sequence of action (or subaction) ids to receive focus events;
        // Each entry corresponds to an action item in the guidedactions pane
        List<List<Integer>> expectedClickedSeq; // Expected sequence of action (or subaction) ids to receive click events;
        // Each entry corresponds to an action item in the guidedactions pane
        List<GuidedAction> actionList;          // List of GuidedActions in the guidedactions pane
    }

    /**
     * Populates a sample list of actions and subactions in the guidedactions pane.
     * @return  An object holding the expected sequence of action and subactions IDs that receive
     * focus and click events as well as the list of GuidedActions.
     */
    private ExpectedSubActionResult setUpActionsForSubActionsTest() {
        Intent intent = new Intent();
        Resources res = mContext.getResources();

        ExpectedSubActionResult result = new ExpectedSubActionResult();
        result.expectedFocusedSeq = new ArrayList<>();
        result.expectedClickedSeq = new ArrayList<>();

        final int NUM_REGULAR_ACTIONS = 4;
        final int[] NUM_SUBACTIONS_PER_ACTION = {2, 1, 0, 3};
        final int[] REGULAR_ACTIONS_INDEX =  new int[NUM_REGULAR_ACTIONS];
        final int[] BEGIN_SUBACTION_INDEX_PER_ACTION = new int[NUM_REGULAR_ACTIONS];
        final int[] END_SUBACTION_INDEX_PER_ACTION = new int[NUM_REGULAR_ACTIONS];
        // Actions and SubActions are assigned unique sequential IDs
        int lastIndex = 0;
        for(int i = 0; i < NUM_REGULAR_ACTIONS; i++) {
            REGULAR_ACTIONS_INDEX[i] = lastIndex;
            lastIndex++;
            BEGIN_SUBACTION_INDEX_PER_ACTION[i] = lastIndex;
            END_SUBACTION_INDEX_PER_ACTION[i] = (lastIndex += NUM_SUBACTIONS_PER_ACTION[i]);
        }

        for (int i = 0; i < NUM_REGULAR_ACTIONS; i++) {
            List<Integer> expectedFocusSeqForEachAction = new ArrayList<>();
            List<Integer> expectedClickedSeqForEachAction = new ArrayList<>();
            expectedFocusSeqForEachAction.add(REGULAR_ACTIONS_INDEX[i]);

            if (NUM_SUBACTIONS_PER_ACTION[i] > 0) {
                for (int j = BEGIN_SUBACTION_INDEX_PER_ACTION[i];
                        j < END_SUBACTION_INDEX_PER_ACTION[i]; j++) {
                    expectedClickedSeqForEachAction.add(REGULAR_ACTIONS_INDEX[i]);
                    for (int k = BEGIN_SUBACTION_INDEX_PER_ACTION[i]; k <= j; k++) {
                        expectedFocusSeqForEachAction.add(k);
                    }
                    expectedClickedSeqForEachAction.add(j);
                    expectedFocusSeqForEachAction.add(REGULAR_ACTIONS_INDEX[i]);
                }
            } else {
                expectedClickedSeqForEachAction.add(REGULAR_ACTIONS_INDEX[i]);
                expectedFocusSeqForEachAction.add(REGULAR_ACTIONS_INDEX[i]);
            }
            result.expectedFocusedSeq.add(expectedFocusSeqForEachAction);
            result.expectedClickedSeq.add(expectedClickedSeqForEachAction);
        }

        String title = "Guided SubActions Test";
        String breadcrumb = "SubActions Test Demo";
        String description = "";
        GuidanceStylist.Guidance guidance = new GuidanceStylist.Guidance(title, description,
                breadcrumb, null);

        List<GuidedAction> actionList = new ArrayList<>();

        lastIndex = 0;
        for (int i = 0; i < NUM_REGULAR_ACTIONS; i++ ) {
            GuidedAction action = new GuidedAction.Builder(mContext)
                    .id(lastIndex++)
                    .title(res.getString(R.string.dropdown_action_title, i))
                    .description(res.getString(R.string.dropdown_action_desc, i))
                    .build();
            if (NUM_SUBACTIONS_PER_ACTION[i] > 0) {
                List<GuidedAction> subActions = new ArrayList<>();
                action.setSubActions(subActions);
                for(int j = 0; j < NUM_SUBACTIONS_PER_ACTION[i]; j++) {
                    subActions.add(new GuidedAction.Builder(mContext)
                            .id(lastIndex++)
                            .title(res.getString(R.string.subaction_title, j))
                            .description("")
                            .build()
                    );
                }
            }
            actionList.add(action);
        }
        result.actionList = actionList;

        GuidedStepAttributesTestFragment.clear();
        GuidedStepAttributesTestFragment.GUIDANCE = guidance;
        GuidedStepAttributesTestFragment.ACTION_LIST = actionList;

        initActivity(intent);
        return result;
    }
}
