/**
   * When L1 is enabled this test should not be ran when a previous value is present as it will
   * cause timeouts. Due to how locking works with L1 this cannot occur when the previous value
   * exists.
   *
   * @param op
   * @throws Exception
   */
  protected void doTestWhereCommitOccursAfterStateTransferBeginsBeforeCompletion(
      final TestWriteOperation op) throws Exception {
    if (l1Enabled() && op.getPreviousValue() != null) {
      fail("This test cannot be ran with L1 when a previous value is set");
    }
    // Test scenario:
    // cache0,1,2 are in the cluster, an owner leaves
    // Key k is in the cache, and is transferred to the non owner
    // A user operation also modifies key k causing an invalidation
    // on the non owner which is getting the state transfer
    final AdvancedCache<Object, Object> primaryOwnerCache = cache(0, cacheName).getAdvancedCache();
    final AdvancedCache<Object, Object> backupOwnerCache = cache(1, cacheName).getAdvancedCache();
    final AdvancedCache<Object, Object> nonOwnerCache = cache(2, cacheName).getAdvancedCache();

    final MagicKey key = new MagicKey(primaryOwnerCache, backupOwnerCache);

    // Prepare for replace/remove: put a previous value in cache0
    final Object previousValue = op.getPreviousValue();
    if (previousValue != null) {
      primaryOwnerCache.put(key, previousValue);
      assertEquals(previousValue, primaryOwnerCache.get(key));
      log.tracef("Previous value inserted: %s = %s", key, previousValue);

      assertEquals(previousValue, nonOwnerCache.get(key));

      if (l1Enabled()) {
        assertIsInL1(nonOwnerCache, key);
      }
    }

    int preJoinTopologyId =
        primaryOwnerCache
            .getComponentRegistry()
            .getStateTransferManager()
            .getCacheTopology()
            .getTopologyId();

    // Block any state response commands on cache0
    CheckPoint checkPoint = new CheckPoint();
    ControlledRpcManager blockingRpcManager0 = blockStateResponseCommand(primaryOwnerCache);

    // Block the rebalance confirmation on cache0
    blockRebalanceConfirmation(primaryOwnerCache.getCacheManager(), checkPoint);

    assertEquals(
        primaryOwnerCache.getCacheManager().getCoordinator(),
        primaryOwnerCache.getCacheManager().getAddress());

    // Remove the leaver
    log.trace("Stopping the cache");
    backupOwnerCache.getCacheManager().stop();

    int rebalanceTopologyId = preJoinTopologyId + 2;

    // Wait for the write CH to contain the joiner everywhere
    eventually(
        new Condition() {
          @Override
          public boolean isSatisfied() throws Exception {
            return primaryOwnerCache.getRpcManager().getMembers().size() == 2
                && nonOwnerCache.getRpcManager().getMembers().size() == 2;
          }
        });

    assertEquals(
        primaryOwnerCache.getCacheManager().getCoordinator(),
        primaryOwnerCache.getCacheManager().getAddress());

    // Wait for cache0 to collect the state to send to cache1 (including our previous value).
    blockingRpcManager0.waitForCommandToBlock();

    // Every PutKeyValueCommand will be blocked before committing the entry on cache1
    CyclicBarrier beforeCommitCache1Barrier = new CyclicBarrier(2);
    BlockingInterceptor blockingInterceptor1 =
        new BlockingInterceptor(beforeCommitCache1Barrier, op.getCommandClass(), true);
    nonOwnerCache.addInterceptorAfter(blockingInterceptor1, EntryWrappingInterceptor.class);

    // Put/Replace/Remove from cache0 with cache0 as primary owner, cache1 will become a backup
    // owner for the retry
    // The put command will be blocked on cache1 just before committing the entry.
    Future<Object> future =
        fork(
            new Callable<Object>() {
              @Override
              public Object call() throws Exception {
                return op.perform(primaryOwnerCache, key);
              }
            });

    // Wait for the entry to be wrapped on cache1
    beforeCommitCache1Barrier.await(10, TimeUnit.SECONDS);

    // Remove the interceptor so we don't mess up any other state transfer puts
    removeAllBlockingInterceptorsFromCache(nonOwnerCache);

    // Allow the state to be applied on cache1 (writing the old value for our entry)
    blockingRpcManager0.stopBlocking();

    // Wait for second in line to finish applying the state, but don't allow the rebalance
    // confirmation to be processed.
    // (It would change the topology and it would trigger a retry for the command.)
    checkPoint.awaitStrict(
        "pre_rebalance_confirmation_"
            + rebalanceTopologyId
            + "_from_"
            + primaryOwnerCache.getCacheManager().getAddress(),
        10,
        SECONDS);

    // Now allow the command to commit on cache1
    beforeCommitCache1Barrier.await(10, TimeUnit.SECONDS);

    // Wait for the command to finish and check that it didn't fail
    Object result = future.get(10, TimeUnit.SECONDS);
    assertEquals(op.getReturnValue(), result);
    log.tracef("%s operation is done", op);

    // Allow the rebalance confirmation to proceed and wait for the topology to change everywhere
    checkPoint.trigger(
        "resume_rebalance_confirmation_"
            + rebalanceTopologyId
            + "_from_"
            + primaryOwnerCache.getCacheManager().getAddress());
    checkPoint.trigger(
        "resume_rebalance_confirmation_"
            + rebalanceTopologyId
            + "_from_"
            + nonOwnerCache.getCacheManager().getAddress());
    TestingUtil.waitForRehashToComplete(primaryOwnerCache, nonOwnerCache);

    switch (op) {
      case REMOVE:
      case REMOVE_EXACT:
        break;
      default:
        assertIsInContainerImmortal(primaryOwnerCache, key);
        assertIsInContainerImmortal(nonOwnerCache, key);
        break;
    }

    // Check the value to make sure data container contains correct value
    assertEquals(op.getValue(), primaryOwnerCache.get(key));
    assertEquals(op.getValue(), nonOwnerCache.get(key));
  }