@Test
public void shouldIgnoreDuplicateDelayedAppendEntriesReplyForLowerTerm() throws StorageException, RPCException {

    insertIntoLog(NOOP(1, 2));
    insertIntoLog(NOOP(2, 2));
    becomeLeaderInTerm(3, false);
    Collection<AppendEntries> appendEntriesRequests;
    // drain out the heartbeats
    // notice that a NOOP entry was added for this tem when we became a leader
    appendEntriesRequests = getRPCs(4, AppendEntries.class);
    assertThatAppendEntriesHaveValues(appendEntriesRequests, 3, 0, 2, 2, NOOP(3, 3));
    assertThatNoMoreRPCsWereSent();
    // get a rejection from S_04
    // can happen because S_04 never got the entry at prevLogIndex (i.e. LogEntry.NoopEntry(2, 2))
    algorithm.onAppendEntriesReply(S_04, 3, 2, 1, false);
    // now, receive a delayed (from earlier term) duplicate rejection from S_04
    algorithm.onAppendEntriesReply(S_04, 2, 1, 1, false);
    algorithm.onAppendEntriesReply(S_04, 2, 1, 1, false);
    algorithm.onAppendEntriesReply(S_04, 2, 1, 1, false);
    // fast-forward time to simulate a heartbeat timeout
    timer.fastForward();
    // we're expecting another set of heartbeats
    // and, even though we got multiple rejections from S_04, we haven't moved its nextIndex
    appendEntriesRequests = getRPCs(4, AppendEntries.class);
    for (AppendEntries appendEntries : appendEntriesRequests) {
        if (appendEntries.server.equals(S_04)) { // we know for sure that they don't have the entry at 2, 2
            assertThatAppendEntriesHasValues(appendEntries, 3, 0, 1, 2, NOOP(2, 2), NOOP(3, 3));
        } else { // there's still a chance that these guys have the entry at index 1
            assertThatAppendEntriesHasValues(appendEntries, 3, 0, 2, 2, NOOP(3, 3));
        }
    }
    assertThatNoMoreRPCsWereSent();
    assertThatLogContains(
            SENTINEL(),
            NOOP(1, 2),
            NOOP(2, 2),
            NOOP(3, 3)
    );
    assertThatTermAndCommitIndexHaveValues(3, 0);
}