code: package io.paradoxical.cassieq.unittests.time;
  
  import io.paradoxical.cassieq.model.time.Clock;
  import io.paradoxical.cassieq.model.time.SleepableClock;
  import lombok.EqualsAndHashCode;
  import lombok.Value;
  import org.apache.commons.lang3.time.DateUtils;
  import org.joda.time.Duration;
  import org.joda.time.Instant;
  
  import java.util.Calendar;
  import java.util.Comparator;
  import java.util.PriorityQueue;
  
  
  public class TestClock implements Clock, SleepableClock {
  
      public static TestClock create() {
          return new TestClock();
      }
  
      private Instant time = truncate(Instant.now());
  
      private final Thread heartbeatThread;
  
      private volatile boolean running = true;
  
      @Value
      @EqualsAndHashCode
      private static class SleepItem {
          Instant sleepUntil;
          Object notificationObject;
      }
  
      public TestClock() {
          heartbeatThread = new Thread(() -> {
              while (running) {
                  try {
                      Thread.sleep(17);
                  }
                  catch (InterruptedException e) {
                      return;
                  }
  
                  tickSeconds(0L);
              }
          });
          heartbeatThread.setDaemon(true);
  
          heartbeatThread.start();
      }
  
      protected final void finalize() {
          running = false;
      }
  
      private final PriorityQueue<SleepItem> sleepItems = new PriorityQueue<>(new Comparator<SleepItem>() {
          @Override
          public int compare(final SleepItem o1, final SleepItem o2) {
              return o1.getSleepUntil().compareTo(o2.getSleepUntil());
          }
      });
  
      public void tick() {
          tickSeconds(1L);
      }
  
      public synchronized void tickSeconds(Long seconds) {
          time = time.plus(Duration.standardSeconds(seconds));
  
          SleepItem peek;
          while ((peek = sleepItems.peek()) != null && shouldExecute(sleepItems.peek())) {
              final Object notificationObject = peek.getNotificationObject();
              synchronized (notificationObject) {
                  notificationObject.notify();
              }
              sleepItems.remove();
          }
      }
  
      private boolean shouldExecute(final SleepItem peek) {
          final Instant now = now();
  
          return truncate(peek.getSleepUntil()).isBefore(now) ||
                 truncate(peek.getSleepUntil()).isEqual(now);
      }
  
      @Override
      public Instant now() {
          return truncate(time);
      }
  
      @Override
      public void sleepFor(final Duration duration) throws InterruptedException {
          sleepTill(now().plus(duration));
      }
  
      @Override
      public void sleepTill(final Instant instant) throws InterruptedException {
          final Object sleepObject = new Object();
  
          final Instant truncatedInstant = truncate(instant);
  
          if (Instant.now().isAfter(truncatedInstant)) {
              return;
          }
  
          synchronized (this) {
              sleepItems.add(new SleepItem(truncatedInstant, sleepObject));
          }
  
          synchronized (sleepObject) {
              sleepObject.wait();
          }
      }
  
      private Instant truncate(final Instant instant) {
          return new Instant(DateUtils.truncate(instant.toDate(), Calendar.SECOND).toInstant().toEpochMilli());
      }
  
      @Override
      public long jitter(final int maximumJitter) {
          return 0;
      }
  }
  