/*
 * 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.geode.cache.util;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.Sequence;
import org.jmock.api.Invocation;
import org.jmock.lib.action.CustomAction;
import org.jmock.lib.concurrent.Synchroniser;
import org.jmock.lib.legacy.ClassImposteriser;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.GemFireConfigException;
import org.apache.geode.cache.control.RebalanceFactory;
import org.apache.geode.cache.control.RebalanceOperation;
import org.apache.geode.cache.control.RebalanceResults;
import org.apache.geode.cache.partition.PartitionMemberInfo;
import org.apache.geode.cache.util.AutoBalancer.AuditScheduler;
import org.apache.geode.cache.util.AutoBalancer.CacheOperationFacade;
import org.apache.geode.cache.util.AutoBalancer.GeodeCacheFacade;
import org.apache.geode.cache.util.AutoBalancer.OOBAuditor;
import org.apache.geode.cache.util.AutoBalancer.SizeBasedOOBAuditor;
import org.apache.geode.cache.util.AutoBalancer.TimeProvider;
import org.apache.geode.internal.cache.GemFireCacheImpl;
import org.apache.geode.internal.cache.PRHARedundancyProvider;
import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.internal.cache.control.InternalResourceManager;
import org.apache.geode.internal.cache.partitioned.InternalPRInfo;
import org.apache.geode.internal.cache.partitioned.LoadProbe;
import org.apache.geode.test.junit.categories.IntegrationTest;

/**
 * UnitTests for AutoBalancer. All collaborators should be mocked.
 */
@Category(IntegrationTest.class)
public class AutoBalancerJUnitTest {
  Mockery mockContext;

  CacheOperationFacade mockCacheFacade;
  OOBAuditor mockAuditor;
  AuditScheduler mockScheduler;
  TimeProvider mockClock;

  @Before
  public void setupMock() {
    mockContext = new Mockery() {
      {
        setImposteriser(ClassImposteriser.INSTANCE);
        setThreadingPolicy(new Synchroniser());
      }
    };

    mockCacheFacade = mockContext.mock(CacheOperationFacade.class);
    mockAuditor = mockContext.mock(OOBAuditor.class);
    mockScheduler = mockContext.mock(AuditScheduler.class);
    mockClock = mockContext.mock(TimeProvider.class);
  }

  @After
  public void validateMock() {
    mockContext.assertIsSatisfied();
    mockContext = null;
  }

  @Test
  public void testLockStatExecuteInSequence() throws InterruptedException {
    final Sequence sequence = mockContext.sequence("sequence");
    mockContext.checking(new Expectations() {
      {
        oneOf(mockCacheFacade).acquireAutoBalanceLock();
        inSequence(sequence);
        will(returnValue(true));
        oneOf(mockCacheFacade).incrementAttemptCounter();
        inSequence(sequence);
        oneOf(mockCacheFacade).getTotalTransferSize();
        inSequence(sequence);
        will(returnValue(0L));
      }
    });

    AutoBalancer balancer = new AutoBalancer(null, null, null, mockCacheFacade);
    balancer.getOOBAuditor().execute();
  }

  @Test
  public void testAcquireLockAfterReleasedRemotely() throws InterruptedException {
    final Sequence sequence = mockContext.sequence("sequence");
    mockContext.checking(new Expectations() {
      {
        oneOf(mockCacheFacade).acquireAutoBalanceLock();
        inSequence(sequence);
        will(returnValue(false));
        oneOf(mockCacheFacade).acquireAutoBalanceLock();
        inSequence(sequence);
        will(returnValue(true));
        oneOf(mockCacheFacade).incrementAttemptCounter();
        oneOf(mockCacheFacade).getTotalTransferSize();
        will(returnValue(0L));
      }
    });

    AutoBalancer balancer = new AutoBalancer(null, null, null, mockCacheFacade);
    balancer.getOOBAuditor().execute();
    balancer.getOOBAuditor().execute();
  }

  @Test
  public void testFailExecuteIfLockedElsewhere() throws InterruptedException {
    mockContext.checking(new Expectations() {
      {
        oneOf(mockCacheFacade).acquireAutoBalanceLock();
        will(returnValue(false));
        // no other methods, rebalance, will be called
      }
    });

    AutoBalancer balancer = new AutoBalancer(null, null, null, mockCacheFacade);
    balancer.getOOBAuditor().execute();
  }

