@Test
  public void testLinearMovAvgModel() {
    MovAvgModel model = new LinearModel();

    int numValues = randomIntBetween(1, 100);
    int windowSize = randomIntBetween(1, 50);

    EvictingQueue<Double> window = EvictingQueue.create(windowSize);
    for (int i = 0; i < numValues; i++) {
      double randValue = randomDouble();

      if (i == 0) {
        window.offer(randValue);
        continue;
      }

      double avg = 0;
      long totalWeight = 1;
      long current = 1;

      for (double value : window) {
        avg += value * current;
        totalWeight += current;
        current += 1;
      }
      double expected = avg / totalWeight;
      double actual = model.next(window);
      assertThat(Double.compare(expected, actual), equalTo(0));
      window.offer(randValue);
    }
  }
  @Test
  public void testEWMAPredictionModel() {
    double alpha = randomDouble();
    MovAvgModel model = new EwmaModel(alpha);

    int windowSize = randomIntBetween(1, 50);
    int numPredictions = randomIntBetween(1, 50);

    EvictingQueue<Double> window = EvictingQueue.create(windowSize);
    for (int i = 0; i < windowSize; i++) {
      window.offer(randomDouble());
    }
    double actual[] = model.predict(window, numPredictions);
    double expected[] = new double[numPredictions];

    double avg = 0;
    boolean first = true;

    for (double value : window) {
      if (first) {
        avg = value;
        first = false;
      } else {
        avg = (value * alpha) + (avg * (1 - alpha));
      }
    }
    Arrays.fill(expected, avg);

    for (int i = 0; i < numPredictions; i++) {
      assertThat(Double.compare(expected[i], actual[i]), equalTo(0));
    }
  }
  @Test
  public void testSimplePredictionModel() {
    MovAvgModel model = new SimpleModel();

    int windowSize = randomIntBetween(1, 50);
    int numPredictions = randomIntBetween(1, 50);

    EvictingQueue<Double> window = EvictingQueue.create(windowSize);
    for (int i = 0; i < windowSize; i++) {
      window.offer(randomDouble());
    }
    double actual[] = model.predict(window, numPredictions);

    double expected[] = new double[numPredictions];
    double t = 0;
    for (double value : window) {
      t += value;
    }
    t /= window.size();
    Arrays.fill(expected, t);

    for (int i = 0; i < numPredictions; i++) {
      assertThat(Double.compare(expected[i], actual[i]), equalTo(0));
    }
  }
  @Test
  public void testSimpleMovAvgModel() {
    MovAvgModel model = new SimpleModel();

    int numValues = randomIntBetween(1, 100);
    int windowSize = randomIntBetween(1, 50);

    EvictingQueue<Double> window = EvictingQueue.create(windowSize);
    for (int i = 0; i < numValues; i++) {

      double randValue = randomDouble();
      double expected = 0;

      if (i == 0) {
        window.offer(randValue);
        continue;
      }

      for (double value : window) {
        expected += value;
      }
      expected /= window.size();

      double actual = model.next(window);
      assertThat(Double.compare(expected, actual), equalTo(0));
      window.offer(randValue);
    }
  }
  @Test
  public void testEWMAMovAvgModel() {
    double alpha = randomDouble();
    MovAvgModel model = new EwmaModel(alpha);

    int numValues = randomIntBetween(1, 100);
    int windowSize = randomIntBetween(1, 50);

    EvictingQueue<Double> window = EvictingQueue.create(windowSize);
    for (int i = 0; i < numValues; i++) {
      double randValue = randomDouble();

      if (i == 0) {
        window.offer(randValue);
        continue;
      }

      double avg = 0;
      boolean first = true;

      for (double value : window) {
        if (first) {
          avg = value;
          first = false;
        } else {
          avg = (value * alpha) + (avg * (1 - alpha));
        }
      }
      double expected = avg;
      double actual = model.next(window);
      assertThat(Double.compare(expected, actual), equalTo(0));
      window.offer(randValue);
    }
  }
  @Test
  public void testLinearPredictionModel() {
    MovAvgModel model = new LinearModel();

    int windowSize = randomIntBetween(1, 50);
    int numPredictions = randomIntBetween(1, 50);

    EvictingQueue<Double> window = EvictingQueue.create(windowSize);
    for (int i = 0; i < windowSize; i++) {
      window.offer(randomDouble());
    }
    double actual[] = model.predict(window, numPredictions);
    double expected[] = new double[numPredictions];

    double avg = 0;
    long totalWeight = 1;
    long current = 1;

    for (double value : window) {
      avg += value * current;
      totalWeight += current;
      current += 1;
    }
    avg = avg / totalWeight;
    Arrays.fill(expected, avg);

    for (int i = 0; i < numPredictions; i++) {
      assertThat(Double.compare(expected[i], actual[i]), equalTo(0));
    }
  }
  @Test
  public void testHoltLinearMovAvgModel() {
    double alpha = randomDouble();
    double beta = randomDouble();
    MovAvgModel model = new HoltLinearModel(alpha, beta);

    int numValues = randomIntBetween(1, 100);
    int windowSize = randomIntBetween(1, 50);

    EvictingQueue<Double> window = EvictingQueue.create(windowSize);
    for (int i = 0; i < numValues; i++) {
      double randValue = randomDouble();

      if (i == 0) {
        window.offer(randValue);
        continue;
      }

      double s = 0;
      double last_s = 0;

      // Trend value
      double b = 0;
      double last_b = 0;
      int counter = 0;

      double last;
      for (double value : window) {
        last = value;
        if (counter == 1) {
          s = value;
          b = value - last;
        } else {
          s = alpha * value + (1.0d - alpha) * (last_s + last_b);
          b = beta * (s - last_s) + (1 - beta) * last_b;
        }

        counter += 1;
        last_s = s;
        last_b = b;
      }

      double expected = s + (0 * b);
      double actual = model.next(window);
      assertThat(Double.compare(expected, actual), equalTo(0));
      window.offer(randValue);
    }
  }
  @Test
  public void testHoltLinearPredictionModel() {
    double alpha = randomDouble();
    double beta = randomDouble();
    MovAvgModel model = new HoltLinearModel(alpha, beta);

    int windowSize = randomIntBetween(1, 50);
    int numPredictions = randomIntBetween(1, 50);

    EvictingQueue<Double> window = EvictingQueue.create(windowSize);
    for (int i = 0; i < windowSize; i++) {
      window.offer(randomDouble());
    }
    double actual[] = model.predict(window, numPredictions);
    double expected[] = new double[numPredictions];

    double s = 0;
    double last_s = 0;

    // Trend value
    double b = 0;
    double last_b = 0;
    int counter = 0;

    double last;
    for (double value : window) {
      last = value;
      if (counter == 1) {
        s = value;
        b = value - last;
      } else {
        s = alpha * value + (1.0d - alpha) * (last_s + last_b);
        b = beta * (s - last_s) + (1 - beta) * last_b;
      }

      counter += 1;
      last_s = s;
      last_b = b;
    }

    for (int i = 0; i < numPredictions; i++) {
      expected[i] = s + (i * b);
      assertThat(Double.compare(expected[i], actual[i]), equalTo(0));
    }
  }
  @Test
  public void testHoltWintersAdditivePredictionModel() {
    double alpha = randomDouble();
    double beta = randomDouble();
    double gamma = randomDouble();
    int period = randomIntBetween(1, 10);
    MovAvgModel model =
        new HoltWintersModel(
            alpha, beta, gamma, period, HoltWintersModel.SeasonalityType.ADDITIVE, false);

    int windowSize = randomIntBetween(period * 2, 50); // HW requires at least two periods of data
    int numPredictions = randomIntBetween(1, 50);

    EvictingQueue<Double> window = EvictingQueue.create(windowSize);
    for (int i = 0; i < windowSize; i++) {
      window.offer(randomDouble());
    }
    double actual[] = model.predict(window, numPredictions);
    double expected[] = new double[numPredictions];

    // Smoothed value
    double s = 0;
    double last_s = 0;

    // Trend value
    double b = 0;
    double last_b = 0;

    // Seasonal value
    double[] seasonal = new double[windowSize];

    int counter = 0;
    double[] vs = new double[windowSize];
    for (double v : window) {
      vs[counter] = v;
      counter += 1;
    }

    // Initial level value is average of first season
    // Calculate the slopes between first and second season for each period
    for (int i = 0; i < period; i++) {
      s += vs[i];
      b += (vs[i + period] - vs[i]) / period;
    }
    s /= (double) period;
    b /= (double) period;
    last_s = s;

    // Calculate first seasonal
    if (Double.compare(s, 0.0) == 0 || Double.compare(s, -0.0) == 0) {
      Arrays.fill(seasonal, 0.0);
    } else {
      for (int i = 0; i < period; i++) {
        seasonal[i] = vs[i] / s;
      }
    }

    for (int i = period; i < vs.length; i++) {
      s = alpha * (vs[i] - seasonal[i - period]) + (1.0d - alpha) * (last_s + last_b);
      b = beta * (s - last_s) + (1 - beta) * last_b;

      seasonal[i] = gamma * (vs[i] - (last_s - last_b)) + (1 - gamma) * seasonal[i - period];
      last_s = s;
      last_b = b;
    }

    for (int i = 1; i <= numPredictions; i++) {
      int idx = window.size() - period + ((i - 1) % period);
      expected[i - 1] = s + (i * b) + seasonal[idx];
      assertThat(Double.compare(expected[i - 1], actual[i - 1]), equalTo(0));
    }
  }
  @Test
  public void testHoltWintersMultiplicativePadModel() {
    double alpha = randomDouble();
    double beta = randomDouble();
    double gamma = randomDouble();
    int period = randomIntBetween(1, 10);
    MovAvgModel model =
        new HoltWintersModel(
            alpha, beta, gamma, period, HoltWintersModel.SeasonalityType.MULTIPLICATIVE, true);

    int windowSize = randomIntBetween(period * 2, 50); // HW requires at least two periods of data

    EvictingQueue<Double> window = EvictingQueue.create(windowSize);
    for (int i = 0; i < windowSize; i++) {
      window.offer(randomDouble());
    }

    // Smoothed value
    double s = 0;
    double last_s = 0;

    // Trend value
    double b = 0;
    double last_b = 0;

    // Seasonal value
    double[] seasonal = new double[windowSize];

    int counter = 0;
    double[] vs = new double[windowSize];
    for (double v : window) {
      vs[counter] = v + 0.0000000001;
      counter += 1;
    }

    // Initial level value is average of first season
    // Calculate the slopes between first and second season for each period
    for (int i = 0; i < period; i++) {
      s += vs[i];
      b += (vs[i + period] - vs[i]) / period;
    }
    s /= (double) period;
    b /= (double) period;
    last_s = s;

    // Calculate first seasonal
    if (Double.compare(s, 0.0) == 0 || Double.compare(s, -0.0) == 0) {
      Arrays.fill(seasonal, 0.0);
    } else {
      for (int i = 0; i < period; i++) {
        seasonal[i] = vs[i] / s;
      }
    }

    for (int i = period; i < vs.length; i++) {
      s = alpha * (vs[i] / seasonal[i - period]) + (1.0d - alpha) * (last_s + last_b);
      b = beta * (s - last_s) + (1 - beta) * last_b;

      seasonal[i] = gamma * (vs[i] / (last_s + last_b)) + (1 - gamma) * seasonal[i - period];
      last_s = s;
      last_b = b;
    }

    int idx = window.size() - period + (0 % period);
    double expected = (s + (1 * b)) * seasonal[idx];
    double actual = model.next(window);
    assertThat(Double.compare(expected, actual), equalTo(0));
  }