/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.phoenix.index;

import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Mutation;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.phoenix.coprocessor.IndexRebuildRegionScanner;
import org.apache.phoenix.hbase.index.IndexRegionObserver;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.query.BaseConnectionlessQueryTest;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.PTableKey;
import org.apache.phoenix.util.SchemaUtil;
import org.junit.Assert;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.junit.Assert.assertEquals;

public class PrepareIndexMutationsForRebuildTest extends BaseConnectionlessQueryTest {
    private static String ROW_KEY = "k1";
    private static String TABLE_NAME = "dataTable";
    private static String INDEX_NAME = "idx";

    class SetupInfo {
        public IndexMaintainer indexMaintainer;
        public PTable pDataTable;
    }

    /**
     * Get the index maintainer and phoenix table definition of data table.
     * @param tableName
     * @param indexName
     * @param columns
     * @param indexColumns
     * @param pk
     * @param includeColumns
     * @return
     * @throws Exception
     */
    private SetupInfo setup(String tableName,
                            String indexName,
                            String columns,
                            String indexColumns,
                            String pk,
                            String includeColumns) throws Exception {
        try(Connection conn = DriverManager.getConnection(getUrl())) {

            String fullTableName = SchemaUtil.getTableName(SchemaUtil.normalizeIdentifier(""), SchemaUtil.normalizeIdentifier(tableName));
            String fullIndexName = SchemaUtil.getTableName(SchemaUtil.normalizeIdentifier(""), SchemaUtil.normalizeIdentifier(indexName));

            // construct the data table and index based from the parameters
            String str1 = String.format("CREATE TABLE %1$s (%2$s CONSTRAINT pk PRIMARY KEY (%3$s)) COLUMN_ENCODED_BYTES=0",
                    fullTableName,
                    columns,
                    pk);
            conn.createStatement().execute(str1);

            String str2 = String.format("CREATE INDEX %1$s ON %2$s (%3$s)",
                    fullIndexName,
                    fullTableName,
                    indexColumns);
            if (!includeColumns.isEmpty())
                str2 += " INCLUDE (" + includeColumns + ")";
            conn.createStatement().execute(str2);

            // Get the data table, index table and index maintainer reference from the client's ConnectionQueryServiceImpl
            // In this way, we don't need to setup a local cluster.
            PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class);
            PTable pIndexTable = pconn.getTable(new PTableKey(pconn.getTenantId(), fullIndexName));
            PTable pDataTable = pconn.getTable(new PTableKey(pconn.getTenantId(), fullTableName));
            IndexMaintainer im = pIndexTable.getIndexMaintainer(pDataTable, pconn);

            SetupInfo info = new SetupInfo();
            info.indexMaintainer = im;
            info.pDataTable = pDataTable;
            return info;
        }
    }

    /**
     * Simulate one put mutation on the indexed column
     * @throws Exception
     */
    @Test
    public void testSinglePutOnIndexColumn() throws Exception {
        SetupInfo info = setup(TABLE_NAME,
                INDEX_NAME,
                "ROW_KEY VARCHAR, C1 VARCHAR, C2 VARCHAR",
                "C1",
                "ROW_KEY",
                "");

        // insert a row
        Put dataPut = new Put(Bytes.toBytes(ROW_KEY));
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C1"),
                1,
                Bytes.toBytes("v1"));
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C2"),
                1,
                Bytes.toBytes("v2"));
        addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1);

        List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer,
                dataPut,
                null);

        // Expect one row of index with row key "v1_k1"
        Put idxPut1 = new Put(generateIndexRowKey("v1"));
        addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1);

        assertEqualMutationList(Arrays.asList((Mutation)idxPut1), actualIndexMutations);
    }

    /**
     * Simulate one put mutation on the non-indexed column
     * @throws Exception
     */
    @Test
    public void testSinglePutOnNonIndexColumn() throws Exception {
        SetupInfo info = setup(TABLE_NAME,
                INDEX_NAME,
                "ROW_KEY VARCHAR, C1 VARCHAR, C2 VARCHAR",
                "C1",
                "ROW_KEY",
                "");

        Put dataPut = new Put(Bytes.toBytes(ROW_KEY));
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C2"),
                1,
                Bytes.toBytes("v2"));
        addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1);

        List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer,
                dataPut,
                null);

        // Expect one row of index with row key "_k1", as indexed column C1 is nullable.
        Put idxPut1 = new Put(generateIndexRowKey(null));
        addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1);

        assertEqualMutationList(Arrays.asList((Mutation)idxPut1), actualIndexMutations);
    }

    /**
     * Simulate the column delete on the index column
     * @throws Exception
     */
    @Test
    public void testDelOnIndexColumn() throws Exception {
        SetupInfo info = setup(TABLE_NAME,
                INDEX_NAME,
                "ROW_KEY VARCHAR, C1 VARCHAR, C2 VARCHAR",
                "C1",
                "ROW_KEY",
                "");

        // insert the row for deletion
        Put dataPut = new Put(Bytes.toBytes(ROW_KEY));
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C1"),
                1,
                Bytes.toBytes("v1"));
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C2"),
                1,
                Bytes.toBytes("v2"));
        addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1);

        // only delete the value of column C1
        Delete dataDel = new Delete(Bytes.toBytes(ROW_KEY));
        addCellToDelMutation(dataDel,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C1"),
                2,
                KeyValue.Type.DeleteColumn);

        List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer,
                dataPut,
                dataDel);

        List<Mutation> expectedIndexMutation = new ArrayList<>();

        // generate the index row key "v1_k1"
        byte[] idxKeyBytes = generateIndexRowKey("v1");

        Put idxPut1 = new Put(idxKeyBytes);
        addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1);
        expectedIndexMutation.add(idxPut1);

        // generate the index row key "_k1"
        Put idxPut2 = new Put(generateIndexRowKey(null));
        addEmptyColumnToIndexPutMutation(idxPut2, info.indexMaintainer, 2);
        expectedIndexMutation.add(idxPut2);

        // This deletion is to remove the row added by the idxPut1, as idxPut2 has different row key as idxPut1.
        // Otherwise the row "v1_k1" will still be shown in the scan result
        Delete idxDel = new Delete(idxKeyBytes);
        addCellToDelMutation(idxDel,
                QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES,
                null,
                2,
                KeyValue.Type.DeleteFamily);
        expectedIndexMutation.add(idxDel);

        assertEqualMutationList(expectedIndexMutation, actualIndexMutations);
    }

    /**
     * Simulate the column delete on the non-indexed column
     * @throws Exception
     */
    @Test
    public void testDelOnNonIndexColumn() throws Exception {
        SetupInfo info = setup(TABLE_NAME,
                INDEX_NAME,
                "ROW_KEY VARCHAR, C1 VARCHAR, C2 VARCHAR",
                "C1",
                "ROW_KEY",
                "");

        // insert the row for deletion
        Put dataPut = new Put(Bytes.toBytes(ROW_KEY));
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C1"),
                1,
                Bytes.toBytes("v1"));
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C2"),
                1,
                Bytes.toBytes("v2"));
        addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1);

        // delete the value of column C2
        Delete dataDel = new Delete(Bytes.toBytes(ROW_KEY));
        addCellToDelMutation(dataDel,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C2"),
                2,
                KeyValue.Type.DeleteColumn);

        List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer,
                dataPut,
                dataDel);

        List<Mutation> expectedIndexMutations = new ArrayList<>();

        byte[] idxKeyBytes = generateIndexRowKey("v1");

        // idxPut1 is the corresponding index mutation of dataPut
        Put idxPut1 = new Put(idxKeyBytes);
        addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1);
        expectedIndexMutations.add(idxPut1);

        // idxPut2 is required to update the timestamp, so the index row will have the same life time as its corresponding data row.
        // No delete mutation is expected on index table, as data mutation happens only on non-indexed column.
        Put idxPut2 = new Put(idxKeyBytes);
        addEmptyColumnToIndexPutMutation(idxPut2, info.indexMaintainer, 2);
        expectedIndexMutations.add(idxPut2);

        assertEqualMutationList(expectedIndexMutations, actualIndexMutations);
    }

    /**
     * Simulate the data deletion of all version on the indexed row
     * @throws Exception
     */
    @Test
    public void testDeleteAllVersions() throws Exception {
        SetupInfo info = setup(TABLE_NAME,
                INDEX_NAME,
                "ROW_KEY VARCHAR, C1 VARCHAR",
                "C1",
                "ROW_KEY",
                "");

        // insert two versions for a single row
        Put dataPut = new Put(Bytes.toBytes(ROW_KEY));
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C1"),
                1,
                Bytes.toBytes("v1"));
        addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1);
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C1"),
                2,
                Bytes.toBytes("v2"));
        addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 2);

        // DeleteFamily will delete all versions of the columns in that family
        // Since C1 is the only column of the default column family, so deleting the default family removes all version
        // of column C1
        Delete dataDel = new Delete(Bytes.toBytes(ROW_KEY));
        addCellToDelMutation(dataDel,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                null,
                3,
                KeyValue.Type.DeleteFamily);

        List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer,
                dataPut,
                dataDel);

        List<Mutation> expectedIndexMutations = new ArrayList<>();

        byte[] idxKeyBytes1 = generateIndexRowKey("v1");
        byte[] idxKeyBytes2 = generateIndexRowKey("v2");

        // idxPut1 and idxPut2 are generated by two versions in dataPut
        Put idxPut1 = new Put(idxKeyBytes1);
        addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1);
        expectedIndexMutations.add(idxPut1);

        Put idxPut2 = new Put(idxKeyBytes2);
        addEmptyColumnToIndexPutMutation(idxPut2, info.indexMaintainer, 2);
        expectedIndexMutations.add(idxPut2);

        // idxDel1 is required to remove the row key "v1_k1" which is added by idxPut1.
        // The ts of idxDel1 is same as idxPut2, because it is a result of idxPut2.
        // Since C1 is the only index column, so it is translated to DeleteFamily mutation.
        Delete idxDel1 = new Delete(idxKeyBytes1);
        addCellToDelMutation(idxDel1,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                null,
                2,
                KeyValue.Type.DeleteFamily);
        expectedIndexMutations.add(idxDel1);

        // idxDel2 is corresponding index mutation of dataDel
        Delete idxDel2 = new Delete(idxKeyBytes2);
        addCellToDelMutation(idxDel2,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                null,
                3,
                KeyValue.Type.DeleteFamily);
        expectedIndexMutations.add(idxDel2);

        assertEqualMutationList(expectedIndexMutations, actualIndexMutations);
    }

    // Simulate the put and delete mutation with the same time stamp on the index
    @Test
    public void testPutDeleteOnSameTimeStamp() throws Exception {
        SetupInfo info = setup(TABLE_NAME,
                INDEX_NAME,
                "ROW_KEY VARCHAR, C1 VARCHAR",
                "C1",
                "ROW_KEY",
                "");

        // insert a row
        Put dataPut = new Put(Bytes.toBytes(ROW_KEY));
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C1"),
                1,
                Bytes.toBytes("v1"));
        addEmptyColumnToDataPutMutation(dataPut, info.pDataTable,1);

        // delete column of C1 from the inserted row
        Delete dataDel = new Delete(Bytes.toBytes(ROW_KEY));
        addCellToDelMutation(dataDel,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C1"),
                1,
                KeyValue.Type.DeleteColumn);

        List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer,
                dataPut,
                dataDel);

        List<Mutation> expectedIndexMutations = new ArrayList<>();

        // The dataDel will be applied on top of dataPut when we replay them for index rebuild, when they have the same time stamp.
        // idxPut1 is expected as in data table we still see the row of k1 with empty C1, so we need a row in index table with row key "_k1"
        Put idxPut1 = new Put(generateIndexRowKey(null));
        addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1);
        expectedIndexMutations.add(idxPut1);

        assertEqualMutationList(Arrays.asList((Mutation)idxPut1), actualIndexMutations);
    }

    // Simulate the put and delete mutation on the covered column of data table
    @Test
    public void testCoveredIndexColumns() throws Exception {
        SetupInfo info = setup(TABLE_NAME,
                INDEX_NAME,
                "ROW_KEY VARCHAR, C1 VARCHAR, C2 VARCHAR",
                "C1",
                "ROW_KEY",
                "C2");

        Put dataPut = new Put(Bytes.toBytes(ROW_KEY));
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C1"),
                1,
                Bytes.toBytes("v1"));
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C2"),
                1,
                Bytes.toBytes("v2"));
        addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1);

        Delete dataDel = new Delete(Bytes.toBytes(ROW_KEY));
        addCellToDelMutation(dataDel,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C1"),
                2,
                KeyValue.Type.DeleteColumn);

        List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer,
                dataPut,
                dataDel);

        List<Mutation> expectedIndexMutations = new ArrayList<>();
        byte[] idxKeyBytes = generateIndexRowKey("v1");

        // idxPut1 is generated corresponding to dataPut.
        // The column "0:C2" is generated from data table column family and column name, its family name is still default family name of index table
        Put idxPut1 = new Put(idxKeyBytes);
        addCellToPutMutation(idxPut1,
                QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES,
                Bytes.toBytes("0:C2"),
                1,
                Bytes.toBytes("v2"));
        addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1);
        expectedIndexMutations.add(idxPut1);

        // idxKey2 is required by dataDel, as dataDel change the corresponding row key of index table
        List<Byte> idxKey2 = new ArrayList<>();
        idxKey2.add(QueryConstants.SEPARATOR_BYTE);
        idxKey2.addAll(com.google.common.primitives.Bytes.asList(Bytes.toBytes(ROW_KEY)));
        byte[] idxKeyBytes2 = com.google.common.primitives.Bytes.toArray(idxKey2);
        Put idxPut2 = new Put(idxKeyBytes2);
        addCellToPutMutation(idxPut2,
                QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES,
                Bytes.toBytes("0:C2"),
                2,
                Bytes.toBytes("v2"));
        addEmptyColumnToIndexPutMutation(idxPut2, info.indexMaintainer, 2);
        expectedIndexMutations.add(idxPut2);

        // idxDel is required to invalid the index row "v1_k1", dataDel removed the value of indexed column
        Delete idxDel = new Delete(idxKeyBytes);
        addCellToDelMutation(idxDel,
                QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES,
                null,
                2,
                KeyValue.Type.DeleteFamily);
        expectedIndexMutations.add(idxDel);

        assertEqualMutationList(expectedIndexMutations, actualIndexMutations);
    }

    // Simulate the scenario that index column, and covered column belong to different column families
    @Test
    public void testForMultipleFamilies() throws Exception {
        SetupInfo info = setup(TABLE_NAME,
                INDEX_NAME,
                "ROW_KEY VARCHAR, CF1.C1 VARCHAR, CF2.C2 VARCHAR",  //define C1 and C2 with different families
                "CF1.C1",
                "ROW_KEY",
                "CF2.C2");

        // insert a row to the data table
        Put dataPut = new Put(Bytes.toBytes(ROW_KEY));
        addCellToPutMutation(dataPut,
                Bytes.toBytes("CF1"),
                Bytes.toBytes("C1"),
                1,
                Bytes.toBytes("v1"));
        addCellToPutMutation(dataPut,
                Bytes.toBytes("CF2"),
                Bytes.toBytes("C2"),
                1,
                Bytes.toBytes("v2"));
        addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1);

        // delete the indexed column CF1:C1
        Delete dataDel = new Delete(Bytes.toBytes(ROW_KEY));
        addCellToDelMutation(dataDel,
                Bytes.toBytes("CF1"),
                Bytes.toBytes("C1"),
                2,
                KeyValue.Type.DeleteColumn);

        List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer,
                dataPut,
                dataDel);

        List<Mutation> expectedIndexMutation = new ArrayList<>();

        byte[] idxKeyBytes = generateIndexRowKey("v1");

        // index table will use the family name of the first covered column, which is CF2 here.
        Put idxPut1 = new Put(idxKeyBytes);
        addCellToPutMutation(idxPut1,
                Bytes.toBytes("CF2"),
                Bytes.toBytes("CF2:C2"),
                1,
                Bytes.toBytes("v2"));
        addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1);
        expectedIndexMutation.add(idxPut1);

        // idxPut2 and idxDel are the result of dataDel
        // idxPut2 is to create the index row "_k1", idxDel is to invalid the index row "v1_k1".
        Put idxPut2 = new Put(generateIndexRowKey(null));
        addCellToPutMutation(idxPut2,
                Bytes.toBytes("CF2"),
                Bytes.toBytes("CF2:C2"),
                2,
                Bytes.toBytes("v2"));
        addEmptyColumnToIndexPutMutation(idxPut2, info.indexMaintainer, 2);
        expectedIndexMutation.add(idxPut2);

        Delete idxDel = new Delete(idxKeyBytes);
        addCellToDelMutation(idxDel,
                Bytes.toBytes("CF2"),
                null,
                2,
                KeyValue.Type.DeleteFamily);
        expectedIndexMutation.add(idxDel);

        assertEqualMutationList(expectedIndexMutation, actualIndexMutations);
    }

    // Simulate two data put with the same value but different time stamp.
    // We expect to see 2 index mutations with same value but different time stamps.
    @Test
    public void testSameTypeOfMutationWithSameValueButDifferentTimeStamp() throws Exception {
        SetupInfo info = setup(TABLE_NAME,
                INDEX_NAME,
                "ROW_KEY VARCHAR, C1 VARCHAR, C2 VARCHAR",
                "C1",
                "ROW_KEY",
                "");

        Put dataPut = new Put(Bytes.toBytes(ROW_KEY));
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C2"),
                1,
                Bytes.toBytes("v2"));
        addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1);
        addCellToPutMutation(dataPut,
                info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                Bytes.toBytes("C2"),
                1,
                Bytes.toBytes("v3"));
        addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 2);

        List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer,
                dataPut,
                null);

        byte[] idxKeyBytes = generateIndexRowKey(null);

        // idxPut1 and idxPut2 have same value but different time stamp
        Put idxPut1 = new Put(idxKeyBytes);
        addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1);

        Put idxPut2 = new Put(idxKeyBytes);
        addEmptyColumnToIndexPutMutation(idxPut2, info.indexMaintainer, 2);

        assertEqualMutationList(Arrays.asList((Mutation)idxPut1, (Mutation)idxPut2), actualIndexMutations);
    }

    /**
     * Generate the row key for index table by the value of indexed column
     * @param indexVal
     * @return
     */
    byte[] generateIndexRowKey(String indexVal) {
        List<Byte> idxKey = new ArrayList<>();
        if (indexVal != null && !indexVal.isEmpty())
            idxKey.addAll(com.google.common.primitives.Bytes.asList(Bytes.toBytes(indexVal)));
        idxKey.add(QueryConstants.SEPARATOR_BYTE);
        idxKey.addAll(com.google.common.primitives.Bytes.asList(Bytes.toBytes(ROW_KEY)));
        return com.google.common.primitives.Bytes.toArray(idxKey);
    }

    void addCellToPutMutation(Put put, byte[] family, byte[] column, long ts, byte[] value) throws Exception {
        byte[] rowKey = put.getRow();
        Cell cell = CellUtil.createCell(rowKey, family, column, ts, KeyValue.Type.Put.getCode(), value);
        put.add(cell);
    }

    void addCellToDelMutation(Delete del, byte[] family, byte[] column, long ts, KeyValue.Type type) throws Exception {
        byte[] rowKey = del.getRow();
        Cell cell = CellUtil.createCell(rowKey, family, column, ts, type.getCode(), null);
        del.addDeleteMarker(cell);
    }

    /**
     * Add Empty column to the existing data put mutation
     * @param put
     * @param ptable
     * @param ts
     * @throws Exception
     */
    void addEmptyColumnToDataPutMutation(Put put, PTable ptable, long ts) throws Exception {
        addCellToPutMutation(put,
                SchemaUtil.getEmptyColumnFamily(ptable),
                QueryConstants.EMPTY_COLUMN_BYTES,
                ts,
                QueryConstants.EMPTY_COLUMN_VALUE_BYTES);
    }

    /**
     * Add the verified flag to the existing index put mutation
     * @param put
     * @param im
     * @param ts
     * @throws Exception
     */
    void addEmptyColumnToIndexPutMutation(Put put, IndexMaintainer im, long ts) throws Exception {
        addCellToPutMutation(put,
                im.getEmptyKeyValueFamily().copyBytesIfNecessary(),
                QueryConstants.EMPTY_COLUMN_BYTES,
                ts,
                IndexRegionObserver.VERIFIED_BYTES);
    }

    /**
     * Compare two mutation lists without worrying about the order of the mutations in the lists
     * @param expectedMutations
     * @param actualMutations
     */
    void assertEqualMutationList(List<Mutation> expectedMutations,
                                 List<Mutation> actualMutations) {
        assertEquals(expectedMutations.size(), actualMutations.size());
        for (Mutation expected : expectedMutations) {
            boolean found = false;
            for (Mutation actual: actualMutations) {
                if (isEqualMutation(expected, actual)) {
                    actualMutations.remove(actual);
                    found = true;
                    break;
                }
            }
            if (!found)
                Assert.fail(String.format("Cannot find mutation:%s", expected));
        }
    }

    /**
     * Compare two mutations without worrying about the order of cells within each mutation
     * @param expectedMutation
     * @param actualMutation
     * @return
     */
    boolean isEqualMutation(Mutation expectedMutation, Mutation actualMutation){
        List<Cell> expectedCells = new ArrayList<>();
        for (List<Cell> cells : expectedMutation.getFamilyCellMap().values()) {
            expectedCells.addAll(cells);
        }

        List<Cell> actualCells = new ArrayList<>();
        for (List<Cell> cells : actualMutation.getFamilyCellMap().values()) {
            actualCells.addAll(cells);
        }

        if (expectedCells.size() != actualCells.size())
            return false;
        for(Cell expected : expectedCells) {
            boolean found = false;
            for(Cell actual: actualCells){
                if (isEqualCell(expected, actual)) {
                    actualCells.remove(actual);
                    found = true;
                    break;
                }
            }
            if (!found)
                return false;
        }

        return true;
    }

    boolean isEqualCell(Cell a, Cell b) {
        return CellUtil.matchingRow(a, b)
                && CellUtil.matchingFamily(a, b)
                && CellUtil.matchingQualifier(a, b)
                && CellUtil.matchingTimestamp(a, b)
                && CellUtil.matchingType(a, b)
                && CellUtil.matchingValue(a, b);
    }
}