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

import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;

import org.junit.Before;
import org.junit.Test;

import org.apache.geode.internal.cache.RegionEntry;

public class ReferenceCountHelperImplTest {

  private BiConsumer<String, Boolean> debugLogger;

  @Before
  public void setUp() {
    debugLogger = uncheckedCast(mock(BiConsumer.class));
  }

  @Test
  public void doTrackReferenceCountsWithTrackRefsTrueAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueTrue();
    assertThat(referenceCountHelperImpl.trackReferenceCounts()).isTrue();
  }

  @Test
  public void doTrackReferenceCountsWithTrackRefsTrueAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueFalse();
    assertThat(referenceCountHelperImpl.trackReferenceCounts()).isTrue();
  }

  @Test
  public void doTrackReferenceCountsWithTrackRefsFalseAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseTrue();
    assertThat(referenceCountHelperImpl.trackReferenceCounts()).isFalse();
  }

  @Test
  public void doTrackReferenceCountsWithTrackRefsFalseAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseFalse();
    assertThat(referenceCountHelperImpl.trackReferenceCounts()).isFalse();
  }

  @Test
  public void doTrackFreedReferenceCountsWithTrackRefsTrueAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueTrue();
    assertThat(referenceCountHelperImpl.trackFreedReferenceCounts()).isTrue();
  }

  @Test
  public void doTrackFreedReferenceCountsWithTrackRefsTrueAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueFalse();
    assertThat(referenceCountHelperImpl.trackFreedReferenceCounts()).isFalse();
  }

  @Test
  public void doTrackFreedReferenceCountsWithTrackRefsFalseAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseTrue();
    assertThat(referenceCountHelperImpl.trackFreedReferenceCounts()).isTrue();
  }

  @Test
  public void doTrackFreedReferenceCountsWithTrackRefsFalseAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseFalse();
    assertThat(referenceCountHelperImpl.trackFreedReferenceCounts()).isFalse();
  }

  @Test
  public void doSkipRefCountTrackingWithTrackRefsTrueAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueTrue();

    Object preOwner = referenceCountHelperImpl.getReferenceCountOwner();
    referenceCountHelperImpl.skipRefCountTracking();
    Object postOwner = referenceCountHelperImpl.getReferenceCountOwner();

    // skip sets owner to SKIP_REF_COUNT_TRACKING
    assertThat(postOwner).isNotEqualTo(preOwner);
    assertThat(referenceCountHelperImpl.isRefCountTracking()).isFalse();

    Long address = (long) 0x1000;
    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    referenceCountHelperImpl.unskipRefCountTracking();
    postOwner = referenceCountHelperImpl.getReferenceCountOwner();

    assertThat(preOwner).isEqualTo(postOwner);
    assertThat(referenceCountHelperImpl.isRefCountTracking()).isTrue();
  }

  @Test
  public void doSkipRefCountTrackingWithTrackRefsFalseAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseTrue();
    Object preOwner = referenceCountHelperImpl.getReferenceCountOwner();
    // getReferenceCountOwner returns null if not tracking
    assertThat(preOwner).isNull();

    referenceCountHelperImpl.skipRefCountTracking();
    assertThat(referenceCountHelperImpl.isRefCountTracking()).isFalse();

    referenceCountHelperImpl.unskipRefCountTracking();
    assertThat(referenceCountHelperImpl.isRefCountTracking()).isFalse(); // system prop not set
  }

  @Test
  public void doSkipRefCountTrackingWithTrackRefsFalseAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseFalse();
    Object preOwner = referenceCountHelperImpl.getReferenceCountOwner();
    // getReferenceCountOwner returns null if not tracking
    assertThat(preOwner).isNull();

    referenceCountHelperImpl.skipRefCountTracking();
    assertThat(referenceCountHelperImpl.isRefCountTracking()).isFalse();

    referenceCountHelperImpl.unskipRefCountTracking();
    // system prop not set
    assertThat(referenceCountHelperImpl.isRefCountTracking()).isFalse();
  }

  @Test
  public void doSkipRefCountTrackingWithTrackRefsTrueAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueFalse();

    Object preOwner = referenceCountHelperImpl.getReferenceCountOwner();
    referenceCountHelperImpl.skipRefCountTracking();
    Object postOwner = referenceCountHelperImpl.getReferenceCountOwner();
    // skip sets owner to SKIP_REF_COUNT_TRACKING
    assertThat(postOwner).isNotEqualTo(preOwner);
    assertThat(referenceCountHelperImpl.isRefCountTracking()).isFalse();

    referenceCountHelperImpl.unskipRefCountTracking();
    postOwner = referenceCountHelperImpl.getReferenceCountOwner();
    assertThat(preOwner).isEqualTo(postOwner);
    assertThat(referenceCountHelperImpl.isRefCountTracking()).isTrue();
  }

  @Test
  public void doSetReferenceCountOwnerWithTrackRefsTrueAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueTrue();
    String owner = null;
    referenceCountHelperImpl.setReferenceCountOwner(owner);
    AtomicInteger reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isZero();

    owner = "SomeOwner";
    referenceCountHelperImpl.setReferenceCountOwner(owner);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isOne();
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isEqualTo(owner);

    String owner2 = "SomeOwner2";
    referenceCountHelperImpl.setReferenceCountOwner(owner2);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isEqualTo(2);
    // stays original owner until cnt = 0
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isNotEqualTo(owner2);

    String owner3 = null;
    referenceCountHelperImpl.setReferenceCountOwner(owner3);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isOne();
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isEqualTo(owner);

    owner = null;
    referenceCountHelperImpl.setReferenceCountOwner(owner);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isZero();
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isNull();

    RegionEntry regionEntry = mock(RegionEntry.class);
    referenceCountHelperImpl.setReferenceCountOwner(regionEntry);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isOne();
    assertThat(regionEntry).isEqualTo(referenceCountHelperImpl.getReferenceCountOwner());

    Long address = (long) 0x1000;
    boolean decRefCount = false;
    int rc = 1;

    referenceCountHelperImpl.refCountChanged(address, decRefCount, rc);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    RefCountChangeInfo refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, decRefCount, rc);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    decRefCount = true;
    referenceCountHelperImpl.refCountChanged(address, decRefCount, rc);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, decRefCount, rc);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isEmpty();
  }

  @Test
  public void doSetReferenceCountOwnerWithTrackRefsFalseAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseTrue();

    referenceCountHelperImpl.setReferenceCountOwner(null);
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isNull();

    AtomicInteger reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount).isNull();
  }

  @Test
  public void doSetReferenceCountOwnerWithTrackRefsFalseAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseFalse();

    referenceCountHelperImpl.setReferenceCountOwner(null);
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isNull();

    AtomicInteger reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount).isNull();
  }

  @Test
  public void doSetReferenceCountOwnerWithTrackRefsTrueAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueFalse();

    String owner = null;
    referenceCountHelperImpl.setReferenceCountOwner(owner);
    AtomicInteger reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isZero();

    owner = "SomeOwner";
    referenceCountHelperImpl.setReferenceCountOwner(owner);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isOne();
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isEqualTo(owner);

    String owner2 = "SomeOwner2";
    referenceCountHelperImpl.setReferenceCountOwner(owner2);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isEqualTo(2);
    // stays original owner until cnt = 0
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isEqualTo(owner);

    String owner3 = null;
    referenceCountHelperImpl.setReferenceCountOwner(owner3);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isOne();
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isEqualTo(owner);

    owner = null;
    referenceCountHelperImpl.setReferenceCountOwner(owner);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isZero();
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isNull();
  }

  @Test
  public void doCreateReferenceCountOwnerWithTrackRefsTrueAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueTrue();

    Object owner = referenceCountHelperImpl.createReferenceCountOwner();
    assertThat(owner).isNotNull();
    AtomicInteger reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isOne();

    owner = null;
    referenceCountHelperImpl.setReferenceCountOwner(owner);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isZero();
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isNull();
  }

  @Test
  public void doCreateReferenceCountOwnerWithTrackRefsFalseAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseTrue();
    Object owner = referenceCountHelperImpl.createReferenceCountOwner();
    assertThat(owner).isNull();
  }

  @Test
  public void doCreateReferenceCountOwnerWithTrackRefsFalseAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseFalse();
    Object owner = referenceCountHelperImpl.createReferenceCountOwner();
    assertThat(owner).isNull();
  }

  @Test
  public void doCreateReferenceCountOwnerWithTrackRefsTrueAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueFalse();
    Object owner = referenceCountHelperImpl.createReferenceCountOwner();
    assertThat(owner).isNotNull();
    AtomicInteger reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isOne();

    owner = null;
    referenceCountHelperImpl.setReferenceCountOwner(owner);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isZero();
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isNull();
  }

  @Test
  public void doRefCountChangedNoOwnerWithTrackRefsTrueAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueTrue();
    Long address = (long) 0x1000;

    // quick check of free of nonexistent info
    referenceCountHelperImpl.freeRefCountInfo(address);
    Object owner = referenceCountHelperImpl.getReferenceCountOwner();
    assertThat(owner).isNull();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);

    RefCountChangeInfo refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);

    refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // inc and dec are tracked in different changeinfo objects (?)
    assertThat(list).hasSize(2);

    refCountChangeInfo = list.get(1);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(2);

    refCountChangeInfo = list.get(1);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // list contains 2 entries from inc/dec done above
    assertThat(list).hasSize(2);

    List<RefCountChangeInfo> freeInfo = referenceCountHelperImpl.getFreeRefCountInfo(address);
    // no freeRefCountInfo calls yet
    assertThat(freeInfo).isNull();

    // when freed, moved to FreeRefCountInfo list
    referenceCountHelperImpl.freeRefCountInfo(address);
    List<RefCountChangeInfo> freeInfo2 = referenceCountHelperImpl.getFreeRefCountInfo(address);
    // the inc/dec info moved to freeRefCountInfo list
    assertThat(freeInfo2).hasSize(2);

    list = referenceCountHelperImpl.getRefCountInfo(address);
    // the inc/dec ref count list should now be null
    assertThat(list).isNull();
  }

  @Test
  public void doRefCountChangedNoOwnerWithTrackRefsFalseAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseTrue();
    Long address = (long) 0x1000;

    Object owner = referenceCountHelperImpl.getReferenceCountOwner();
    assertThat(owner).isNull();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    List<RefCountChangeInfo> freeInfo = referenceCountHelperImpl.getFreeRefCountInfo(address);
    // no freeRefCountInfo calls yet
    assertThat(freeInfo).isNull();

    // noop when not tracking
    referenceCountHelperImpl.freeRefCountInfo(address);
    freeInfo = referenceCountHelperImpl.getFreeRefCountInfo(address);
    // should still be null
    assertThat(freeInfo).isNull();
  }

  @Test
  public void doRefCountChangedNoOwnerWithTrackRefsFalseAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseFalse();
    Long address = (long) 0x1000;

    Object owner = referenceCountHelperImpl.getReferenceCountOwner();
    assertThat(owner).isNull();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    List<RefCountChangeInfo> freeInfo = referenceCountHelperImpl.getFreeRefCountInfo(address);
    // no freeRefCountInfo calls yet
    assertThat(freeInfo).isNull();

    // noop when not tracking
    referenceCountHelperImpl.freeRefCountInfo(address);
    // should still be null
    freeInfo = referenceCountHelperImpl.getFreeRefCountInfo(address);
    assertThat(freeInfo).isNull();
  }

  @Test
  public void doRefCountChangedNoOwnerWithTrackRefsTrueAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueFalse();
    Long address = (long) 0x1000;

    Object owner = referenceCountHelperImpl.getReferenceCountOwner();
    assertThat(owner).isNull();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    RefCountChangeInfo refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // inc and dec are tracked in different changeinfo objects (?)
    assertThat(list).hasSize(2);
    refCountChangeInfo = list.get(1);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(2);
    refCountChangeInfo = list.get(1);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // list contains 2 entries from inc/dec done above
    assertThat(list).hasSize(2);

    List<RefCountChangeInfo> freeInfo = referenceCountHelperImpl.getFreeRefCountInfo(address);
    // no freeRefCountInfo calls yet
    assertThat(freeInfo).isNull();

    // when freed, moved to FreeRefCountInfo list
    referenceCountHelperImpl.freeRefCountInfo(address);

    List<RefCountChangeInfo> freeInfo2 = referenceCountHelperImpl.getFreeRefCountInfo(address);
    // not tracking freed info
    assertThat(freeInfo2).isNull();

    list = referenceCountHelperImpl.getRefCountInfo(address);
    // the inc/dec ref count list should now be null
    assertThat(list).isNull();
  }

  @Test
  public void doRefCountChangedWithOwnerWithTrackRefsTrueAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueTrue();
    Long address = (long) 0x1000;

    Object owner = referenceCountHelperImpl.createReferenceCountOwner();
    assertThat(owner).isNotNull();

    AtomicInteger reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isOne();

    owner = null;
    referenceCountHelperImpl.setReferenceCountOwner(owner);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isZero();
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isNull();

    owner = referenceCountHelperImpl.createReferenceCountOwner();
    assertThat(owner).isNotNull();

    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isOne();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    RefCountChangeInfo refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // inc and dec are tracked in different changeinfo objects (?)
    assertThat(list).hasSize(1);
    refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.getRefCountInfo(address);
    assertThat(list).isEmpty();
  }

  @Test
  public void doRefCountChangedWithOwnerWithTrackRefsFalseAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseTrue();
    Long address = (long) 0x1000;

    Object owner = referenceCountHelperImpl.createReferenceCountOwner();
    assertThat(owner).isNull();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // inc and dec are tracked in different changeinfo objects (?)
    assertThat(list).isNull();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    List<RefCountChangeInfo> freeInfo = referenceCountHelperImpl.getFreeRefCountInfo(address);
    // no freeRefCountInfo calls yet
    assertThat(freeInfo).isNull();

    // when freed, moved to FreeRefCountInfo list
    referenceCountHelperImpl.freeRefCountInfo(address);

    freeInfo = referenceCountHelperImpl.getFreeRefCountInfo(address);
    // the inc/dec info moved to freeRefCountInfo list
    assertThat(freeInfo).isNull();
  }

  @Test
  public void doRefCountChangedWithOwnerWithTrackRefsFalseAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseFalse();
    Long address = (long) 0x1000;

    Object owner = referenceCountHelperImpl.createReferenceCountOwner();
    assertThat(owner).isNull();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // inc and dec are tracked in different changeinfo objects (?)
    assertThat(list).isNull();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    List<RefCountChangeInfo> freeInfo = referenceCountHelperImpl.getFreeRefCountInfo(address);
    // no freeRefCountInfo calls yet
    assertThat(freeInfo).isNull();

    // when freed, moved to FreeRefCountInfo list
    referenceCountHelperImpl.freeRefCountInfo(address);

    freeInfo = referenceCountHelperImpl.getFreeRefCountInfo(address);
    // the inc/dec info moved to freeRefCountInfo list
    assertThat(freeInfo).isNull();
  }

  @Test
  public void doRefCountChangedWithOwnerWithTrackRefsTrueAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueFalse();
    Long address = (long) 0x1000;

    Object owner = referenceCountHelperImpl.createReferenceCountOwner();
    assertThat(owner).isNotNull();

    AtomicInteger reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isOne();

    owner = null;
    referenceCountHelperImpl.setReferenceCountOwner(owner);
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isZero();
    assertThat(referenceCountHelperImpl.getReferenceCountOwner()).isNull();

    owner = referenceCountHelperImpl.createReferenceCountOwner();
    assertThat(owner).isNotNull();
    reenterCount = referenceCountHelperImpl.getReenterCount();
    assertThat(reenterCount.get()).isOne();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    RefCountChangeInfo refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // inc and dec are tracked in different changeinfo objects (?)
    assertThat(list).hasSize(1);
    refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isEmpty();
  }

  @Test
  public void doGetRefCountInfoWithTrackRefsTrueAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueTrue();
    Long address = (long) 0x1000;

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    RefCountChangeInfo refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    // now getRefCountInfo
    List<RefCountChangeInfo> info = referenceCountHelperImpl.getRefCountInfo(address);
    assertThat(info).hasSize(1);
    refCountChangeInfo = info.get(0);
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // getRefCountInfo leaves list LOCKED (i.e. empty)
    assertThat(list).isEmpty();
  }

  @Test
  public void doRefCountChangedAfterGetRefCountInfoWithTrackRefsTrueAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueTrue();
    Long address = (long) 0x1000;

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    RefCountChangeInfo refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    // now getRefCountInfo
    List<RefCountChangeInfo> info = referenceCountHelperImpl.getRefCountInfo(address);
    assertThat(info).hasSize(1);
    refCountChangeInfo = info.get(0);
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // getRefCountInfo leaves list LOCKED (i.e. empty)
    assertThat(list).isEmpty();

    // this line should fail. no inc after getInfo allowed
    referenceCountHelperImpl.refCountChanged(address, false, 1);
    verify(debugLogger).accept("refCount inced after orphan detected for @1000", true);

    // this line should fail. no inc after getInfo allowed
    referenceCountHelperImpl.refCountChanged(address, true, 1);
    verify(debugLogger).accept("refCount deced after orphan detected for @1000", true);

    // this line should fail. no free after getInfo allowed
    referenceCountHelperImpl.freeRefCountInfo(address);
    verify(debugLogger).accept("freed after orphan detected for @1000", true);
  }

  @Test
  public void doGetRefCountInfoWithTrackRefsFalseAndTrackFreesTrue() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseTrue();
    Long address = (long) 0x1000;

    List<RefCountChangeInfo> list = referenceCountHelperImpl.getRefCountInfo(address);
    assertThat(list).isNull();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    List<RefCountChangeInfo> info = referenceCountHelperImpl.getRefCountInfo(address);
    assertThat(info).isNull();

    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    // TODO: add assertions for the following

    // this will be ignored.
    referenceCountHelperImpl.refCountChanged(address, false, 1);
    // this will be ignored.
    referenceCountHelperImpl.refCountChanged(address, true, 1);
    // this will be ignored.
    referenceCountHelperImpl.freeRefCountInfo(address);
  }

  @Test
  public void doGetRefCountInfoWithTrackRefsFalseAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_FalseFalse();
    Long address = (long) 0x1000;

    List<RefCountChangeInfo> list = referenceCountHelperImpl.getRefCountInfo(address);
    assertThat(list).isNull();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();

    List<RefCountChangeInfo> info = referenceCountHelperImpl.getRefCountInfo(address);
    assertThat(info).isNull();

    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).isNull();
  }

  @Test
  public void doGetRefCountInfoWithTrackRefsTrueAndTrackFreesFalse() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newReferenceCountHelperImpl_TrueFalse();
    long address = 0x1000;

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    RefCountChangeInfo refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    refCountChangeInfo = list.get(0);
    // line 258 of ref cnt helper does not set useCount = 1 when adding new entry?
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    // now getRefCountInfo
    List<RefCountChangeInfo> info = referenceCountHelperImpl.getRefCountInfo(address);
    assertThat(info).hasSize(1);
    refCountChangeInfo = info.get(0);
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // getRefCountInfo leaves list LOCKED (i.e. empty)
    assertThat(list).isEmpty();
  }

  @Test
  public void doGetRefCountInfoNonRegionEntryConcurrencyTest() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newHookedReferenceCountHelperImpl();
    Long address = (long) 0x1000;

    // assume test identity
    referenceCountHelperImpl.setReferenceCountOwner("TestOwner");

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    RefCountChangeInfo refCountChangeInfo = list.get(0);
    // hooked impl simulates a concurrent update, so cnt is > expected
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    // sets owner to null and resets count
    referenceCountHelperImpl.setReferenceCountOwner(null);
    // sets owner to null and resets count // sets owner to null and resets count

    // assume new identity
    referenceCountHelperImpl.setReferenceCountOwner("TestOwner2");

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    refCountChangeInfo = list.get(0);
    // list is not null, so hook not used
    assertThat(refCountChangeInfo.getUseCount()).isEqualTo(2);

    // dec ref count
    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // dec adds new list of stack traces
    assertThat(list).hasSize(2);
    refCountChangeInfo = list.get(1);
    // cnt starts at 0 for new entries
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    // now getRefCountInfo
    List<RefCountChangeInfo> info = referenceCountHelperImpl.getRefCountInfo(address);
    // hooked impl added one to list
    assertThat(info).hasSize(3);
    refCountChangeInfo = info.get(2);
    // count starts at 0 for new entries
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // getRefCountInfo leaves list LOCKED (i.e. empty)
    assertThat(list).isEmpty();
  }

  @Test
  public void doGetRefCountInfoRegionEntryConcurrencyTest() {
    ReferenceCountHelperImpl referenceCountHelperImpl = newHookedReferenceCountHelperImpl();
    Long address = (long) 0x1000;

    RegionEntry regionEntry1 = mock(RegionEntry.class);
    // set owner to region entry type
    referenceCountHelperImpl.setReferenceCountOwner(regionEntry1);

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    List<RefCountChangeInfo> list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    RefCountChangeInfo refCountChangeInfo = list.get(0);
    // hooked impl simulates a concurrent update, so cnt is > expected
    assertThat(refCountChangeInfo.getUseCount()).isOne();

    // sets owner to null and resets count
    referenceCountHelperImpl.setReferenceCountOwner(null);
    // sets owner to null and resets count
    referenceCountHelperImpl.setReferenceCountOwner(null);

    RegionEntry regionEntry2 = mock(RegionEntry.class);
    // set owner to region entry type
    referenceCountHelperImpl.setReferenceCountOwner(regionEntry2);

    referenceCountHelperImpl.refCountChanged(address, false, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    assertThat(list).hasSize(1);
    refCountChangeInfo = list.get(0);
    // list is not null, so hook not used
    assertThat(refCountChangeInfo.getUseCount()).isEqualTo(2);

    // dec ref count
    referenceCountHelperImpl.refCountChanged(address, true, 1);
    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // dec adds new list of stack traces
    assertThat(list).hasSize(2);
    refCountChangeInfo = list.get(1);
    // cnt starts at 0 for new entries
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    // now getRefCountInfo
    List<RefCountChangeInfo> info = referenceCountHelperImpl.getRefCountInfo(address);
    // hooked impl added one to list
    assertThat(info).hasSize(3);
    refCountChangeInfo = info.get(2);
    // count starts at 0 for new entries
    assertThat(refCountChangeInfo.getUseCount()).isZero();

    list = referenceCountHelperImpl.peekRefCountInfo(address);
    // getRefCountInfo leaves list LOCKED (i.e. empty)
    assertThat(list).isEmpty();
  }

  private ReferenceCountHelperImpl newReferenceCountHelperImpl_TrueTrue() {
    return new ReferenceCountHelperImpl(true, true, debugLogger);
  }

  private ReferenceCountHelperImpl newReferenceCountHelperImpl_TrueFalse() {
    return new ReferenceCountHelperImpl(true, false, debugLogger);
  }

  private ReferenceCountHelperImpl newReferenceCountHelperImpl_FalseTrue() {
    return new ReferenceCountHelperImpl(false, true, debugLogger);
  }

  private ReferenceCountHelperImpl newReferenceCountHelperImpl_FalseFalse() {
    return new ReferenceCountHelperImpl(false, false, debugLogger);
  }

  private ReferenceCountHelperImpl newHookedReferenceCountHelperImpl() {
    return new HookedReferenceCountHelperImpl(true, true, debugLogger);
  }

  private static class HookedReferenceCountHelperImpl extends ReferenceCountHelperImpl {

    private int refCountChangedTestHookCount;

    private HookedReferenceCountHelperImpl(boolean trackRefCounts, boolean trackFreedRefCounts,
        BiConsumer<String, Boolean> debugLogger) {
      super(trackRefCounts, trackFreedRefCounts, debugLogger);
    }

    /**
     * Update list of stack traces for address. Hooked SUT should see that the list changed.
     */
    @Override
    protected void getReferenceCountInfoTestHook(
        ConcurrentMap<Long, List<RefCountChangeInfo>> stackTraces, long address) {
      List<RefCountChangeInfo> updatedList =
          new ArrayList<>(stackTraces.get(address));
      RefCountChangeInfo refCountChangeInfo = new RefCountChangeInfo(false, 0, "TestOwner");
      updatedList.add(refCountChangeInfo);
      stackTraces.put(address, updatedList);
    }

    /**
     * Reinvoke refCountChanged to update reference count. Hooked SUT should see that the count has
     * changed.
     */
    @Override
    protected void refCountChangedTestHook(Long address, boolean decRefCount, int rc) {
      if (refCountChangedTestHookCount == 0) {
        refCountChangedTestHookCount++;
        refCountChanged(address, decRefCount, rc);
      } else {
        refCountChangedTestHookCount--;
      }
    }
  }

}
