/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.datasketches.thetacommon;

import static org.apache.datasketches.thetacommon.BinomialBoundsN.checkArgs;
import static org.apache.datasketches.thetacommon.BinomialBoundsN.getLowerBound;
import static org.apache.datasketches.thetacommon.BinomialBoundsN.getUpperBound;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

import org.apache.datasketches.common.SketchesArgumentException;
import org.apache.datasketches.thetacommon.BinomialBoundsN;
import org.testng.annotations.Test;

/**
 * @author Kevin Lang
 */
public class BinomialBoundsNTest {

  public static double[] runTestAux(final long max_numSamplesI, final int ci, final double min_p) {
    long numSamplesI = 0;
    double p, lb, ub;
    double sum1 = 0.0;
    double sum2 = 0.0;
    double sum3 = 0.0;
    double sum4 = 0.0;
    long count = 0;

    while (numSamplesI <= max_numSamplesI) { /* was <= */
      p = 1.0;

      while (p >= min_p) {
        lb = BinomialBoundsN.getLowerBound(numSamplesI, p, ci, false);
        ub = BinomialBoundsN.getUpperBound(numSamplesI, p, ci, false);

        // if (numSamplesI == 300 && p > 0.365 && p < 0.367) { ub += 0.01; }  // artificial discrepancy

        // the logarithm helps discrepancies to not be swamped out of the total
        sum1 += Math.log(lb + 1.0);
        sum2 += Math.log(ub + 1.0);
        count += 2;

        if (p < 1.0) {
          lb = BinomialBoundsN.getLowerBound(numSamplesI, 1.0 - p, ci, false);
          ub = BinomialBoundsN.getUpperBound(numSamplesI, 1.0 - p, ci, false);
          sum3 += Math.log(lb + 1.0);
          sum4 += Math.log(ub + 1.0);
          count += 2;
        }

        p *= 0.99;
      }
      numSamplesI = Math.max(numSamplesI + 1, (1001 * numSamplesI) / 1000);
    }

    println(String.format("{%.15e, %.15e, %.15e, %.15e, %d}", sum1, sum2, sum3, sum4, count));
    final double[] arrOut = {sum1, sum2, sum3, sum4, count};
    return arrOut;
  }

  private static final double TOL = 1E-15;

  @Test
  public static void checkBounds() {
    int i = 0;
    for (int ci = 1; ci <= 3; ci++, i++) {
      final double[] arr = runTestAux(20, ci, 1e-3);
      for (int j = 0; j < 5; j++) {
        assertTrue(((arr[j] / std[i][j]) - 1.0) < TOL);
      }
    }
    for (int ci = 1; ci <= 3; ci++, i++) {
      final double[] arr = runTestAux(200, ci, 1e-5);
      for (int j = 0; j < 5; j++) {
        assertTrue(((arr[j] / std[i][j]) - 1.0) < TOL);
      }
    }
    //comment last one out for a shorter test
    //  for (int ci = 1; ci <= 3; ci++, i++) {
    //    final double[] arr = runTestAux(2000, ci, 1e-7);
    //    for (int j = 0; j < 5; j++) {
    //      assertTrue(((arr[j] / std[i][j]) - 1.0) < TOL);
    //  }
    //}
  }

  // With all 3 enabled the test should produce in groups of 3 */
  private static final double[][] std = {
    {7.083330682531043e+04, 8.530373642825481e+04, 3.273647725073409e+04, 3.734024243699785e+04, 57750},
    {6.539415269641498e+04, 8.945522372568645e+04, 3.222302546497840e+04, 3.904738469737429e+04, 57750},
    {6.006043493107306e+04, 9.318105731423477e+04, 3.186269956585285e+04, 4.096466221922520e+04, 57750},

    {2.275584770163813e+06, 2.347586549014998e+06, 1.020399409477305e+06, 1.036729927598294e+06, 920982},
    {2.243569126699713e+06, 2.374663344107342e+06, 1.017017233582122e+06, 1.042597845553438e+06, 920982},
    {2.210056231903739e+06, 2.400441267999687e+06, 1.014081235946986e+06, 1.049480769755676e+06, 920982},

    {4.688240115809608e+07, 4.718067204619278e+07, 2.148362024482338e+07, 2.153118905212302e+07, 12834414},
    {4.674205938540214e+07, 4.731333757486791e+07, 2.146902141966406e+07, 2.154916650733873e+07, 12834414},
    {4.659896614422579e+07, 4.744404182094614e+07, 2.145525391547799e+07, 2.156815612325058e+07, 12834414}
  };

  @Test
  public static void checkCheckArgs() {
    try {
      checkArgs(-1L, 1.0, 1);
      checkArgs(10L, 0.0, 1);
      checkArgs(10L, 1.01, 1);
      checkArgs(10L, 1.0, 3);
      checkArgs(10L, 1.0, 0);
      checkArgs(10L, 1.0, 4);
      fail("Expected SketchesArgumentException");
    } catch (final SketchesArgumentException e) {
      //pass
    }
  }

  @Test
  public static void checkComputeApproxBino_LB_UB() {
    final long n = 100;
    final double theta = (2.0 - 1e-5) / 2.0;
    double result = getLowerBound(n, theta, 1, false);
    assertEquals(result, n, 0.0);
    result = getUpperBound(n, theta, 1, false);
    assertEquals(result, n + 1, 0.0);
    result = getLowerBound(n, theta, 1, true);
    assertEquals(result, 0.0, 0.0);
    result = getUpperBound(n, theta, 1, true);
    assertEquals(result, 0.0, 0.0);
  }

  @Test(expectedExceptions = SketchesArgumentException.class)
  public static void checkThetaLimits1() {
    BinomialBoundsN.getUpperBound(100, 1.1, 1, false);
  }

  @Test
  public static void boundsExample() {
    println("BinomialBoundsN Example:");
    final int k = 500;
    final double theta = 0.001;
    final int stdDev = 2;
    final double ub = BinomialBoundsN.getUpperBound(k, theta, stdDev, false);
    final double est = k / theta;
    final double lb = BinomialBoundsN.getLowerBound(k, theta, stdDev, false);
    println("K=" + k + ", Theta=" + theta + ", SD=" + stdDev);
    println("UB:  " + ub);
    println("Est: " + est);
    println("LB:  " + lb);
    println("");
  }

  @Test
  public void printlnTest() {
    println("PRINTING: " + this.getClass().getName());
  }

  /**
   * @param s value to print
   */
  static void println(final String s) {
    //System.out.println(s); //disable here
  }

}