  @Test(expected = IllegalStateException.class)
  public void testNoCacheError() {
    AutoBalancer balancer = new AutoBalancer();
    OOBAuditor auditor = balancer.getOOBAuditor();
    auditor.execute();
  }

  @Test
  public void testOOBWhenBelowSizeThreshold() {
    final long totalSize = 1000L;

    final Map<PartitionedRegion, InternalPRInfo> details = new HashMap<>();
    mockContext.checking(new Expectations() {
      {
        allowing(mockCacheFacade).getRegionMemberDetails();
        will(returnValue(details));
        // first run
        oneOf(mockCacheFacade).getTotalDataSize(details);
        will(returnValue(totalSize));
        oneOf(mockCacheFacade).getTotalTransferSize();
        // half of threshold limit
        will(returnValue((AutoBalancer.DEFAULT_SIZE_THRESHOLD_PERCENT * totalSize / 100) / 2));

        // second run
        oneOf(mockCacheFacade).getTotalTransferSize();
        // nothing to transfer
        will(returnValue(0L));
      }
    });

    AutoBalancer balancer = new AutoBalancer(null, null, null, mockCacheFacade);
    Properties config = getBasicConfig();
    config.put(AutoBalancer.MINIMUM_SIZE, "10");
    balancer.init(config);
    SizeBasedOOBAuditor auditor = (SizeBasedOOBAuditor) balancer.getOOBAuditor();

    // first run
    assertFalse(auditor.needsRebalancing());

    // second run
    assertFalse(auditor.needsRebalancing());
  }

  @Test
  public void testOOBWhenAboveThresholdButBelowMin() {
    final long totalSize = 1000L;

    mockContext.checking(new Expectations() {
      {
        // first run
        oneOf(mockCacheFacade).getTotalTransferSize();
        // twice threshold
        will(returnValue((AutoBalancer.DEFAULT_SIZE_THRESHOLD_PERCENT * totalSize / 100) * 2));

        // second run
        oneOf(mockCacheFacade).getTotalTransferSize();
        // more than total size
        will(returnValue(2 * totalSize));
      }
    });

    AutoBalancer balancer = new AutoBalancer(null, null, null, mockCacheFacade);
    Properties config = getBasicConfig();
    config.put(AutoBalancer.MINIMUM_SIZE, "" + (totalSize * 5));
    balancer.init(config);
    SizeBasedOOBAuditor auditor = (SizeBasedOOBAuditor) balancer.getOOBAuditor();

    // first run
    assertFalse(auditor.needsRebalancing());

    // second run
    assertFalse(auditor.needsRebalancing());
  }

  @Test
  public void testOOBWhenAboveThresholdAndMin() {
    final long totalSize = 1000L;

    final Map<PartitionedRegion, InternalPRInfo> details = new HashMap<>();
    mockContext.checking(new Expectations() {
      {
        allowing(mockCacheFacade).getRegionMemberDetails();
        will(returnValue(details));

        // first run
        oneOf(mockCacheFacade).getTotalDataSize(details);
        will(returnValue(totalSize));
        oneOf(mockCacheFacade).getTotalTransferSize();
        // twice threshold
        will(returnValue((AutoBalancer.DEFAULT_SIZE_THRESHOLD_PERCENT * totalSize / 100) * 2));

        // second run
        oneOf(mockCacheFacade).getTotalDataSize(details);
        will(returnValue(totalSize));
        oneOf(mockCacheFacade).getTotalTransferSize();
        // more than total size
        will(returnValue(2 * totalSize));
      }
    });

    AutoBalancer balancer = new AutoBalancer(null, null, null, mockCacheFacade);
    Properties config = getBasicConfig();
    config.put(AutoBalancer.MINIMUM_SIZE, "10");
    balancer.init(config);
    SizeBasedOOBAuditor auditor = (SizeBasedOOBAuditor) balancer.getOOBAuditor();

    // first run
    assertTrue(auditor.needsRebalancing());

    // second run
    assertTrue(auditor.needsRebalancing());
  }

  @Test(expected = GemFireConfigException.class)
  public void testInvalidSchedule() {
    String someSchedule = "X Y * * * *";
    Properties props = new Properties();
    props.put(AutoBalancer.SCHEDULE, someSchedule);

    AutoBalancer autoR = new AutoBalancer();
    autoR.init(props);
  }

