/*
 * Copyright (C) 2019 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 android.server.wm.intent;

import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY;
import static android.content.Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP;
import static android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT;
import static android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static android.server.wm.intent.LaunchSequence.intent;
import static android.server.wm.intent.LaunchSequence.intentForResult;
import static android.server.wm.intent.Persistence.flag;

import android.content.Intent;
import android.server.wm.intent.Activities.RegularActivity;
import android.server.wm.intent.Persistence.IntentFlag;
import android.server.wm.intent.Persistence.LaunchIntent;

import com.google.common.collect.Lists;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Contains all information to create and reuse intent test launches.
 * It enumerates all the flags so they can easily be used.
 *
 * It also stores commonly used {@link LaunchSequence} objects for reuse.
 */
public class Cases {

    public static final IntentFlag CLEAR_TASK = flag(FLAG_ACTIVITY_CLEAR_TASK,
            "FLAG_ACTIVITY_CLEAR_TASK");
    public static final IntentFlag CLEAR_TOP = flag(FLAG_ACTIVITY_CLEAR_TOP,
            "FLAG_ACTIVITY_CLEAR_TOP");
    private static final IntentFlag SINGLE_TOP = flag(FLAG_ACTIVITY_SINGLE_TOP,
            "FLAG_ACTIVITY_SINGLE_TOP");
    public static final IntentFlag NEW_TASK = flag(FLAG_ACTIVITY_NEW_TASK,
            "FLAG_ACTIVITY_NEW_TASK");
    public static final IntentFlag NEW_DOCUMENT = flag(FLAG_ACTIVITY_NEW_DOCUMENT,
            "FLAG_ACTIVITY_NEW_DOCUMENT");
    private static final IntentFlag MULTIPLE_TASK = flag(FLAG_ACTIVITY_MULTIPLE_TASK,
            "FLAG_ACTIVITY_MULTIPLE_TASK");
    public static final IntentFlag RESET_TASK_IF_NEEDED = flag(
            FLAG_ACTIVITY_RESET_TASK_IF_NEEDED,
            "FLAG_ACTIVITY_RESET_TASK_IF_NEEDED");
    public static final IntentFlag PREVIOUS_IS_TOP = flag(FLAG_ACTIVITY_PREVIOUS_IS_TOP,
            "FLAG_ACTIVITY_PREVIOUS_IS_TOP");
    public static final IntentFlag REORDER_TO_FRONT = flag(FLAG_ACTIVITY_REORDER_TO_FRONT,
            "FLAG_ACTIVITY_REORDER_TO_FRONT");
    public static final IntentFlag NO_HISTORY = flag(FLAG_ACTIVITY_NO_HISTORY,
            "FLAG_ACTIVITY_NO_HISTORY");

    // Flag only used for parsing intents that contain no flags.
    private static final IntentFlag NONE = flag(0, "");

    public final List<IntentFlag> flags = Lists.newArrayList(
            CLEAR_TASK,
            CLEAR_TOP,
            SINGLE_TOP,
            NEW_TASK,
            NEW_DOCUMENT,
            MULTIPLE_TASK,
            RESET_TASK_IF_NEEDED,
            PREVIOUS_IS_TOP,
            REORDER_TO_FRONT,
            NO_HISTORY
    );

    // Definition of intents used across multiple test cases.
    private final LaunchIntent mRegularIntent = intent(RegularActivity.class);
    private final LaunchIntent mSingleTopIntent = intent(Activities.SingleTopActivity.class);
    private final LaunchIntent mAff1Intent = intent(Activities.TaskAffinity1Activity.class);
    private final LaunchIntent mSecondAff1Intent = intent(Activities.TaskAffinity1Activity2.class);
    private final LaunchIntent mSingleInstanceIntent = intent(
            Activities.SingleInstanceActivity.class);
    private final LaunchIntent mSingleTaskIntent = intent(Activities.SingleTaskActivity.class);
    private final LaunchIntent mRegularForResultIntent = intentForResult(RegularActivity.class);

    // LaunchSequences used across multiple test cases.
    private final LaunchSequence mRegularSequence = LaunchSequence.create(mRegularIntent);

    // To show that the most recent task get's resumed by new task
    private final LaunchSequence mTwoAffinitiesSequence =
            LaunchSequence.create(mAff1Intent)
                    .append(mRegularIntent)
                    .append(mAff1Intent.withFlags(NEW_TASK, MULTIPLE_TASK));

    // Used to show that the task affinity is determined by the activity that started it,
    // Not the affinity of the  current root activity.
    private final LaunchSequence mRearrangedRootSequence = LaunchSequence.create(mRegularIntent)
            .append(mAff1Intent.withFlags(NEW_TASK))
            .append(mRegularIntent)
            .append(mAff1Intent.withFlags(REORDER_TO_FRONT));


    private final LaunchSequence mSingleInstanceActivitySequence =
            LaunchSequence.create(mSingleInstanceIntent);

    private final LaunchSequence mSingleTaskActivitySequence = LaunchSequence.create(
            mSingleTaskIntent);

