/*
 * Written by Doug Lea and Martin Buchholz with assistance from
 * members of JCP JSR-166 Expert Group and released to the public
 * domain, as explained at
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

/*
 * Source:
 * http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/test/tck-jsr166e/AtomicDoubleArrayTest.java?revision=1.13
 * (Modified to adapt to guava coding conventions)
 */

package com.google.common.util.concurrent;

import static java.lang.Math.max;
import static org.junit.Assert.assertThrows;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.J2ktIncompatible;
import com.google.common.testing.NullPointerTester;
import java.util.Arrays;
import org.jspecify.annotations.NullUnmarked;

/** Unit test for {@link AtomicDoubleArray}. */
@NullUnmarked
public class AtomicDoubleArrayTest extends JSR166TestCase {

  private static final double[] VALUES = {
    Double.NEGATIVE_INFINITY,
    -Double.MAX_VALUE,
    (double) Long.MIN_VALUE,
    (double) Integer.MIN_VALUE,
    -Math.PI,
    -1.0,
    -Double.MIN_VALUE,
    -0.0,
    +0.0,
    Double.MIN_VALUE,
    1.0,
    Math.PI,
    (double) Integer.MAX_VALUE,
    (double) Long.MAX_VALUE,
    Double.MAX_VALUE,
    Double.POSITIVE_INFINITY,
    Double.NaN,
    Float.MAX_VALUE,
  };

  /** The notion of equality used by AtomicDoubleArray */
  static boolean bitEquals(double x, double y) {
    return Double.doubleToRawLongBits(x) == Double.doubleToRawLongBits(y);
  }

  static void assertBitEquals(double x, double y) {
    assertEquals(Double.doubleToRawLongBits(x), Double.doubleToRawLongBits(y));
  }

  @J2ktIncompatible
  @GwtIncompatible // NullPointerTester
  public void testNulls() {
    new NullPointerTester().testAllPublicStaticMethods(AtomicDoubleArray.class);
    new NullPointerTester().testAllPublicConstructors(AtomicDoubleArray.class);
    new NullPointerTester().testAllPublicInstanceMethods(new AtomicDoubleArray(1));
  }

