/*
 * 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.internal.cache.wan.parallel;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import org.apache.geode.CancelCriterion;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.Region;
import org.apache.geode.internal.cache.AbstractBucketRegionQueue;
import org.apache.geode.internal.cache.BucketRegionQueue;
import org.apache.geode.internal.cache.GemFireCacheImpl;
import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.internal.cache.PartitionedRegionDataStore;
import org.apache.geode.internal.cache.execute.BucketMovedException;
import org.apache.geode.internal.cache.wan.AbstractGatewaySender;
import org.apache.geode.internal.cache.wan.GatewaySenderEventImpl;
import org.apache.geode.internal.cache.wan.GatewaySenderStats;
import org.apache.geode.internal.cache.wan.parallel.ParallelGatewaySenderQueue.MetaRegionFactory;
import org.apache.geode.internal.cache.wan.parallel.ParallelGatewaySenderQueue.ParallelGatewaySenderQueueMetaRegion;
import org.apache.geode.test.junit.categories.UnitTest;

@Category(UnitTest.class)
public class ParallelGatewaySenderQueueJUnitTest {

  private ParallelGatewaySenderQueue queue;
  private MetaRegionFactory metaRegionFactory;
  private GemFireCacheImpl cache;
  private AbstractGatewaySender sender;

  @Before
  public void createParallelGatewaySenderQueue() {
    cache = mock(GemFireCacheImpl.class);
    sender = mock(AbstractGatewaySender.class);
    CancelCriterion cancelCriterion = mock(CancelCriterion.class);
    when(sender.getCancelCriterion()).thenReturn(cancelCriterion);
    when(sender.getCache()).thenReturn(cache);
    when(sender.getMaximumQueueMemory()).thenReturn(100);
    when(sender.getLifeCycleLock()).thenReturn(new ReentrantReadWriteLock());
    metaRegionFactory = mock(MetaRegionFactory.class);
    queue = new ParallelGatewaySenderQueue(sender, Collections.emptySet(), 0, 1, metaRegionFactory);
  }

  @Test
  public void whenEventReleaseFromOffHeapFailsExceptionShouldNotBeThrownToAckReaderThread()
      throws Exception {
    GatewaySenderEventImpl event = mock(GatewaySenderEventImpl.class);
    when(event.makeHeapCopyIfOffHeap()).thenReturn(event);
    Mockito.doThrow(new IllegalStateException()).when(event).release();
    Queue backingList = new LinkedList();
    backingList.add(event);

    BucketRegionQueue bucketRegionQueue = mockBucketRegionQueue(backingList);

    TestableParallelGatewaySenderQueue queue = new TestableParallelGatewaySenderQueue(sender,
        Collections.emptySet(), 0, 1, metaRegionFactory);
    queue.setMockedAbstractBucketRegionQueue(bucketRegionQueue);

    List peeked = queue.peek(1, 1000);
    assertEquals(1, peeked.size());
    queue.remove();
  }

  @Test
  public void whenGatewayEventUnableToResolveFromOffHeapTheStatForNotQueuedConflatedShouldBeIncremented()
      throws Exception {
    GatewaySenderStats stats = mockGatewaySenderStats();

    GatewaySenderEventImpl event = mock(GatewaySenderEventImpl.class);
    when(event.makeHeapCopyIfOffHeap()).thenReturn(null);
    GatewaySenderEventImpl eventResolvesFromOffHeap = mock(GatewaySenderEventImpl.class);
    when(eventResolvesFromOffHeap.makeHeapCopyIfOffHeap()).thenReturn(eventResolvesFromOffHeap);
    Queue backingList = new LinkedList();
    backingList.add(event);
    backingList.add(eventResolvesFromOffHeap);

    BucketRegionQueue bucketRegionQueue = mockBucketRegionQueue(backingList);

    TestableParallelGatewaySenderQueue queue = new TestableParallelGatewaySenderQueue(sender,
        Collections.emptySet(), 0, 1, metaRegionFactory);
    queue.setMockedAbstractBucketRegionQueue(bucketRegionQueue);

    List peeked = queue.peek(1, 1000);
    assertEquals(1, peeked.size());
    verify(stats, times(1)).incEventsNotQueuedConflated();
  }

  private GatewaySenderStats mockGatewaySenderStats() {
    GatewaySenderStats stats = mock(GatewaySenderStats.class);
    when(sender.getStatistics()).thenReturn(stats);
    return stats;
  }

  @Test
  public void whenNullPeekedEventFromBucketRegionQueueTheStatForNotQueuedConflatedShouldBeIncremented()
      throws Exception {
    GatewaySenderStats stats = mockGatewaySenderStats();

    GatewaySenderEventImpl eventResolvesFromOffHeap = mock(GatewaySenderEventImpl.class);
    when(eventResolvesFromOffHeap.makeHeapCopyIfOffHeap()).thenReturn(eventResolvesFromOffHeap);
    Queue backingList = new LinkedList();
    backingList.add(null);
    backingList.add(eventResolvesFromOffHeap);

    BucketRegionQueue bucketRegionQueue = mockBucketRegionQueue(backingList);

    TestableParallelGatewaySenderQueue queue = new TestableParallelGatewaySenderQueue(sender,
        Collections.emptySet(), 0, 1, metaRegionFactory);
    queue.setMockedAbstractBucketRegionQueue(bucketRegionQueue);

    List peeked = queue.peek(1, 1000);
    assertEquals(1, peeked.size());
    verify(stats, times(1)).incEventsNotQueuedConflated();
  }

  @Test
  public void testLocalSize() throws Exception {
    ParallelGatewaySenderQueueMetaRegion mockMetaRegion =
        mock(ParallelGatewaySenderQueueMetaRegion.class);
    PartitionedRegionDataStore dataStore = mock(PartitionedRegionDataStore.class);
    when(mockMetaRegion.getDataStore()).thenReturn(dataStore);
    when(dataStore.getSizeOfLocalPrimaryBuckets()).thenReturn(3);
    when(metaRegionFactory.newMetataRegion(any(), any(), any(), any())).thenReturn(mockMetaRegion);
    when(cache.createVMRegion(any(), any(), any())).thenReturn(mockMetaRegion);

    queue.addShadowPartitionedRegionForUserPR(mockPR("region1"));

    assertEquals(3, queue.localSize());
  }

  private PartitionedRegion mockPR(String name) {
    PartitionedRegion region = mock(PartitionedRegion.class);
    when(region.getFullPath()).thenReturn(name);
    when(region.getPartitionAttributes()).thenReturn(new PartitionAttributesFactory<>().create());
    when(region.getTotalNumberOfBuckets()).thenReturn(113);
    when(region.getDataPolicy()).thenReturn(DataPolicy.PARTITION);
    return region;
  }

  private BucketRegionQueue mockBucketRegionQueue(final Queue backingList) {
    PartitionedRegion mockBucketRegion = mockPR("bucketRegion");
    // These next mocked return calls are for when peek is called. It ends up checking these on the
    // mocked pr region
    when(mockBucketRegion.getLocalMaxMemory()).thenReturn(100);
    when(mockBucketRegion.size()).thenReturn(backingList.size());

    BucketRegionQueue bucketRegionQueue = mock(BucketRegionQueue.class);
    when(bucketRegionQueue.getPartitionedRegion()).thenReturn(mockBucketRegion);
    when(bucketRegionQueue.peek()).thenAnswer((Answer) invocation -> backingList.poll());
    return bucketRegionQueue;
  }



  private class TestableParallelGatewaySenderQueue extends ParallelGatewaySenderQueue {

    private BucketRegionQueue mockedAbstractBucketRegionQueue;

    public TestableParallelGatewaySenderQueue(final AbstractGatewaySender sender,
        final Set<Region> userRegions, final int idx, final int nDispatcher) {
      super(sender, userRegions, idx, nDispatcher);
    }

    public TestableParallelGatewaySenderQueue(final AbstractGatewaySender sender,
        final Set<Region> userRegions, final int idx, final int nDispatcher,
        final MetaRegionFactory metaRegionFactory) {
      super(sender, userRegions, idx, nDispatcher, metaRegionFactory);
    }


    public void setMockedAbstractBucketRegionQueue(BucketRegionQueue mocked) {
      this.mockedAbstractBucketRegionQueue = mocked;
    }

    public AbstractBucketRegionQueue getBucketRegion(final PartitionedRegion prQ,
        final int bucketId) {
      return mockedAbstractBucketRegionQueue;
    }

    @Override
    public boolean areLocalBucketQueueRegionsPresent() {
      return true;
    }

    @Override
    protected PartitionedRegion getRandomShadowPR() {
      return mockedAbstractBucketRegionQueue.getPartitionedRegion();
    }

    @Override
    protected int getRandomPrimaryBucket(PartitionedRegion pr) {
      return 0;
    }

    @Override
    protected BucketRegionQueue getBucketRegionQueueByBucketId(PartitionedRegion prQ,
        int bucketId) {
      return mockedAbstractBucketRegionQueue;
    }

    // @Override
    // public int localSizeForProcessor() {
    // return 1;
    // }
  }

}