    public List<LaunchSequence> newTaskCases() {
        LaunchIntent aff1NewTask = mAff1Intent.withFlags(NEW_TASK);

        return Lists.newArrayList(
                // 1. Single instance will start a new task even without new task set
                mRegularSequence.act(mSingleInstanceIntent),
                // 2. With new task it will still end up in a new task
                mRegularSequence.act(mSingleInstanceIntent.withFlags(NEW_TASK)),
                // 3. Starting an activity with a different affinity without new task will put it in
                // the existing task
                mRegularSequence.act(mAff1Intent),
                // 4. Starting a task with a different affinity and new task will start a new task
                mRegularSequence.act(aff1NewTask),
                // 5. Starting the intent with new task will not add a new activity to the task
                mRegularSequence.act(mRegularIntent.withFlags(NEW_TASK)),
                // 6. A different activity with the same affinity will start a new activity in
                // the sam task
                mRegularSequence.act(mSingleTopIntent.withFlags(NEW_TASK)),
                // 7. To show that the most recent task get's resumed by new task, this can't
                // be observed without differences in the same activity / task
                mTwoAffinitiesSequence.act(aff1NewTask),
                // 8. To show that new task respects the root as a single even
                // if it is not at the bottom
                mRearrangedRootSequence.act(mAff1Intent.withFlags(NEW_TASK)),
                // 9. To show that new task with non root does start a new activity.
                mRearrangedRootSequence.act(mSecondAff1Intent.withFlags(NEW_TASK)),
                // 10. Multiple task will allow starting activities of the same affinity in
                // different tasks
                mRegularSequence.act(mRegularIntent.withFlags(NEW_TASK, MULTIPLE_TASK)),
                // 11. Single instance will not start a new task even with multiple task on
                mSingleInstanceActivitySequence.act(
                        mSingleInstanceIntent.withFlags(NEW_TASK, MULTIPLE_TASK)),
                // 12. The same should happen for single task.
                mSingleTaskActivitySequence.act(
                        mSingleTaskIntent.withFlags(NEW_TASK, MULTIPLE_TASK)),
                // 13. This starts a regular in a new task
                mSingleInstanceActivitySequence.act(mRegularIntent),
                // 14. This adds regular in the same task
                mSingleTaskActivitySequence.act(mRegularIntent),
                // 15. Starting the activity for result keeps it in the same task
                mSingleInstanceActivitySequence.act(mRegularForResultIntent),
                // 16. Restarts the previous task with regular activity.
                mRegularSequence.append(mSingleInstanceIntent).act(mRegularIntent)
        );
    }

    /**
     * {@link android.content.Intent#FLAG_ACTIVITY_NEW_DOCUMENT }test cases are the same as
     * {@link Cases#newTaskCases()}, we should check them for differences.
     */
    public List<LaunchSequence> newDocumentCases() {
        LaunchIntent aff1NewDocument = mAff1Intent.withFlags(NEW_DOCUMENT);

        return Lists.newArrayList(
                // 1. Single instance will start a new task even without new task set
                mRegularSequence.act(mSingleInstanceIntent),
                // 2. With new task it will still end up in a new task
                mRegularSequence.act(mSingleInstanceIntent.withFlags(NEW_DOCUMENT)),
                // 3. Starting an activity with a different affinity without new task will put it
                // in the existing task
                mRegularSequence.act(mAff1Intent),
                // 4. With new document it will start it's own task
                mRegularSequence.act(aff1NewDocument),
                // 5. Starting the intent with new task will not add a new activity to the task
                mRegularSequence.act(mRegularIntent.withFlags(NEW_DOCUMENT)),
                // 6. Unlike the case with NEW_TASK, with new Document
                mRegularSequence.act(mSingleTopIntent.withFlags(NEW_DOCUMENT)),
                // 7. To show that the most recent task get's resumed by new task
                mTwoAffinitiesSequence.act(aff1NewDocument),
                // 8. To show that new task respects the root as a single
                mRearrangedRootSequence.act(mAff1Intent.withFlags(NEW_DOCUMENT)),
                // 9. New document starts a third task here, because there was no task for the
                // document yet
                mRearrangedRootSequence.act(mSecondAff1Intent.withFlags(NEW_DOCUMENT)),
                // 10. Multiple task wil allow starting activities of the same affinity in different
                // tasks
                mRegularSequence.act(mRegularIntent.withFlags(NEW_DOCUMENT, MULTIPLE_TASK)),
                // 11. Single instance will not start a new task even with multiple task on
                mSingleInstanceActivitySequence
                        .act(mSingleInstanceIntent.withFlags(NEW_DOCUMENT, MULTIPLE_TASK)),
                // 12. The same should happen for single task.
                mSingleTaskActivitySequence.act(
                        mSingleTaskIntent.withFlags(NEW_DOCUMENT, MULTIPLE_TASK))
        );
    }

