FileSpan addUndeleteEntry(MockId idToUndelete, MessageInfo info, short lifeVersion) throws StoreException {
    IndexValue value = getExpectedValue(idToUndelete, false);
    boolean hasLifeVersion = IndexValue.hasLifeVersion(lifeVersion);
    if (value == null && info == null) {
      throw new IllegalArgumentException(idToUndelete + " does not exist in the index");
    }
    if (info != null && !hasLifeVersion) {
      throw new IllegalArgumentException("Invalid lifeVersion " + lifeVersion + " when info is present");
    }

    // add undelete to log
    byte[] dataWritten = appendToLog(CuratedLogIndexState.UNDELETE_RECORD_SIZE);
    Offset endOffsetOfPrevMsg = index.getCurrentEndOffset();
    FileSpan fileSpan = log.getFileSpanForMessage(endOffsetOfPrevMsg, CuratedLogIndexState.UNDELETE_RECORD_SIZE);

    Offset startOffset = fileSpan.getStartOffset();
    Offset indexSegmentStartOffset = generateReferenceIndexSegmentStartOffset(startOffset);
    if (!referenceIndex.containsKey(indexSegmentStartOffset)) {
      // rollover will occur
      advanceTime(DELAY_BETWEEN_LAST_MODIFIED_TIMES_MS);
      referenceIndex.put(indexSegmentStartOffset, new TreeMap<>());
    }

    // After compaction, undelete record would not be kept without Put and Delete record, so don't bother to forcely
    // add the undelete record to log.
    IndexValue newValue;
    if (value != null) {
      short undelLifeVersion = hasLifeVersion ? lifeVersion : (short) (value.getLifeVersion() + 1);
      newValue = new IndexValue(UNDELETE_RECORD_SIZE, startOffset, value.getFlags(), value.getExpiresAtMs(),
          time.milliseconds(), value.getAccountId(), value.getContainerId(), undelLifeVersion);
    } else {
      newValue = new IndexValue(UNDELETE_RECORD_SIZE, startOffset, IndexValue.FLAGS_DEFAULT_VALUE,
          info.getExpirationTimeInMs(), time.milliseconds(), info.getAccountId(), info.getContainerId(), lifeVersion);
      if (info.isTtlUpdated()) {
        newValue.setFlag(IndexValue.Flags.Ttl_Update_Index);
      }
    }
    newValue.clearOriginalMessageOffset();
    newValue.setFlag(IndexValue.Flags.Undelete_Index);
    newValue.clearFlag(IndexValue.Flags.Delete_Index);
    if (info != null || hasLifeVersion) {
      index.markAsUndeleted(idToUndelete, fileSpan, info, newValue.getOperationTimeInMs(), lifeVersion);
    } else {
      index.markAsUndeleted(idToUndelete, fileSpan, newValue.getOperationTimeInMs());
    }
    markAsUndeleted(idToUndelete);
    logOrder.put(startOffset, new Pair<>(idToUndelete, new LogEntry(dataWritten, newValue)));
    allKeys.computeIfAbsent(idToUndelete, k -> new TreeSet<>()).add(newValue);
    referenceIndex.get(indexSegmentStartOffset).computeIfAbsent(idToUndelete, k -> new TreeSet<>()).add(newValue);
    lastModifiedTimesInSecs.put(indexSegmentStartOffset, newValue.getOperationTimeInMs() / Time.MsPerSec);
    endOffsetOfPrevMsg = fileSpan.getEndOffset();
    assertEquals("End Offset of index not as expected", endOffsetOfPrevMsg, index.getCurrentEndOffset());
    assertEquals("Journal's last offset not as expected", startOffset, index.journal.getLastOffset());
    return fileSpan;
  }
Adds an undelete entry in the index (real and reference) for {@code idToDelete}.
@param idToUndelete the id to be undeleted.
@param info The {@link MessageInfo} to use when there is no previous value for {@code idToUndelete}.
@param lifeVersion the lifeVersion for undeleted entry.
@return the {@link FileSpan} of the added entries.
@throws StoreException
ambry-store/src/test/java/com/github/ambry/store/CuratedLogIndexState.java