private void addReceivedDataFrame(final int msgNum) {
    dataHeader.wrap(rcvBuffer, 0);

    dataHeader
        .termId(TERM_ID)
        .streamId(STREAM_ID)
        .sessionId(SESSION_ID)
        .termOffset(offsetOfFrame(msgNum))
        .frameLength(MESSAGE_LENGTH)
        .headerType(HeaderFlyweight.HDR_TYPE_DATA)
        .flags(DataHeaderFlyweight.BEGIN_AND_END_FLAGS)
        .version(HeaderFlyweight.CURRENT_VERSION);

    dataHeader.buffer().putBytes(dataHeader.dataOffset(), DATA);

    TermRebuilder.insert(termBuffer, offsetOfFrame(msgNum), rcvBuffer, MESSAGE_LENGTH);
  }
@RunWith(Theories.class)
public class RetransmitHandlerTest {
  private static final int MTU_LENGTH = 1024;
  private static final int TERM_BUFFER_LENGTH = LogBufferDescriptor.TERM_MIN_LENGTH;
  private static final int META_DATA_BUFFER_LENGTH = LogBufferDescriptor.TERM_META_DATA_LENGTH;
  private static final byte[] DATA = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
  private static final int MESSAGE_LENGTH = DataHeaderFlyweight.HEADER_LENGTH + DATA.length;
  private static final int ALIGNED_FRAME_LENGTH =
      align(MESSAGE_LENGTH, FrameDescriptor.FRAME_ALIGNMENT);
  private static final int SESSION_ID = 0x5E55101D;
  private static final int STREAM_ID = 0x5400E;
  private static final int TERM_ID = 0x7F003355;

  private static final FeedbackDelayGenerator DELAY_GENERATOR =
      () -> TimeUnit.MILLISECONDS.toNanos(20);
  private static final FeedbackDelayGenerator ZERO_DELAY_GENERATOR =
      () -> TimeUnit.MILLISECONDS.toNanos(0);
  private static final FeedbackDelayGenerator LINGER_GENERATOR =
      () -> TimeUnit.MILLISECONDS.toNanos(40);

  private final UnsafeBuffer termBuffer =
      new UnsafeBuffer(ByteBuffer.allocateDirect(TERM_BUFFER_LENGTH));
  private final UnsafeBuffer metaDataBuffer =
      new UnsafeBuffer(ByteBuffer.allocateDirect(META_DATA_BUFFER_LENGTH));

  private final TermAppender termAppender =
      new TermAppender(
          termBuffer, metaDataBuffer, DataHeaderFlyweight.createDefaultHeader(0, 0, 0), 1024);

  private final UnsafeBuffer rcvBuffer = new UnsafeBuffer(new byte[MESSAGE_LENGTH]);
  private DataHeaderFlyweight dataHeader = new DataHeaderFlyweight();

  private long currentTime;

  private final TimerWheel wheel =
      new TimerWheel(
          () -> currentTime,
          Configuration.CONDUCTOR_TICK_DURATION_US,
          TimeUnit.MICROSECONDS,
          Configuration.CONDUCTOR_TICKS_PER_WHEEL);

  private final RetransmitSender retransmitSender = mock(RetransmitSender.class);
  private final SystemCounters systemCounters = mock(SystemCounters.class);

  private RetransmitHandler handler =
      new RetransmitHandler(
          wheel,
          systemCounters,
          DELAY_GENERATOR,
          LINGER_GENERATOR,
          retransmitSender,
          TERM_ID,
          TERM_BUFFER_LENGTH);

  @DataPoint
  public static final BiConsumer<RetransmitHandlerTest, Integer> SENDER_ADD_DATA_FRAME =
      (h, i) -> h.addSentDataFrame();

  @DataPoint
  public static final BiConsumer<RetransmitHandlerTest, Integer> RECEIVER_ADD_DATA_FRAME =
      RetransmitHandlerTest::addReceivedDataFrame;

