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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import org.apache.hadoop.hbase.master.RegionState;
import org.apache.hadoop.hbase.testclassification.MediumTests;
import org.apache.hadoop.hbase.testclassification.MiscTests;
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
import org.apache.hadoop.hbase.util.Threads;
import org.apache.hadoop.hbase.zookeeper.MetaTableLocator;
import org.apache.hadoop.hbase.zookeeper.ZKWatcher;
import org.apache.zookeeper.KeeperException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.hbase.thirdparty.com.google.protobuf.RpcController;
import org.apache.hbase.thirdparty.com.google.protobuf.ServiceException;

import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos.GetRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos.GetResponse;

/**
 * Test {@link org.apache.hadoop.hbase.zookeeper.MetaTableLocator}
 */
@Tag(MiscTests.TAG)
@Tag(MediumTests.TAG)
public class TestMetaTableLocator {

  private static final Logger LOG = LoggerFactory.getLogger(TestMetaTableLocator.class);
  private static final HBaseTestingUtility UTIL = new HBaseTestingUtility();
  private static final ServerName SN =
    ServerName.valueOf("example.org", 1234, EnvironmentEdgeManager.currentTime());
  private ZKWatcher watcher;
  private Abortable abortable;

  @BeforeAll
  public static void beforeClass() throws Exception {
    // Set this down so tests run quicker
    UTIL.getConfiguration().setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 3);
    UTIL.startMiniZKCluster();
  }

  @AfterAll
  public static void afterClass() throws IOException {
    UTIL.getZkCluster().shutdown();
  }

  @BeforeEach
  public void before() throws IOException {
    this.abortable = new Abortable() {
      @Override
      public void abort(String why, Throwable e) {
        LOG.info(why, e);
      }

      @Override
      public boolean isAborted() {
        return false;
      }
    };
    this.watcher =
      new ZKWatcher(UTIL.getConfiguration(), this.getClass().getSimpleName(), this.abortable, true);
  }

  @AfterEach
  public void after() {
    try {
      // Clean out meta location or later tests will be confused... they presume
      // start fresh in zk.
      MetaTableLocator.deleteMetaLocation(this.watcher);
    } catch (KeeperException e) {
      LOG.warn("Unable to delete hbase:meta location", e);
    }

    this.watcher.close();
  }

  /**
   * Test normal operations
   */
  @Test
  public void testMetaLookup()
    throws IOException, InterruptedException, ServiceException, KeeperException {
    final ClientProtos.ClientService.BlockingInterface client =
      Mockito.mock(ClientProtos.ClientService.BlockingInterface.class);

    Mockito.when(client.get((RpcController) Mockito.any(), (GetRequest) Mockito.any()))
      .thenReturn(GetResponse.newBuilder().build());

    assertNull(MetaTableLocator.getMetaRegionLocation(this.watcher));
    for (RegionState.State state : RegionState.State.values()) {
      if (state.equals(RegionState.State.OPEN)) {
        continue;
      }
      MetaTableLocator.setMetaLocation(this.watcher, SN, state);
      assertNull(MetaTableLocator.getMetaRegionLocation(this.watcher));
      assertEquals(state, MetaTableLocator.getMetaRegionState(this.watcher).getState());
    }
    MetaTableLocator.setMetaLocation(this.watcher, SN, RegionState.State.OPEN);
    assertEquals(SN, MetaTableLocator.getMetaRegionLocation(this.watcher));
    assertEquals(RegionState.State.OPEN,
      MetaTableLocator.getMetaRegionState(this.watcher).getState());

    MetaTableLocator.deleteMetaLocation(this.watcher);
    assertNull(MetaTableLocator.getMetaRegionState(this.watcher).getServerName());
    assertEquals(RegionState.State.OFFLINE,
      MetaTableLocator.getMetaRegionState(this.watcher).getState());
    assertNull(MetaTableLocator.getMetaRegionLocation(this.watcher));
  }

  @Test
  public void testTimeoutWaitForMeta() throws IOException, InterruptedException {
    assertThrows(NotAllMetaRegionsOnlineException.class, () -> {
      MetaTableLocator.waitMetaRegionLocation(watcher, 100);
    });
  }

  /**
   * Test waiting on meat w/ no timeout specified.
   */
  @Test
  public void testNoTimeoutWaitForMeta() throws IOException, InterruptedException, KeeperException {
    ServerName hsa = MetaTableLocator.getMetaRegionLocation(watcher);
    assertNull(hsa);

    // Now test waiting on meta location getting set.
    Thread t = new WaitOnMetaThread();
    startWaitAliveThenWaitItLives(t, 1);
    // Set a meta location.
    MetaTableLocator.setMetaLocation(this.watcher, SN, RegionState.State.OPEN);
    hsa = SN;
    // Join the thread... should exit shortly.
    t.join();
    // Now meta is available.
    assertTrue(MetaTableLocator.getMetaRegionLocation(watcher).equals(hsa));
  }

  private void startWaitAliveThenWaitItLives(final Thread t, final int ms) {
    t.start();
    UTIL.waitFor(2000, t::isAlive);
    // Wait one second.
    Threads.sleep(ms);
    assertTrue(t.isAlive(), "Assert " + t.getName() + " still waiting");
  }

  /**
   * Wait on META.
   */
  class WaitOnMetaThread extends Thread {

    WaitOnMetaThread() {
      super("WaitOnMeta");
    }

    @Override
    public void run() {
      try {
        doWaiting();
      } catch (InterruptedException e) {
        throw new RuntimeException("Failed wait", e);
      }
      LOG.info("Exiting " + getName());
    }

    void doWaiting() throws InterruptedException {
      try {
        for (;;) {
          if (MetaTableLocator.waitMetaRegionLocation(watcher, 10000) != null) {
            break;
          }
        }
      } catch (NotAllMetaRegionsOnlineException e) {
        // Ignore
      }
    }
  }
}
