/*
 * Copyright (C) 2011 The Guava Authors
 *
 * Licensed 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 com.google.common.util.concurrent;

import static com.google.common.collect.Iterables.concat;

import com.google.common.base.Functions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.testing.GcFinalization;
import com.google.common.testing.NullPointerTester;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import junit.framework.TestCase;
import org.jspecify.annotations.NullUnmarked;

/**
 * Tests for Striped.
 *
 * @author Dimitris Andreou
 */
@NullUnmarked
public class StripedTest extends TestCase {
  private static List<Striped<?>> strongImplementations() {
    return ImmutableList.of(
        Striped.readWriteLock(100),
        Striped.readWriteLock(256),
        Striped.lock(100),
        Striped.lock(256),
        Striped.custom(100, FAIR_LOCK_SUPPLER),
        Striped.custom(256, FAIR_LOCK_SUPPLER),
        Striped.semaphore(100, 1),
        Striped.semaphore(256, 1));
  }

  private static final Supplier<ReadWriteLock> READ_WRITE_LOCK_SUPPLIER =
      new Supplier<ReadWriteLock>() {
        @Override
        public ReadWriteLock get() {
          return new ReentrantReadWriteLock();
        }
      };

  private static final Supplier<Lock> LOCK_SUPPLER =
      new Supplier<Lock>() {
        @Override
        public Lock get() {
          return new ReentrantLock();
        }
      };

  private static final Supplier<Lock> FAIR_LOCK_SUPPLER =
      new Supplier<Lock>() {
        @Override
        public Lock get() {
          return new ReentrantLock(true);
        }
      };

  private static final Supplier<Semaphore> SEMAPHORE_SUPPLER =
      new Supplier<Semaphore>() {
        @Override
        public Semaphore get() {
          return new Semaphore(1, false);
        }
      };

  private static List<Striped<?>> weakImplementations() {
    return ImmutableList.<Striped<?>>builder()
        .add(new Striped.SmallLazyStriped<ReadWriteLock>(50, READ_WRITE_LOCK_SUPPLIER))
        .add(new Striped.SmallLazyStriped<ReadWriteLock>(64, READ_WRITE_LOCK_SUPPLIER))
        .add(new Striped.LargeLazyStriped<ReadWriteLock>(50, READ_WRITE_LOCK_SUPPLIER))
        .add(new Striped.LargeLazyStriped<ReadWriteLock>(64, READ_WRITE_LOCK_SUPPLIER))
        .add(new Striped.SmallLazyStriped<Lock>(50, LOCK_SUPPLER))
        .add(new Striped.SmallLazyStriped<Lock>(64, LOCK_SUPPLER))
        .add(new Striped.LargeLazyStriped<Lock>(50, LOCK_SUPPLER))
        .add(new Striped.LargeLazyStriped<Lock>(64, LOCK_SUPPLER))
        .add(new Striped.SmallLazyStriped<Semaphore>(50, SEMAPHORE_SUPPLER))
        .add(new Striped.SmallLazyStriped<Semaphore>(64, SEMAPHORE_SUPPLER))
        .add(new Striped.LargeLazyStriped<Semaphore>(50, SEMAPHORE_SUPPLER))
        .add(new Striped.LargeLazyStriped<Semaphore>(64, SEMAPHORE_SUPPLER))
        .add(Striped.lazyWeakCustom(50, FAIR_LOCK_SUPPLER))
        .add(Striped.lazyWeakCustom(64, FAIR_LOCK_SUPPLER))
        .build();
  }

  private static Iterable<Striped<?>> allImplementations() {
    return concat(strongImplementations(), weakImplementations());
  }

  public void testNull() throws Exception {
    for (Striped<?> striped : allImplementations()) {
      new NullPointerTester().testAllPublicInstanceMethods(striped);
    }
  }

  public void testSizes() {
    // not bothering testing all variations, since we know they share implementations
    assertTrue(Striped.lock(100).size() >= 100);
    assertTrue(Striped.lock(256).size() == 256);
    assertTrue(Striped.lazyWeakLock(100).size() >= 100);
    assertTrue(Striped.lazyWeakLock(256).size() == 256);
  }