  @Test
  public void testOOBAuditorInit() {
    AutoBalancer balancer = new AutoBalancer();
    balancer.init(getBasicConfig());
    SizeBasedOOBAuditor auditor = (SizeBasedOOBAuditor) balancer.getOOBAuditor();
    assertEquals(AutoBalancer.DEFAULT_SIZE_THRESHOLD_PERCENT, auditor.getSizeThreshold());
    assertEquals(AutoBalancer.DEFAULT_MINIMUM_SIZE, auditor.getSizeMinimum());

    Properties props = getBasicConfig();
    props.put(AutoBalancer.SIZE_THRESHOLD_PERCENT, "17");
    props.put(AutoBalancer.MINIMUM_SIZE, "10");
    balancer = new AutoBalancer();
    balancer.init(props);
    auditor = (SizeBasedOOBAuditor) balancer.getOOBAuditor();
    assertEquals(17, auditor.getSizeThreshold());
    assertEquals(10, auditor.getSizeMinimum());
  }

  @Test(expected = GemFireConfigException.class)
  public void testConfigTransferThresholdNegative() {
    AutoBalancer balancer = new AutoBalancer();
    Properties props = getBasicConfig();
    props.put(AutoBalancer.SIZE_THRESHOLD_PERCENT, "-1");
    balancer.init(props);
  }

  @Test(expected = GemFireConfigException.class)
  public void testConfigSizeMinNegative() {
    AutoBalancer balancer = new AutoBalancer();
    Properties props = getBasicConfig();
    props.put(AutoBalancer.MINIMUM_SIZE, "-1");
    balancer.init(props);
  }

  @Test(expected = GemFireConfigException.class)
  public void testConfigTransferThresholdZero() {
    AutoBalancer balancer = new AutoBalancer();
    Properties props = getBasicConfig();
    props.put(AutoBalancer.SIZE_THRESHOLD_PERCENT, "0");
    balancer.init(props);
  }

  @Test(expected = GemFireConfigException.class)
  public void testConfigTransferThresholdTooHigh() {
    AutoBalancer balancer = new AutoBalancer();
    Properties props = getBasicConfig();
    props.put(AutoBalancer.SIZE_THRESHOLD_PERCENT, "100");
    balancer.init(props);
  }

  @Test
  public void testAutoBalancerInit() {
    final String someSchedule = "1 * * * 1 *";
    final Properties props = new Properties();
    props.put(AutoBalancer.SCHEDULE, someSchedule);
    props.put(AutoBalancer.SIZE_THRESHOLD_PERCENT, 17);

    mockContext.checking(new Expectations() {
      {
        oneOf(mockScheduler).init(someSchedule);
        oneOf(mockAuditor).init(props);
      }
    });

    AutoBalancer autoR = new AutoBalancer(mockScheduler, mockAuditor, null, null);
    autoR.init(props);
  }

  @Test
  public void testMinimalConfiguration() {
    AutoBalancer autoR = new AutoBalancer();
    try {
      autoR.init(null);
      fail();
    } catch (GemFireConfigException e) {
      // expected
    }

    Properties props = getBasicConfig();
    autoR.init(props);
  }

  @Test
  @Ignore("GEODE-2789: need to rewrite this test")
  public void testFacadeTotalTransferSize() throws Exception {
    assertEquals(12345, getFacadeForResourceManagerOps(true).getTotalTransferSize());
  }

  @Test
  @Ignore("GEODE-2789: need to rewrite this test")
  public void testFacadeRebalance() throws Exception {
    getFacadeForResourceManagerOps(false).rebalance();
  }

  private GeodeCacheFacade getFacadeForResourceManagerOps(final boolean simulate) throws Exception {
    final GemFireCacheImpl mockCache = mockContext.mock(GemFireCacheImpl.class);
    final InternalResourceManager mockRM = mockContext.mock(InternalResourceManager.class);
    final RebalanceFactory mockRebalanceFactory = mockContext.mock(RebalanceFactory.class);
    final RebalanceOperation mockRebalanceOperation = mockContext.mock(RebalanceOperation.class);
    final RebalanceResults mockRebalanceResults = mockContext.mock(RebalanceResults.class);

    mockContext.checking(new Expectations() {
      {
        oneOf(mockCache).isClosed();
        will(returnValue(false));
        oneOf(mockCache).getResourceManager();
        will(returnValue(mockRM));
        oneOf(mockRM).createRebalanceFactory();
        will(returnValue(mockRebalanceFactory));
        if (simulate) {
          oneOf(mockRebalanceFactory).simulate();
        } else {
          oneOf(mockRebalanceFactory).start();
        }
        will(returnValue(mockRebalanceOperation));
        oneOf(mockRebalanceOperation).getResults();
        will(returnValue(mockRebalanceResults));
        if (simulate) {
          atLeast(1).of(mockRebalanceResults).getTotalBucketTransferBytes();
          will(returnValue(12345L));
        }
        allowing(mockRebalanceResults);
      }
    });

    GeodeCacheFacade facade = new GeodeCacheFacade(mockCache);

    return facade;
  }