    public List<LaunchSequence> clearCases() {
        LaunchSequence doubleRegularActivity = mRegularSequence.append(mRegularIntent);

        return Lists.newArrayList(
                // 1. This will clear the bottom and end up with just one activity
                mRegularSequence.act(mRegularIntent.withFlags(CLEAR_TOP)),
                // 2. This will result in still two regulars
                doubleRegularActivity.act(mRegularIntent.withFlags(CLEAR_TOP)),
                // 3. This will result in a single regular it clears the top regular
                // activity and then fails to start a new regular activity because it is already
                // at the root of the task
                doubleRegularActivity.act(mRegularIntent.withFlags(CLEAR_TOP, NEW_TASK)),
                // 3. This will also result in two regulars, showing the first difference between
                // new document and new task.
                doubleRegularActivity.act(mRegularIntent.withFlags(CLEAR_TOP, NEW_DOCUMENT)),
                // 4. This is here to show that previous is top has no effect on clear top or single
                // top
                doubleRegularActivity.act(mRegularIntent.withFlags(CLEAR_TOP, PREVIOUS_IS_TOP)),
                // 5. Clear top finds the same activity in the task and clears from there
                mRegularSequence.append(mAff1Intent).append(mRegularIntent).act(
                        mAff1Intent.withFlags(CLEAR_TOP))
        );
    }

    /**
     * Tests for {@link android.app.Activity#startActivityForResult(Intent, int)}
     */
    //TODO: If b/122968776 is fixed these tests need to be updated
    public List<LaunchSequence> forResultCases() {
        LaunchIntent singleTopForResult = intentForResult(Activities.SingleTopActivity.class);

        return Lists.newArrayList(
                // 1. Start just a single regular activity
                mRegularSequence.act(mRegularIntent.withFlags(SINGLE_TOP)),
                // 2. For result will start a second activity
                mRegularSequence.act(mRegularForResultIntent.withFlags(SINGLE_TOP)),
                // 3. The same but for SINGLE_TOP as a launch mode
                LaunchSequence.create(mSingleTopIntent).act(mSingleTopIntent),
                // 4. Launch mode SINGLE_TOP when launched for result also starts a second activity.
                LaunchSequence.create(mSingleTopIntent).act(singleTopForResult),
                // 5. CLEAR_TOP results in a single regular activity
                mRegularSequence.act(mRegularIntent.withFlags(CLEAR_TOP)),
                // 6. Clear will still kill the for result
                mRegularSequence.act(mRegularForResultIntent.withFlags(CLEAR_TOP)),
                // 7. An activity started for result can go to a different task
                mRegularSequence.act(mRegularForResultIntent.withFlags(NEW_TASK, MULTIPLE_TASK)),
                // 8. Reorder to front with for result
                mRegularSequence.append(mAff1Intent).act(
                        mRegularForResultIntent.withFlags(REORDER_TO_FRONT)),
                // 9. Reorder can move an activity above one that it started for result
                mRegularSequence.append(intentForResult(Activities.TaskAffinity1Activity.class))
                        .act(mRegularIntent.withFlags(REORDER_TO_FRONT))
        );
    }

    /**
     * Reset task if needed will trigger when it is delivered with new task set
     * and there are activities in the task that have a different affinity.
     *
     * @return the test cases
     */
    //TODO: If b/122324373 is fixed these test need to be updated.
    public List<LaunchSequence> resetTaskIfNeeded() {
        // If a task with a different affinity like this get's reset
        // it will create another task in the same stack with the now orphaned activity.
        LaunchIntent resetRegularTask = mRegularIntent.withFlags(NEW_TASK,
                RESET_TASK_IF_NEEDED);
        LaunchSequence resetIfNeeded = mRegularSequence.append(mAff1Intent)
                .act(resetRegularTask);

        // If you try to reset a task with an activity that was started for result
        // it will not move task.
        LaunchSequence resetWontMoveResult = mRegularSequence.append(
                intentForResult(Activities.TaskAffinity1Activity.class))
                .act(resetRegularTask);

        // Reset will not move activities with to a task with that affinity,
        // instead it will always create a new task in that stack.
        LaunchSequence resetToExistingTask2 = mRegularSequence
                .append(mAff1Intent)
                .append(mAff1Intent.withFlags(NEW_TASK))
                .act(resetRegularTask);

        // If a reset occurs the activities that move retain the same order
        // in the new task as they had in the old task.
        LaunchSequence resetOrdering = mRegularSequence
                .append(mAff1Intent)
                .append(mSecondAff1Intent)
                .act(resetRegularTask);

        return Lists.newArrayList(resetIfNeeded, resetWontMoveResult, resetToExistingTask2,
                resetOrdering);
    }

    /**
     * The human readable flags in the JSON files need to be converted back to the corresponding
     * IntentFlag object when reading the file. This creates a map from the flags to their
     * corresponding object.
     *
     * @return lookup table for the parsing of intent flags in the json files.
     */
    public Map<String, IntentFlag> createFlagParsingTable() {
        HashMap<String, IntentFlag> flags = new HashMap<>();
        for (IntentFlag flag : this.flags) {
            flags.put(flag.name, flag);
        }

        flags.put(NONE.getName(), NONE);
        return flags;
    }
}
