/*
 * 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;

import static org.assertj.core.api.Assertions.assertThat;
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 static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.EntryNotFoundException;
import org.apache.geode.cache.Operation;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.internal.cache.versions.VersionHolder;
import org.apache.geode.test.junit.categories.UnitTest;
import org.junit.Test;
import org.junit.experimental.categories.Category;

@Category(UnitTest.class)
public class AbstractRegionMapTest {

  @Test
  public void shouldBeMockable() throws Exception {
    AbstractRegionMap mockAbstractRegionMap = mock(AbstractRegionMap.class);
    RegionEntry mockRegionEntry = mock(RegionEntry.class);
    VersionHolder mockVersionHolder = mock(VersionHolder.class);

    when(mockAbstractRegionMap.removeTombstone(eq(mockRegionEntry), eq(mockVersionHolder),
        anyBoolean(), anyBoolean())).thenReturn(true);

    assertThat(
        mockAbstractRegionMap.removeTombstone(mockRegionEntry, mockVersionHolder, true, true))
            .isTrue();
  }

  @Test
  public void invalidateOfNonExistentRegionThrowsEntryNotFound() {
    TestableAbstractRegionMap arm = new TestableAbstractRegionMap();
    EntryEventImpl event = createEventForInvalidate(arm.owner);
    when(arm.owner.isInitialized()).thenReturn(true);

    try {
      arm.invalidate(event, true, false, false);
      fail("expected EntryNotFoundException");
    } catch (EntryNotFoundException expected) {
    }
    verify(arm.owner, never()).basicInvalidatePart2(any(), any(), anyBoolean(), anyBoolean());
    verify(arm.owner, never()).invokeInvalidateCallbacks(any(), any(), anyBoolean());
  }

  @Test
  public void invalidateOfNonExistentRegionThrowsEntryNotFoundWithForce() {
    AbstractRegionMap.FORCE_INVALIDATE_EVENT = true;
    try {
      TestableAbstractRegionMap arm = new TestableAbstractRegionMap();
      EntryEventImpl event = createEventForInvalidate(arm.owner);
      when(arm.owner.isInitialized()).thenReturn(true);

      try {
        arm.invalidate(event, true, false, false);
        fail("expected EntryNotFoundException");
      } catch (EntryNotFoundException expected) {
      }
      verify(arm.owner, never()).basicInvalidatePart2(any(), any(), anyBoolean(), anyBoolean());
      verify(arm.owner, times(1)).invokeInvalidateCallbacks(any(), any(), anyBoolean());
    } finally {
      AbstractRegionMap.FORCE_INVALIDATE_EVENT = false;
    }
  }

  @Test
  public void invalidateOfAlreadyInvalidEntryReturnsFalse() {
    TestableAbstractRegionMap arm = new TestableAbstractRegionMap();
    EntryEventImpl event = createEventForInvalidate(arm.owner);

    // invalidate on region that is not initialized should create
    // entry in map as invalid.
    try {
      arm.invalidate(event, true, false, false);
      fail("expected EntryNotFoundException");
    } catch (EntryNotFoundException expected) {
    }

    when(arm.owner.isInitialized()).thenReturn(true);
    assertFalse(arm.invalidate(event, true, false, false));
    verify(arm.owner, never()).basicInvalidatePart2(any(), any(), anyBoolean(), anyBoolean());
    verify(arm.owner, never()).invokeInvalidateCallbacks(any(), any(), anyBoolean());
  }

  @Test
  public void invalidateOfAlreadyInvalidEntryReturnsFalseWithForce() {
    AbstractRegionMap.FORCE_INVALIDATE_EVENT = true;
    try {
      TestableAbstractRegionMap arm = new TestableAbstractRegionMap();
      EntryEventImpl event = createEventForInvalidate(arm.owner);

      // invalidate on region that is not initialized should create
      // entry in map as invalid.
      try {
        arm.invalidate(event, true, false, false);
        fail("expected EntryNotFoundException");
      } catch (EntryNotFoundException expected) {
      }

      when(arm.owner.isInitialized()).thenReturn(true);
      assertFalse(arm.invalidate(event, true, false, false));
      verify(arm.owner, never()).basicInvalidatePart2(any(), any(), anyBoolean(), anyBoolean());
      verify(arm.owner, times(1)).invokeInvalidateCallbacks(any(), any(), anyBoolean());
    } finally {
      AbstractRegionMap.FORCE_INVALIDATE_EVENT = false;
    }
  }

  private EntryEventImpl createEventForInvalidate(LocalRegion lr) {
    Object key = "key";
    when(lr.getKeyInfo(key)).thenReturn(new KeyInfo(key, null, null));
    return EntryEventImpl.create(lr, Operation.INVALIDATE, key, false, null, true, false);
  }

  @Test
  public void invalidateForceNewEntryOfAlreadyInvalidEntryReturnsFalse() {
    TestableAbstractRegionMap arm = new TestableAbstractRegionMap();
    EntryEventImpl event = createEventForInvalidate(arm.owner);

    // invalidate on region that is not initialized should create
    // entry in map as invalid.
    assertTrue(arm.invalidate(event, true, true, false));
    verify(arm.owner, times(1)).basicInvalidatePart2(any(), any(), anyBoolean(), anyBoolean());

    when(arm.owner.isInitialized()).thenReturn(true);
    assertFalse(arm.invalidate(event, true, true, false));
    verify(arm.owner, times(1)).basicInvalidatePart2(any(), any(), anyBoolean(), anyBoolean());
    verify(arm.owner, never()).invokeInvalidateCallbacks(any(), any(), anyBoolean());
  }

  @Test
  public void invalidateForceNewEntryOfAlreadyInvalidEntryReturnsFalseWithForce() {
    AbstractRegionMap.FORCE_INVALIDATE_EVENT = true;
    try {
      TestableAbstractRegionMap arm = new TestableAbstractRegionMap();
      EntryEventImpl event = createEventForInvalidate(arm.owner);

      // invalidate on region that is not initialized should create
      // entry in map as invalid.
      assertTrue(arm.invalidate(event, true, true, false));
      verify(arm.owner, times(1)).basicInvalidatePart2(any(), any(), anyBoolean(), anyBoolean());
      verify(arm.owner, never()).invokeInvalidateCallbacks(any(), any(), anyBoolean());

      when(arm.owner.isInitialized()).thenReturn(true);
      assertFalse(arm.invalidate(event, true, true, false));
      verify(arm.owner, times(1)).basicInvalidatePart2(any(), any(), anyBoolean(), anyBoolean());
      verify(arm.owner, times(1)).invokeInvalidateCallbacks(any(), any(), anyBoolean());
    } finally {
      AbstractRegionMap.FORCE_INVALIDATE_EVENT = false;
    }
  }

  private static class TestableAbstractRegionMap extends AbstractRegionMap {

    public LocalRegion owner;

    protected TestableAbstractRegionMap() {
      super(null);
      this.owner = mock(LocalRegion.class);
      when(this.owner.getDataPolicy()).thenReturn(DataPolicy.REPLICATE);
      doThrow(EntryNotFoundException.class).when(this.owner).checkEntryNotFound(any());
      initialize(owner, new Attributes(), null, false);
    }
  }

  private static class TxTestableAbstractRegionMap extends AbstractRegionMap {

    public LocalRegion owner;

    protected TxTestableAbstractRegionMap() {
      super(null);
      this.owner = mock(LocalRegion.class);
      when(this.owner.getCache()).thenReturn(mock(InternalCache.class));
      when(this.owner.isAllEvents()).thenReturn(true);
      when(this.owner.isInitialized()).thenReturn(true);
      initialize(owner, new Attributes(), null, false);
    }
  }

  @Test
  public void txApplyInvalidateDoesNotInvalidateRemovedToken() throws RegionClearedException {
    TxTestableAbstractRegionMap arm = new TxTestableAbstractRegionMap();

    Object key = "key";
    Object newValue = "value";
    arm.txApplyPut(Operation.CREATE, key, newValue, false,
        new TXId(mock(InternalDistributedMember.class), 1), mock(TXRmtEvent.class),
        mock(EventID.class), null, null, null, null, null, null, 1);
    RegionEntry re = arm.getEntry(key);
    assertNotNull(re);

    Token[] removedTokens =
        {Token.REMOVED_PHASE2, Token.REMOVED_PHASE1, Token.DESTROYED, Token.TOMBSTONE};

    for (Token token : removedTokens) {
      verifyTxApplyInvalidate(arm, key, re, token);
    }

  }

  private void verifyTxApplyInvalidate(TxTestableAbstractRegionMap arm, Object key, RegionEntry re,
      Token token) throws RegionClearedException {
    re.setValue(arm.owner, token);
    arm.txApplyInvalidate(key, Token.INVALID, false,
        new TXId(mock(InternalDistributedMember.class), 1), mock(TXRmtEvent.class), false,
        mock(EventID.class), null, null, null, null, null, null, 1);
    assertEquals(re.getValueAsToken(), token);
  }

}