  @Test
  public void testFacadeTotalBytesNoRegion() {
    CacheOperationFacade facade = new AutoBalancer().getCacheOperationFacade();

    assertEquals(0, facade.getTotalDataSize(new HashMap<PartitionedRegion, InternalPRInfo>()));
  }

  @Test
  @Ignore("GEODE-2789: need to rewrite this test")
  public void testFacadeCollectMemberDetailsNoRegion() {
    final GemFireCacheImpl mockCache = mockContext.mock(GemFireCacheImpl.class);
    mockContext.checking(new Expectations() {
      {
        oneOf(mockCache).isClosed();
        will(returnValue(false));
        oneOf(mockCache).getPartitionedRegions();
        will(returnValue(new HashSet<PartitionedRegion>()));
      }
    });

    GeodeCacheFacade facade = new GeodeCacheFacade(mockCache);

    assertEquals(0, facade.getRegionMemberDetails().size());
  }

  @Test
  @Ignore("GEODE-2789: need to rewrite this test")
  public void testFacadeCollectMemberDetails2Regions() {
    final GemFireCacheImpl mockCache = mockContext.mock(GemFireCacheImpl.class);
    final InternalResourceManager mockRM = mockContext.mock(InternalResourceManager.class);
    final LoadProbe mockProbe = mockContext.mock(LoadProbe.class);

    final PartitionedRegion mockR1 = mockContext.mock(PartitionedRegion.class, "r1");
    final PartitionedRegion mockR2 = mockContext.mock(PartitionedRegion.class, "r2");
    final HashSet<PartitionedRegion> regions = new HashSet<>();
    regions.add(mockR1);
    regions.add(mockR2);

    final PRHARedundancyProvider mockRedundancyProviderR1 =
        mockContext.mock(PRHARedundancyProvider.class, "prhaR1");
    final InternalPRInfo mockR1PRInfo = mockContext.mock(InternalPRInfo.class, "prInforR1");

    final PRHARedundancyProvider mockRedundancyProviderR2 =
        mockContext.mock(PRHARedundancyProvider.class, "prhaR2");
    final InternalPRInfo mockR2PRInfo = mockContext.mock(InternalPRInfo.class, "prInforR2");

    mockContext.checking(new Expectations() {
      {
        oneOf(mockCache).isClosed();
        will(returnValue(false));
        oneOf(mockCache).getPartitionedRegions();
        will(returnValue(regions));
        exactly(2).of(mockCache).getResourceManager();
        will(returnValue(mockRM));
        exactly(2).of(mockRM).getLoadProbe();
        will(returnValue(mockProbe));
        allowing(mockR1).getFullPath();
        oneOf(mockR1).getRedundancyProvider();
        will(returnValue(mockRedundancyProviderR1));
        allowing(mockR2).getFullPath();
        oneOf(mockR2).getRedundancyProvider();
        will(returnValue(mockRedundancyProviderR2));

        oneOf(mockRedundancyProviderR1).buildPartitionedRegionInfo(with(true),
            with(any(LoadProbe.class)));
        will(returnValue(mockR1PRInfo));

        oneOf(mockRedundancyProviderR2).buildPartitionedRegionInfo(with(true),
            with(any(LoadProbe.class)));
        will(returnValue(mockR2PRInfo));
      }
    });

    GeodeCacheFacade facade = new GeodeCacheFacade(mockCache);

    Map<PartitionedRegion, InternalPRInfo> map = facade.getRegionMemberDetails();
    assertNotNull(map);
    assertEquals(2, map.size());
    assertEquals(map.get(mockR1), mockR1PRInfo);
    assertEquals(map.get(mockR2), mockR2PRInfo);
  }