  @AndroidIncompatible // Presumably GC doesn't trigger, despite our efforts.
  public void testWeakImplementations() {
    for (Striped<?> striped : weakImplementations()) {
      WeakReference<Object> weakRef = new WeakReference<>(striped.get(new Object()));
      GcFinalization.awaitClear(weakRef);
    }
  }

  @AndroidIncompatible // Presumably GC doesn't trigger, despite our efforts.
  public void testWeakReadWrite() {
    Striped<ReadWriteLock> striped = Striped.lazyWeakReadWriteLock(1000);
    Object key = new Object();
    Lock readLock = striped.get(key).readLock();
    WeakReference<Object> garbage = new WeakReference<>(new Object());
    GcFinalization.awaitClear(garbage);
    Lock writeLock = striped.get(key).writeLock();
    readLock.lock();
    assertFalse(writeLock.tryLock());
    readLock.unlock();
  }

  @AndroidIncompatible // Presumably GC doesn't trigger, despite our efforts.
  public void testStrongImplementations() {
    for (Striped<?> striped : strongImplementations()) {
      WeakReference<Object> weakRef = new WeakReference<>(striped.get(new Object()));
      WeakReference<Object> garbage = new WeakReference<>(new Object());
      GcFinalization.awaitClear(garbage);
      assertNotNull(weakRef.get());
    }
  }

  public void testMaximalWeakStripedLock() {
    Striped<Lock> stripedLock = Striped.lazyWeakLock(Integer.MAX_VALUE);
    for (int i = 0; i < 10000; i++) {
      stripedLock.get(new Object()).lock();
      // nothing special (e.g. an exception) happens
    }
  }

  public void testBulkGetReturnsSorted() {
    for (Striped<?> striped : allImplementations()) {
      Map<Object, Integer> indexByLock = new HashMap<>();
      for (int i = 0; i < striped.size(); i++) {
        indexByLock.put(striped.getAt(i), i);
      }

      // ensure that bulkGet returns locks in monotonically increasing order
      for (int objectsNum = 1; objectsNum <= striped.size() * 2; objectsNum++) {
        Set<Object> objects = Sets.newHashSetWithExpectedSize(objectsNum);
        for (int i = 0; i < objectsNum; i++) {
          objects.add(new Object());
        }

        Iterable<?> locks = striped.bulkGet(objects);
        assertTrue(Ordering.natural().onResultOf(Functions.forMap(indexByLock)).isOrdered(locks));

        // check idempotency
        Iterable<?> locks2 = striped.bulkGet(objects);
        assertEquals(Lists.newArrayList(locks), Lists.newArrayList(locks2));
      }
    }
  }

  /** Checks idempotency, and that we observe the promised number of stripes. */
  public void testBasicInvariants() {
    for (Striped<?> striped : allImplementations()) {
      assertBasicInvariants(striped);
    }
  }

  private static void assertBasicInvariants(Striped<?> striped) {
    Set<Object> observed = Sets.newIdentityHashSet(); // for the sake of weakly referenced locks.
    // this gets the stripes with #getAt(index)
    for (int i = 0; i < striped.size(); i++) {
      Object object = striped.getAt(i);
      assertNotNull(object);
      assertSame(object, striped.getAt(i)); // idempotent
      observed.add(object);
    }
    assertTrue("All stripes observed", observed.size() == striped.size());

    // this uses #get(key), makes sure an already observed stripe is returned
    for (int i = 0; i < striped.size() * 100; i++) {
      assertTrue(observed.contains(striped.get(new Object())));
    }

    try {
      striped.getAt(-1);
      fail();
    } catch (RuntimeException expected) {
    }

    try {
      striped.getAt(striped.size());
      fail();
    } catch (RuntimeException expected) {
    }
  }

  public void testMaxSize() {
    for (Striped<?> striped :
        ImmutableList.of(
            Striped.lazyWeakLock(Integer.MAX_VALUE),
            Striped.lazyWeakSemaphore(Integer.MAX_VALUE, Integer.MAX_VALUE),
            Striped.lazyWeakReadWriteLock(Integer.MAX_VALUE))) {
      for (int i = 0; i < 3; i++) {
        // doesn't throw exception
        Object unused = striped.getAt(Integer.MAX_VALUE - i);
      }
    }
  }
}