  @Theory
  public void shouldRetransmitOnNak(final BiConsumer<RetransmitHandlerTest, Integer> creator) {
    createTermBuffer(creator, 5);
    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
    processTimersUntil(() -> wheel.clock().nanoTime() >= TimeUnit.MILLISECONDS.toNanos(100));

    verify(retransmitSender).resend(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
  }

  @Theory
  public void shouldNotRetransmitOnNakWhileInLinger(
      final BiConsumer<RetransmitHandlerTest, Integer> creator) {
    createTermBuffer(creator, 5);
    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
    processTimersUntil(() -> wheel.clock().nanoTime() >= TimeUnit.MILLISECONDS.toNanos(40));
    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
    processTimersUntil(() -> wheel.clock().nanoTime() >= TimeUnit.MILLISECONDS.toNanos(100));

    verify(retransmitSender).resend(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
  }

  @Theory
  public void shouldRetransmitOnNakAfterLinger(
      final BiConsumer<RetransmitHandlerTest, Integer> creator) {
    createTermBuffer(creator, 5);
    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
    processTimersUntil(() -> wheel.clock().nanoTime() >= TimeUnit.MILLISECONDS.toNanos(100));
    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
    processTimersUntil(() -> wheel.clock().nanoTime() >= TimeUnit.MILLISECONDS.toNanos(200));

    verify(retransmitSender, times(2)).resend(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
  }

  @Theory
  public void shouldRetransmitOnMultipleNaks(
      final BiConsumer<RetransmitHandlerTest, Integer> creator) {
    createTermBuffer(creator, 5);
    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
    handler.onNak(TERM_ID, offsetOfFrame(1), ALIGNED_FRAME_LENGTH);
    processTimersUntil(() -> wheel.clock().nanoTime() >= TimeUnit.MILLISECONDS.toNanos(100));

    final InOrder inOrder = inOrder(retransmitSender);
    inOrder.verify(retransmitSender).resend(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
    inOrder.verify(retransmitSender).resend(TERM_ID, offsetOfFrame(1), ALIGNED_FRAME_LENGTH);
  }

  @Theory
  public void shouldRetransmitOnNakOverMessageLength(
      final BiConsumer<RetransmitHandlerTest, Integer> creator) {
    createTermBuffer(creator, 10);
    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH * 5);
    processTimersUntil(() -> wheel.clock().nanoTime() >= TimeUnit.MILLISECONDS.toNanos(100));

    verify(retransmitSender).resend(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH * 5);
  }

  @Theory
  public void shouldRetransmitOnNakOverMtuLength(
      final BiConsumer<RetransmitHandlerTest, Integer> creator) {
    final int numFramesPerMtu = MTU_LENGTH / ALIGNED_FRAME_LENGTH;
    createTermBuffer(creator, numFramesPerMtu * 5);
    handler.onNak(TERM_ID, offsetOfFrame(0), MTU_LENGTH * 2);
    processTimersUntil(() -> wheel.clock().nanoTime() >= TimeUnit.MILLISECONDS.toNanos(100));

    verify(retransmitSender).resend(TERM_ID, offsetOfFrame(0), MTU_LENGTH * 2);
  }

  @Theory
  public void shouldStopRetransmitOnRetransmitReception(
      final BiConsumer<RetransmitHandlerTest, Integer> creator) {
    createTermBuffer(creator, 5);
    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
    handler.onRetransmitReceived(TERM_ID, offsetOfFrame(0));
    processTimersUntil(() -> wheel.clock().nanoTime() >= TimeUnit.MILLISECONDS.toNanos(100));

    verifyZeroInteractions(retransmitSender);
  }

  @Theory
  public void shouldStopOneRetransmitOnRetransmitReception(
      final BiConsumer<RetransmitHandlerTest, Integer> creator) {
    createTermBuffer(creator, 5);
    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
    handler.onNak(TERM_ID, offsetOfFrame(1), ALIGNED_FRAME_LENGTH);
    handler.onRetransmitReceived(TERM_ID, offsetOfFrame(0));
    processTimersUntil(() -> wheel.clock().nanoTime() >= TimeUnit.MILLISECONDS.toNanos(100));

    verify(retransmitSender).resend(TERM_ID, offsetOfFrame(1), ALIGNED_FRAME_LENGTH);
  }

  @Theory
  public void shouldImmediateRetransmitOnNak(
      final BiConsumer<RetransmitHandlerTest, Integer> creator) {
    createTermBuffer(creator, 5);
    handler = newZeroDelayRetransmitHandler();

    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);

    verify(retransmitSender).resend(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
  }

  @Theory
  public void shouldGoIntoLingerOnImmediateRetransmit(
      final BiConsumer<RetransmitHandlerTest, Integer> creator) {
    createTermBuffer(creator, 5);
    handler = newZeroDelayRetransmitHandler();

    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
    processTimersUntil(() -> wheel.clock().nanoTime() >= TimeUnit.MILLISECONDS.toNanos(40));
    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);

    verify(retransmitSender).resend(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);
  }

  @Theory
  public void shouldOnlyRetransmitOnNakWhenConfiguredTo(
      final BiConsumer<RetransmitHandlerTest, Integer> creator) {
    createTermBuffer(creator, 5);
    handler.onNak(TERM_ID, offsetOfFrame(0), ALIGNED_FRAME_LENGTH);

    verifyZeroInteractions(retransmitSender);
  }

  private RetransmitHandler newZeroDelayRetransmitHandler() {
    return new RetransmitHandler(
        wheel,
        systemCounters,
        ZERO_DELAY_GENERATOR,
        LINGER_GENERATOR,
        retransmitSender,
        TERM_ID,
        TERM_BUFFER_LENGTH);
  }

  private void createTermBuffer(
      final BiConsumer<RetransmitHandlerTest, Integer> creator, final int num) {
    IntStream.range(0, num).forEach((i) -> creator.accept(this, i));
  }

  private static int offsetOfFrame(final int index) {
    return index * ALIGNED_FRAME_LENGTH;
  }

  private void addSentDataFrame() {
    rcvBuffer.putBytes(0, DATA);
    termAppender.append(rcvBuffer, 0, DATA.length);
  }

  private void addReceivedDataFrame(final int msgNum) {
    dataHeader.wrap(rcvBuffer, 0);

    dataHeader
        .termId(TERM_ID)
        .streamId(STREAM_ID)
        .sessionId(SESSION_ID)
        .termOffset(offsetOfFrame(msgNum))
        .frameLength(MESSAGE_LENGTH)
        .headerType(HeaderFlyweight.HDR_TYPE_DATA)
        .flags(DataHeaderFlyweight.BEGIN_AND_END_FLAGS)
        .version(HeaderFlyweight.CURRENT_VERSION);

    dataHeader.buffer().putBytes(dataHeader.dataOffset(), DATA);

    TermRebuilder.insert(termBuffer, offsetOfFrame(msgNum), rcvBuffer, MESSAGE_LENGTH);
  }

  private long processTimersUntil(final BooleanSupplier condition) {
    final long start = wheel.clock().nanoTime();

    while (!condition.getAsBoolean()) {
      if (wheel.computeDelayInMs() > 0) {
        currentTime += TimeUnit.MICROSECONDS.toNanos(Configuration.CONDUCTOR_TICK_DURATION_US);
      }

      wheel.expireTimers();
    }

    return wheel.clock().nanoTime() - start;
  }
}