  @Test
  @Ignore("GEODE-2789: need to rewrite this test")
  public void testFacadeTotalBytes2Regions() {
    final PartitionedRegion mockR1 = mockContext.mock(PartitionedRegion.class, "r1");
    final PartitionedRegion mockR2 = mockContext.mock(PartitionedRegion.class, "r2");
    final HashSet<PartitionedRegion> regions = new HashSet<>();
    regions.add(mockR1);
    regions.add(mockR2);

    final InternalPRInfo mockR1PRInfo = mockContext.mock(InternalPRInfo.class, "prInforR1");
    final PartitionMemberInfo mockR1M1Info = mockContext.mock(PartitionMemberInfo.class, "r1M1");
    final PartitionMemberInfo mockR1M2Info = mockContext.mock(PartitionMemberInfo.class, "r1M2");
    final HashSet<PartitionMemberInfo> r1Members = new HashSet<>();
    r1Members.add(mockR1M1Info);
    r1Members.add(mockR1M2Info);

    final InternalPRInfo mockR2PRInfo = mockContext.mock(InternalPRInfo.class, "prInforR2");
    final PartitionMemberInfo mockR2M1Info = mockContext.mock(PartitionMemberInfo.class, "r2M1");
    final HashSet<PartitionMemberInfo> r2Members = new HashSet<>();
    r2Members.add(mockR2M1Info);

    final Map<PartitionedRegion, InternalPRInfo> details = new HashMap<>();
    details.put(mockR1, mockR1PRInfo);
    details.put(mockR2, mockR2PRInfo);

    mockContext.checking(new Expectations() {
      {
        allowing(mockR1).getFullPath();
        allowing(mockR2).getFullPath();

        oneOf(mockR1PRInfo).getPartitionMemberInfo();
        will(returnValue(r1Members));
        atLeast(1).of(mockR1M1Info).getSize();
        will(returnValue(123L));
        atLeast(1).of(mockR1M2Info).getSize();
        will(returnValue(74L));

        oneOf(mockR2PRInfo).getPartitionMemberInfo();
        will(returnValue(r2Members));
        atLeast(1).of(mockR2M1Info).getSize();
        will(returnValue(3475L));
      }
    });

    GeodeCacheFacade facade = new GeodeCacheFacade() {
      @Override
      public Map<PartitionedRegion, InternalPRInfo> getRegionMemberDetails() {
        return details;
      }
    };

    assertEquals(123 + 74 + 3475, facade.getTotalDataSize(details));
  }

  @Test
  public void testAuditorInvocation() throws InterruptedException {
    final CountDownLatch latch = new CountDownLatch(3);

    mockContext.checking(new Expectations() {
      {
        oneOf(mockAuditor).init(with(any(Properties.class)));
        exactly(2).of(mockAuditor).execute();
        allowing(mockClock).currentTimeMillis();
        will(new CustomAction("returnTime") {
          @Override
          public Object invoke(Invocation invocation) throws Throwable {
            latch.countDown();
            return 990L;
          }
        });
      }
    });

    Properties props = AutoBalancerJUnitTest.getBasicConfig();

    assertEquals(3, latch.getCount());
    AutoBalancer autoR = new AutoBalancer(null, mockAuditor, mockClock, null);
    autoR.init(props);
    assertTrue(latch.await(1, TimeUnit.SECONDS));
  }

  @Test
  public void destroyAutoBalancer() throws InterruptedException {
    final CountDownLatch latch = new CountDownLatch(2);
    final CountDownLatch timerLatch = new CountDownLatch(1);
    final int timer = 20; // simulate 20 milliseconds

    mockContext.checking(new Expectations() {
      {
        oneOf(mockAuditor).init(with(any(Properties.class)));
        allowing(mockAuditor).execute();
        allowing(mockClock).currentTimeMillis();
        will(new CustomAction("returnTime") {
          @Override
          public Object invoke(Invocation invocation) throws Throwable {
            latch.countDown();
            if (latch.getCount() == 0) {
              assertTrue(timerLatch.await(1, TimeUnit.SECONDS));
              // scheduler is destroyed before wait is over
              fail();
            }
            return 1000L - timer;
          }
        });
      }
    });

    Properties props = AutoBalancerJUnitTest.getBasicConfig();

    assertEquals(2, latch.getCount());
    AutoBalancer autoR = new AutoBalancer(null, mockAuditor, mockClock, null);
    autoR.init(props);
    assertTrue(latch.await(1, TimeUnit.SECONDS));

    // after destroy no more execute will be called.
    autoR.destroy();
    timerLatch.countDown();
    TimeUnit.MILLISECONDS.sleep(2 * timer);
  }

  static Properties getBasicConfig() {
    Properties props = new Properties();
    // every second schedule
    props.put(AutoBalancer.SCHEDULE, "* 0/30 * * * ?");
    return props;
  }
}
