/**
 *
 * 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.hadoop.hbase.util;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;

import junit.framework.Test;
import junit.framework.TestCase;

import org.apache.hadoop.hbase.SmallTests;
import org.apache.hadoop.hbase.util.PoolMap.PoolType;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({TestPoolMap.TestRoundRobinPoolType.class, TestPoolMap.TestThreadLocalPoolType.class, TestPoolMap.TestReusablePoolType.class})
@Category(SmallTests.class)
public class TestPoolMap {
  public abstract static class TestPoolType extends TestCase {
    protected PoolMap<String, String> poolMap;
    protected Random random = new Random();

    protected static final int POOL_SIZE = 3;

    @Override
    protected void setUp() throws Exception {
      this.poolMap = new PoolMap<String, String>(getPoolType(), POOL_SIZE);
    }

    protected abstract PoolType getPoolType();

    @Override
    protected void tearDown() throws Exception {
      this.poolMap.clear();
    }

    protected void runThread(final String randomKey, final String randomValue,
        final String expectedValue) throws InterruptedException {
      final AtomicBoolean matchFound = new AtomicBoolean(false);
      Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
          poolMap.put(randomKey, randomValue);
          String actualValue = poolMap.get(randomKey);
          matchFound.set(expectedValue == null ? actualValue == null
              : expectedValue.equals(actualValue));
        }
      });
      thread.start();
      thread.join();
      assertTrue(matchFound.get());
    }
  }

  @Category(SmallTests.class)
  public static class TestRoundRobinPoolType extends TestPoolType {
    @Override
    protected PoolType getPoolType() {
      return PoolType.RoundRobin;
    }

    public void testSingleThreadedClient() throws InterruptedException,
        ExecutionException {
      String randomKey = String.valueOf(random.nextInt());
      String randomValue = String.valueOf(random.nextInt());
      // As long as the pool is not full, we'll get null back.
      // This forces the user to create new values that can be used to populate
      // the pool.
      runThread(randomKey, randomValue, null);
      assertEquals(1, poolMap.size(randomKey));
    }

    public void testMultiThreadedClients() throws InterruptedException,
        ExecutionException {
      for (int i = 0; i < POOL_SIZE; i++) {
        String randomKey = String.valueOf(random.nextInt());
        String randomValue = String.valueOf(random.nextInt());
        // As long as the pool is not full, we'll get null back
        runThread(randomKey, randomValue, null);
        // As long as we use distinct keys, each pool will have one value
        assertEquals(1, poolMap.size(randomKey));
      }
      poolMap.clear();
      String randomKey = String.valueOf(random.nextInt());
      for (int i = 0; i < POOL_SIZE - 1; i++) {
        String randomValue = String.valueOf(random.nextInt());
        // As long as the pool is not full, we'll get null back
        runThread(randomKey, randomValue, null);
        // since we use the same key, the pool size should grow
        assertEquals(i + 1, poolMap.size(randomKey));
      }
      // at the end of the day, there should be as many values as we put
      assertEquals(POOL_SIZE - 1, poolMap.size(randomKey));
    }

    public void testPoolCap() throws InterruptedException, ExecutionException {
      String randomKey = String.valueOf(random.nextInt());
      List<String> randomValues = new ArrayList<String>();
      for (int i = 0; i < POOL_SIZE * 2; i++) {
        String randomValue = String.valueOf(random.nextInt());
        randomValues.add(randomValue);
        if (i < POOL_SIZE - 1) {
          // As long as the pool is not full, we'll get null back
          runThread(randomKey, randomValue, null);
        } else {
          // when the pool becomes full, we expect the value we get back to be
          // what we put earlier, in round-robin order
          runThread(randomKey, randomValue,
              randomValues.get((i - POOL_SIZE + 1) % POOL_SIZE));
        }
      }
      assertEquals(POOL_SIZE, poolMap.size(randomKey));
    }

  }

  @Category(SmallTests.class)
  public static class TestThreadLocalPoolType extends TestPoolType {
    @Override
    protected PoolType getPoolType() {
      return PoolType.ThreadLocal;
    }

    public void testSingleThreadedClient() throws InterruptedException,
        ExecutionException {
      String randomKey = String.valueOf(random.nextInt());
      String randomValue = String.valueOf(random.nextInt());
      // As long as the pool is not full, we should get back what we put
      runThread(randomKey, randomValue, randomValue);
      assertEquals(1, poolMap.size(randomKey));
    }

    public void testMultiThreadedClients() throws InterruptedException,
        ExecutionException {
      // As long as the pool is not full, we should get back what we put
      for (int i = 0; i < POOL_SIZE; i++) {
        String randomKey = String.valueOf(random.nextInt());
        String randomValue = String.valueOf(random.nextInt());
        runThread(randomKey, randomValue, randomValue);
        assertEquals(1, poolMap.size(randomKey));
      }
      String randomKey = String.valueOf(random.nextInt());
      for (int i = 0; i < POOL_SIZE; i++) {
        String randomValue = String.valueOf(random.nextInt());
        runThread(randomKey, randomValue, randomValue);
        assertEquals(i + 1, poolMap.size(randomKey));
      }
    }

    public void testPoolCap() throws InterruptedException, ExecutionException {
      String randomKey = String.valueOf(random.nextInt());
      for (int i = 0; i < POOL_SIZE * 2; i++) {
        String randomValue = String.valueOf(random.nextInt());
        // as of HBASE-4150, pool limit is no longer used with ThreadLocalPool
          runThread(randomKey, randomValue, randomValue);
      }
      assertEquals(POOL_SIZE * 2, poolMap.size(randomKey));
    }

  }

  @Category(SmallTests.class)
  public static class TestReusablePoolType extends TestPoolType {
    @Override
    protected PoolType getPoolType() {
      return PoolType.Reusable;
    }

    public void testSingleThreadedClient() throws InterruptedException,
        ExecutionException {
      String randomKey = String.valueOf(random.nextInt());
      String randomValue = String.valueOf(random.nextInt());
      // As long as we poll values we put, the pool size should remain zero
      runThread(randomKey, randomValue, randomValue);
      assertEquals(0, poolMap.size(randomKey));
    }

    public void testMultiThreadedClients() throws InterruptedException,
        ExecutionException {
      // As long as we poll values we put, the pool size should remain zero
      for (int i = 0; i < POOL_SIZE; i++) {
        String randomKey = String.valueOf(random.nextInt());
        String randomValue = String.valueOf(random.nextInt());
        runThread(randomKey, randomValue, randomValue);
        assertEquals(0, poolMap.size(randomKey));
      }
      poolMap.clear();
      String randomKey = String.valueOf(random.nextInt());
      for (int i = 0; i < POOL_SIZE - 1; i++) {
        String randomValue = String.valueOf(random.nextInt());
        runThread(randomKey, randomValue, randomValue);
        assertEquals(0, poolMap.size(randomKey));
      }
      assertEquals(0, poolMap.size(randomKey));
    }

    public void testPoolCap() throws InterruptedException, ExecutionException {
      // As long as we poll values we put, the pool size should remain zero
      String randomKey = String.valueOf(random.nextInt());
      List<String> randomValues = new ArrayList<String>();
      for (int i = 0; i < POOL_SIZE * 2; i++) {
        String randomValue = String.valueOf(random.nextInt());
        randomValues.add(randomValue);
        runThread(randomKey, randomValue, randomValue);
      }
      assertEquals(0, poolMap.size(randomKey));
    }

  }

}