  /** constructor creates array of given size with all elements zero */
  public void testConstructor() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i = 0; i < SIZE; i++) {
      assertBitEquals(0.0, aa.get(i));
    }
  }

  /** constructor with null array throws NPE */
  public void testConstructor2NPE() {
    double[] a = null;
    assertThrows(NullPointerException.class, () -> new AtomicDoubleArray(a));
  }

  /** constructor with array is of same size and has all elements */
  public void testConstructor2() {
    AtomicDoubleArray aa = new AtomicDoubleArray(VALUES);
    assertEquals(VALUES.length, aa.length());
    for (int i = 0; i < VALUES.length; i++) {
      assertBitEquals(VALUES[i], aa.get(i));
    }
  }

  /** constructor with empty array has size 0 and contains no elements */
  public void testConstructorEmptyArray() {
    AtomicDoubleArray aa = new AtomicDoubleArray(new double[0]);
    assertEquals(0, aa.length());
    assertThrows(IndexOutOfBoundsException.class, () -> aa.get(0));
  }

  /** constructor with length zero has size 0 and contains no elements */
  public void testConstructorZeroLength() {
    AtomicDoubleArray aa = new AtomicDoubleArray(0);
    assertEquals(0, aa.length());
    assertThrows(IndexOutOfBoundsException.class, () -> aa.get(0));
  }

  /** get and set for out of bound indices throw IndexOutOfBoundsException */
  public void testIndexing() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int index : new int[] {-1, SIZE}) {
      assertThrows(IndexOutOfBoundsException.class, () -> aa.get(index));
      assertThrows(IndexOutOfBoundsException.class, () -> aa.set(index, 1.0));
      assertThrows(IndexOutOfBoundsException.class, () -> aa.lazySet(index, 1.0));
      assertThrows(IndexOutOfBoundsException.class, () -> aa.compareAndSet(index, 1.0, 2.0));
      assertThrows(IndexOutOfBoundsException.class, () -> aa.weakCompareAndSet(index, 1.0, 2.0));
      assertThrows(IndexOutOfBoundsException.class, () -> aa.getAndAdd(index, 1.0));
      assertThrows(IndexOutOfBoundsException.class, () -> aa.addAndGet(index, 1.0));
    }
  }

  /** get returns the last value set at index */
  public void testGetSet() {
    AtomicDoubleArray aa = new AtomicDoubleArray(VALUES.length);
    for (int i = 0; i < VALUES.length; i++) {
      assertBitEquals(0.0, aa.get(i));
      aa.set(i, VALUES[i]);
      assertBitEquals(VALUES[i], aa.get(i));
      aa.set(i, -3.0);
      assertBitEquals(-3.0, aa.get(i));
    }
  }

  /** get returns the last value lazySet at index by same thread */
  public void testGetLazySet() {
    AtomicDoubleArray aa = new AtomicDoubleArray(VALUES.length);
    for (int i = 0; i < VALUES.length; i++) {
      assertBitEquals(0.0, aa.get(i));
      aa.lazySet(i, VALUES[i]);
      assertBitEquals(VALUES[i], aa.get(i));
      aa.lazySet(i, -3.0);
      assertBitEquals(-3.0, aa.get(i));
    }
  }

  /** compareAndSet succeeds in changing value if equal to expected else fails */
  public void testCompareAndSet() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      double prev = 0.0;
      double unused = Math.E + Math.PI;
      for (double x : VALUES) {
        assertBitEquals(prev, aa.get(i));
        assertFalse(aa.compareAndSet(i, unused, x));
        assertBitEquals(prev, aa.get(i));
        assertTrue(aa.compareAndSet(i, prev, x));
        assertBitEquals(x, aa.get(i));
        prev = x;
      }
    }
  }

  /** compareAndSet in one thread enables another waiting for value to succeed */
  public void testCompareAndSetInMultipleThreads() throws InterruptedException {
    AtomicDoubleArray a = new AtomicDoubleArray(1);
    a.set(0, 1.0);
    Thread t =
        newStartedThread(
            new CheckedRunnable() {
              @Override
              @SuppressWarnings("ThreadPriorityCheck") // doing our best to test for races
              public void realRun() {
                while (!a.compareAndSet(0, 2.0, 3.0)) {
                  Thread.yield();
                }
              }
            });

    assertTrue(a.compareAndSet(0, 1.0, 2.0));
    awaitTermination(t);
    assertBitEquals(3.0, a.get(0));
  }

  /** repeated weakCompareAndSet succeeds in changing value when equal to expected */
  public void testWeakCompareAndSet() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      double prev = 0.0;
      double unused = Math.E + Math.PI;
      for (double x : VALUES) {
        assertBitEquals(prev, aa.get(i));
        assertFalse(aa.weakCompareAndSet(i, unused, x));
        assertBitEquals(prev, aa.get(i));
        while (!aa.weakCompareAndSet(i, prev, x)) {
          ;
        }
        assertBitEquals(x, aa.get(i));
        prev = x;
      }
    }
  }

  /** getAndSet returns previous value and sets to given value at given index */
  public void testGetAndSet() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      double prev = 0.0;
      for (double x : VALUES) {
        assertBitEquals(prev, aa.getAndSet(i, x));
        prev = x;
      }
    }
  }

  /** getAndAdd returns previous value and adds given value */
  public void testGetAndAdd() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      for (double x : VALUES) {
        for (double y : VALUES) {
          aa.set(i, x);
          double z = aa.getAndAdd(i, y);
          assertBitEquals(x, z);
          assertBitEquals(x + y, aa.get(i));
        }
      }
    }
  }

  /** addAndGet adds given value to current, and returns current value */
  public void testAddAndGet() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      for (double x : VALUES) {
        for (double y : VALUES) {
          aa.set(i, x);
          double z = aa.addAndGet(i, y);
          assertBitEquals(x + y, z);
          assertBitEquals(x + y, aa.get(i));
        }
      }
    }
  }

  /** getAndAccumulate with sum adds given value to current, and returns previous value */
  public void testGetAndAccumulateWithSum() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      for (double x : VALUES) {
        for (double y : VALUES) {
          aa.set(i, x);
          double z = aa.getAndAccumulate(i, y, Double::sum);
          assertBitEquals(x, z);
          assertBitEquals(x + y, aa.get(i));
        }
      }
    }
  }

  /** getAndAccumulate with max stores max of given value to current, and returns previous value */
  public void testGetAndAccumulateWithMax() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      for (double x : VALUES) {
        for (double y : VALUES) {
          aa.set(i, x);
          double z = aa.getAndAccumulate(i, y, Double::max);
          double expectedMax = max(x, y);
          assertBitEquals(x, z);
          assertBitEquals(expectedMax, aa.get(i));
        }
      }
    }
  }

  /** accumulateAndGet with sum adds given value to current, and returns current value */
  public void testAccumulateAndGetWithSum() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      for (double x : VALUES) {
        for (double y : VALUES) {
          aa.set(i, x);
          double z = aa.accumulateAndGet(i, y, Double::sum);
          assertBitEquals(x + y, z);
          assertBitEquals(x + y, aa.get(i));
        }
      }
    }
  }

  /** accumulateAndGet with max stores max of given value to current, and returns current value */
  public void testAccumulateAndGetWithMax() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      for (double x : VALUES) {
        for (double y : VALUES) {
          aa.set(i, x);
          double z = aa.accumulateAndGet(i, y, Double::max);
          double expectedMax = max(x, y);
          assertBitEquals(expectedMax, z);
          assertBitEquals(expectedMax, aa.get(i));
        }
      }
    }
  }

  /** getAndUpdate adds given value to current, and returns previous value */
  public void testGetAndUpdateWithSum() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      for (double x : VALUES) {
        for (double y : VALUES) {
          aa.set(i, x);
          double z = aa.getAndUpdate(i, value -> value + y);
          assertBitEquals(x, z);
          assertBitEquals(x + y, aa.get(i));
        }
      }
    }
  }

  /** getAndUpdate subtracts given value to current, and returns previous value */
  public void testGetAndUpdateWithSubtract() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      for (double x : VALUES) {
        for (double y : VALUES) {
          aa.set(i, x);
          double z = aa.getAndUpdate(i, value -> value - y);
          assertBitEquals(x, z);
          assertBitEquals(x - y, aa.get(i));
        }
      }
    }
  }

  /** updateAndGet adds given value to current, and returns current value */
  public void testUpdateAndGetWithSum() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      for (double x : VALUES) {
        for (double y : VALUES) {
          aa.set(i, x);
          double z = aa.updateAndGet(i, value -> value + y);
          assertBitEquals(x + y, z);
          assertBitEquals(x + y, aa.get(i));
        }
      }
    }
  }

  /** updateAndGet subtracts given value to current, and returns current value */
  public void testUpdateAndGetWithSubtract() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      for (double x : VALUES) {
        for (double y : VALUES) {
          aa.set(i, x);
          double z = aa.updateAndGet(i, value -> value - y);
          assertBitEquals(x - y, z);
          assertBitEquals(x - y, aa.get(i));
        }
      }
    }
  }

  static final long COUNTDOWN = 100000;

  class Counter extends CheckedRunnable {
    final AtomicDoubleArray aa;
    volatile long counts;

    Counter(AtomicDoubleArray a) {
      aa = a;
    }

    @Override
    public void realRun() {
      for (; ; ) {
        boolean done = true;
        for (int i = 0; i < aa.length(); i++) {
          double v = aa.get(i);
          assertTrue(v >= 0);
          if (v != 0) {
            done = false;
            if (aa.compareAndSet(i, v, v - 1.0)) {
              ++counts;
            }
          }
        }
        if (done) {
          break;
        }
      }
    }
  }

  /**
   * Multiple threads using same array of counters successfully update a number of times equal to
   * total count
   */
  public void testCountingInMultipleThreads() throws InterruptedException {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i = 0; i < SIZE; i++) {
      aa.set(i, (double) COUNTDOWN);
    }
    Counter c1 = new Counter(aa);
    Counter c2 = new Counter(aa);
    Thread t1 = newStartedThread(c1);
    Thread t2 = newStartedThread(c2);
    awaitTermination(t1);
    awaitTermination(t2);
    assertEquals(SIZE * COUNTDOWN, c1.counts + c2.counts);
  }

  /** a deserialized serialized array holds same values */
  public void testSerialization() throws Exception {
    AtomicDoubleArray x = new AtomicDoubleArray(SIZE);
    for (int i = 0; i < SIZE; i++) {
      x.set(i, (double) -i);
    }
    AtomicDoubleArray y = serialClone(x);
    assertTrue(x != y);
    assertEquals(x.length(), y.length());
    for (int i = 0; i < SIZE; i++) {
      assertBitEquals(x.get(i), y.get(i));
    }

    AtomicDoubleArray a = new AtomicDoubleArray(VALUES);
    AtomicDoubleArray b = serialClone(a);
    assertFalse(a.equals(b));
    assertFalse(b.equals(a));
    assertEquals(a.length(), b.length());
    for (int i = 0; i < VALUES.length; i++) {
      assertBitEquals(a.get(i), b.get(i));
    }
  }

  /** toString returns current value */
  public void testToString() {
    AtomicDoubleArray aa = new AtomicDoubleArray(VALUES);
    assertEquals(Arrays.toString(VALUES), aa.toString());
    assertEquals("[]", new AtomicDoubleArray(0).toString());
    assertEquals("[]", new AtomicDoubleArray(new double[0]).toString());
  }

  /** compareAndSet treats +0.0 and -0.0 as distinct values */
  public void testDistinctZeros() {
    AtomicDoubleArray aa = new AtomicDoubleArray(SIZE);
    for (int i : new int[] {0, SIZE - 1}) {
      assertFalse(aa.compareAndSet(i, -0.0, 7.0));
      assertFalse(aa.weakCompareAndSet(i, -0.0, 7.0));
      assertBitEquals(+0.0, aa.get(i));
      assertTrue(aa.compareAndSet(i, +0.0, -0.0));
      assertBitEquals(-0.0, aa.get(i));
      assertFalse(aa.compareAndSet(i, +0.0, 7.0));
      assertFalse(aa.weakCompareAndSet(i, +0.0, 7.0));
      assertBitEquals(-0.0, aa.get(i));
    }
  }
}
