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

import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICAPROP;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.DELETEREPLICA;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.MODIFYCOLLECTION;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.SPLITSHARD;

import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.cloud.OverseerMessageHandler.Lock;
import org.apache.solr.common.params.CollectionParams.CollectionAction;
import org.apache.solr.common.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestLockTree extends SolrTestCaseJ4 {
  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  public void testLocks() throws Exception {
    LockTree lockTree = new LockTree();
    Lock coll1Lock = lockTree.getSession().lock(CollectionAction.CREATE, Arrays.asList("coll1"));
    assertNotNull(coll1Lock);
    assertNull(
        "Should not be able to lock coll1/shard1",
        lockTree
            .getSession()
            .lock(CollectionAction.BALANCESHARDUNIQUE, Arrays.asList("coll1", "shard1")));

    assertNull(
        lockTree.getSession().lock(ADDREPLICAPROP, Arrays.asList("coll1", "shard1", "core_node2")));
    coll1Lock.unlock();
    Lock shard1Lock =
        lockTree
            .getSession()
            .lock(CollectionAction.BALANCESHARDUNIQUE, Arrays.asList("coll1", "shard1"));
    assertNotNull(shard1Lock);
    shard1Lock.unlock();
    Lock replica1Lock =
        lockTree.getSession().lock(ADDREPLICAPROP, Arrays.asList("coll1", "shard1", "core_node2"));
    assertNotNull(replica1Lock);

    List<Pair<CollectionAction, List<String>>> operations = new ArrayList<>();
    operations.add(new Pair<>(ADDREPLICAPROP, Arrays.asList("coll1", "shard1", "core_node2")));
    operations.add(new Pair<>(MODIFYCOLLECTION, Arrays.asList("coll1")));
    operations.add(new Pair<>(SPLITSHARD, Arrays.asList("coll1", "shard1")));
    operations.add(new Pair<>(SPLITSHARD, Arrays.asList("coll2", "shard2")));
    operations.add(new Pair<>(MODIFYCOLLECTION, Arrays.asList("coll2")));
    operations.add(new Pair<>(DELETEREPLICA, Arrays.asList("coll2", "shard1")));

    List<Set<String>> orderOfExecution =
        Arrays.asList(
            Set.of("coll1/shard1/core_node2", "coll2/shard2"),
            Set.of("coll1", "coll2"),
            Set.of("coll1/shard1", "coll2/shard1"));
    lockTree = new LockTree();
    for (int counter = 0; counter < orderOfExecution.size(); counter++) {
      LockTree.Session session = lockTree.getSession();
      List<Pair<CollectionAction, List<String>>> completedOps = new CopyOnWriteArrayList<>();
      List<Lock> locks = new CopyOnWriteArrayList<>();
      List<Thread> threads = new ArrayList<>();
      for (Pair<CollectionAction, List<String>> operation : operations) {
        final Lock lock = session.lock(operation.first(), operation.second());
        if (lock != null) {
          Thread thread = new Thread(getRunnable(completedOps, operation, locks, lock));
          threads.add(thread);
          thread.start();
        }
      }

      for (Thread thread : threads) thread.join();
      if (locks.isEmpty())
        throw new RuntimeException("Could not attain lock for anything " + operations);

      Set<String> expectedOps = orderOfExecution.get(counter);
      log.info("counter : {} , expected : {}, actual : {}", counter, expectedOps, locks);
      assertEquals(expectedOps.size(), locks.size());
      for (Lock lock : locks)
        assertTrue(
            "locks : " + locks + " expectedOps : " + expectedOps,
            expectedOps.contains(lock.toString()));
      locks.clear();
      for (Pair<CollectionAction, List<String>> completedOp : completedOps) {
        operations.remove(completedOp);
      }
    }
  }

  private Runnable getRunnable(
      List<Pair<CollectionAction, List<String>>> completedOps,
      Pair<CollectionAction, List<String>> operation,
      List<Lock> locks,
      Lock lock) {
    return () -> {
      try {
        Thread.sleep(1);
      } catch (Exception e) {
        throw new RuntimeException(e);
      } finally {
        lock.unlock();
        completedOps.add(operation);
        locks.add(lock);
      }
    };
  }
}
