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

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.concurrent.Future;
import java.util.regex.Pattern;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.CacheEvictionStats;
import org.apache.hadoop.hbase.ClusterMetrics;
import org.apache.hadoop.hbase.ClusterMetrics.Option;
import org.apache.hadoop.hbase.NamespaceDescriptor;
import org.apache.hadoop.hbase.NamespaceNotFoundException;
import org.apache.hadoop.hbase.RegionMetrics;
import org.apache.hadoop.hbase.ServerName;
import org.apache.hadoop.hbase.TableExistsException;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.TableNotFoundException;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
import org.apache.hadoop.hbase.client.CompactType;
import org.apache.hadoop.hbase.client.CompactionState;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.OnlineLogRecord;
import org.apache.hadoop.hbase.client.RegionInfo;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.LogQueryFilter;
import org.apache.hadoop.hbase.client.SnapshotDescription;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.client.TableDescriptor;
import org.apache.hadoop.hbase.client.replication.TableCFs;
import org.apache.hadoop.hbase.client.security.SecurityCapability;
import org.apache.hadoop.hbase.exceptions.DeserializationException;
import org.apache.hadoop.hbase.ipc.CoprocessorRpcChannel;
import org.apache.hadoop.hbase.net.Address;
import org.apache.hadoop.hbase.quotas.QuotaFilter;
import org.apache.hadoop.hbase.quotas.QuotaSettings;
import org.apache.hadoop.hbase.quotas.SpaceQuotaSnapshotView;
import org.apache.hadoop.hbase.regionserver.wal.FailedLogCloseException;
import org.apache.hadoop.hbase.replication.ReplicationPeerConfig;
import org.apache.hadoop.hbase.replication.ReplicationPeerDescription;
import org.apache.hadoop.hbase.replication.SyncReplicationState;
import org.apache.hadoop.hbase.security.access.GetUserPermissionsRequest;
import org.apache.hadoop.hbase.security.access.Permission;
import org.apache.hadoop.hbase.security.access.UserPermission;
import org.apache.hadoop.hbase.snapshot.HBaseSnapshotException;
import org.apache.hadoop.hbase.snapshot.RestoreSnapshotException;
import org.apache.hadoop.hbase.snapshot.SnapshotCreationException;
import org.apache.hadoop.hbase.snapshot.UnknownSnapshotException;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.hadoop.hbase.zookeeper.ZKUtil;
import org.apache.hadoop.hbase.zookeeper.ZKWatcher;
import org.apache.hadoop.hbase.zookeeper.ZNodePaths;
import org.apache.yetus.audience.InterfaceAudience;
import org.apache.zookeeper.KeeperException;

import org.apache.hbase.thirdparty.com.google.common.collect.Maps;
import org.apache.hbase.thirdparty.com.google.common.collect.Sets;

import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
import org.apache.hadoop.hbase.shaded.protobuf.generated.RSGroupProtos;

@InterfaceAudience.Private
public class VerifyingRSGroupAdmin implements Admin, Closeable {

  private final Connection conn;

  private final Admin admin;

  private final ZKWatcher zkw;

  public VerifyingRSGroupAdmin(Configuration conf) throws IOException {
    conn = ConnectionFactory.createConnection(conf);
    admin = conn.getAdmin();
    zkw = new ZKWatcher(conf, this.getClass().getSimpleName(), null);
  }

  public int getOperationTimeout() {
    return admin.getOperationTimeout();
  }

  public int getSyncWaitTimeout() {
    return admin.getSyncWaitTimeout();
  }

  public void abort(String why, Throwable e) {
    admin.abort(why, e);
  }

  public boolean isAborted() {
    return admin.isAborted();
  }

  public Connection getConnection() {
    return admin.getConnection();
  }

  public boolean tableExists(TableName tableName) throws IOException {
    return admin.tableExists(tableName);
  }

  public List<TableDescriptor> listTableDescriptors() throws IOException {
    return admin.listTableDescriptors();
  }

  public List<TableDescriptor> listTableDescriptors(boolean includeSysTables) throws IOException {
    return admin.listTableDescriptors(includeSysTables);
  }

  public List<TableDescriptor> listTableDescriptors(Pattern pattern, boolean includeSysTables)
    throws IOException {
    return admin.listTableDescriptors(pattern, includeSysTables);
  }

  public TableName[] listTableNames() throws IOException {
    return admin.listTableNames();
  }

  public TableName[] listTableNames(Pattern pattern, boolean includeSysTables) throws IOException {
    return admin.listTableNames(pattern, includeSysTables);
  }

  public TableDescriptor getDescriptor(TableName tableName)
    throws TableNotFoundException, IOException {
    return admin.getDescriptor(tableName);
  }

  public void createTable(TableDescriptor desc, byte[] startKey, byte[] endKey, int numRegions)
    throws IOException {
    admin.createTable(desc, startKey, endKey, numRegions);
  }

  public Future<Void> createTableAsync(TableDescriptor desc) throws IOException {
    return admin.createTableAsync(desc);
  }

  public Future<Void> createTableAsync(TableDescriptor desc, byte[][] splitKeys)
    throws IOException {
    return admin.createTableAsync(desc, splitKeys);
  }

  public Future<Void> deleteTableAsync(TableName tableName) throws IOException {
    return admin.deleteTableAsync(tableName);
  }

  public Future<Void> truncateTableAsync(TableName tableName, boolean preserveSplits)
    throws IOException {
    return admin.truncateTableAsync(tableName, preserveSplits);
  }

  public Future<Void> enableTableAsync(TableName tableName) throws IOException {
    return admin.enableTableAsync(tableName);
  }

  public Future<Void> disableTableAsync(TableName tableName) throws IOException {
    return admin.disableTableAsync(tableName);
  }

  public boolean isTableEnabled(TableName tableName) throws IOException {
    return admin.isTableEnabled(tableName);
  }

  public boolean isTableDisabled(TableName tableName) throws IOException {
    return admin.isTableDisabled(tableName);
  }

  public boolean isTableAvailable(TableName tableName) throws IOException {
    return admin.isTableAvailable(tableName);
  }

  public Future<Void> addColumnFamilyAsync(TableName tableName, ColumnFamilyDescriptor columnFamily)
    throws IOException {
    return admin.addColumnFamilyAsync(tableName, columnFamily);
  }

  public Future<Void> deleteColumnFamilyAsync(TableName tableName, byte[] columnFamily)
    throws IOException {
    return admin.deleteColumnFamilyAsync(tableName, columnFamily);
  }

  public Future<Void> modifyColumnFamilyAsync(TableName tableName,
    ColumnFamilyDescriptor columnFamily) throws IOException {
    return admin.modifyColumnFamilyAsync(tableName, columnFamily);
  }

  public List<RegionInfo> getRegions(ServerName serverName) throws IOException {
    return admin.getRegions(serverName);
  }

  public void flush(TableName tableName) throws IOException {
    admin.flush(tableName);
  }

  public void flushRegion(byte[] regionName) throws IOException {
    admin.flushRegion(regionName);
  }

  public void flushRegionServer(ServerName serverName) throws IOException {
    admin.flushRegionServer(serverName);
  }

  public void compact(TableName tableName) throws IOException {
    admin.compact(tableName);
  }

  public void compactRegion(byte[] regionName) throws IOException {
    admin.compactRegion(regionName);
  }

  public void compact(TableName tableName, byte[] columnFamily) throws IOException {
    admin.compact(tableName, columnFamily);
  }

  public void compactRegion(byte[] regionName, byte[] columnFamily) throws IOException {
    admin.compactRegion(regionName, columnFamily);
  }

  public void compact(TableName tableName, CompactType compactType)
    throws IOException, InterruptedException {
    admin.compact(tableName, compactType);
  }

  public void compact(TableName tableName, byte[] columnFamily, CompactType compactType)
    throws IOException, InterruptedException {
    admin.compact(tableName, columnFamily, compactType);
  }

  public void majorCompact(TableName tableName) throws IOException {
    admin.majorCompact(tableName);
  }

  public void majorCompactRegion(byte[] regionName) throws IOException {
    admin.majorCompactRegion(regionName);
  }

  public void majorCompact(TableName tableName, byte[] columnFamily) throws IOException {
    admin.majorCompact(tableName, columnFamily);
  }

  public void majorCompactRegion(byte[] regionName, byte[] columnFamily) throws IOException {
    admin.majorCompactRegion(regionName, columnFamily);
  }

  public void majorCompact(TableName tableName, CompactType compactType)
    throws IOException, InterruptedException {
    admin.majorCompact(tableName, compactType);
  }

  public void majorCompact(TableName tableName, byte[] columnFamily, CompactType compactType)
    throws IOException, InterruptedException {
    admin.majorCompact(tableName, columnFamily, compactType);
  }

  public Map<ServerName, Boolean> compactionSwitch(boolean switchState,
    List<String> serverNamesList) throws IOException {
    return admin.compactionSwitch(switchState, serverNamesList);
  }

  public void compactRegionServer(ServerName serverName) throws IOException {
    admin.compactRegionServer(serverName);
  }

  public void majorCompactRegionServer(ServerName serverName) throws IOException {
    admin.majorCompactRegionServer(serverName);
  }

  public void move(byte[] encodedRegionName) throws IOException {
    admin.move(encodedRegionName);
  }

  public void move(byte[] encodedRegionName, ServerName destServerName) throws IOException {
    admin.move(encodedRegionName, destServerName);
  }

  public void assign(byte[] regionName) throws IOException {
    admin.assign(regionName);
  }

  public void unassign(byte[] regionName, boolean force) throws IOException {
    admin.unassign(regionName, force);
  }

  public void offline(byte[] regionName) throws IOException {
    admin.offline(regionName);
  }

  public boolean balancerSwitch(boolean onOrOff, boolean synchronous) throws IOException {
    return admin.balancerSwitch(onOrOff, synchronous);
  }

  public boolean balance() throws IOException {
    return admin.balance();
  }

  public boolean balance(boolean force) throws IOException {
    return admin.balance(force);
  }

  public boolean isBalancerEnabled() throws IOException {
    return admin.isBalancerEnabled();
  }

  public CacheEvictionStats clearBlockCache(TableName tableName) throws IOException {
    return admin.clearBlockCache(tableName);
  }

  public boolean normalize() throws IOException {
    return admin.normalize();
  }

  public boolean isNormalizerEnabled() throws IOException {
    return admin.isNormalizerEnabled();
  }

  public boolean normalizerSwitch(boolean on) throws IOException {
    return admin.normalizerSwitch(on);
  }

  public boolean catalogJanitorSwitch(boolean onOrOff) throws IOException {
    return admin.catalogJanitorSwitch(onOrOff);
  }

  public int runCatalogJanitor() throws IOException {
    return admin.runCatalogJanitor();
  }

  public boolean isCatalogJanitorEnabled() throws IOException {
    return admin.isCatalogJanitorEnabled();
  }

  public boolean cleanerChoreSwitch(boolean onOrOff) throws IOException {
    return admin.cleanerChoreSwitch(onOrOff);
  }

  public boolean runCleanerChore() throws IOException {
    return admin.runCleanerChore();
  }

  public boolean isCleanerChoreEnabled() throws IOException {
    return admin.isCleanerChoreEnabled();
  }

  public Future<Void> mergeRegionsAsync(byte[][] nameofRegionsToMerge, boolean forcible)
    throws IOException {
    return admin.mergeRegionsAsync(nameofRegionsToMerge, forcible);
  }

  public void split(TableName tableName) throws IOException {
    admin.split(tableName);
  }

  public void split(TableName tableName, byte[] splitPoint) throws IOException {
    admin.split(tableName, splitPoint);
  }

  public Future<Void> splitRegionAsync(byte[] regionName) throws IOException {
    return admin.splitRegionAsync(regionName);
  }

  public Future<Void> splitRegionAsync(byte[] regionName, byte[] splitPoint) throws IOException {
    return admin.splitRegionAsync(regionName, splitPoint);
  }

  public Future<Void> modifyTableAsync(TableDescriptor td) throws IOException {
    return admin.modifyTableAsync(td);
  }

  public void shutdown() throws IOException {
    admin.shutdown();
  }

  public void stopMaster() throws IOException {
    admin.stopMaster();
  }

  public boolean isMasterInMaintenanceMode() throws IOException {
    return admin.isMasterInMaintenanceMode();
  }

  public void stopRegionServer(String hostnamePort) throws IOException {
    admin.stopRegionServer(hostnamePort);
  }

  public ClusterMetrics getClusterMetrics(EnumSet<Option> options) throws IOException {
    return admin.getClusterMetrics(options);
  }

  public List<RegionMetrics> getRegionMetrics(ServerName serverName) throws IOException {
    return admin.getRegionMetrics(serverName);
  }

  public List<RegionMetrics> getRegionMetrics(ServerName serverName, TableName tableName)
    throws IOException {
    return admin.getRegionMetrics(serverName, tableName);
  }

  public Configuration getConfiguration() {
    return admin.getConfiguration();
  }

  public Future<Void> createNamespaceAsync(NamespaceDescriptor descriptor) throws IOException {
    return admin.createNamespaceAsync(descriptor);
  }

  public Future<Void> modifyNamespaceAsync(NamespaceDescriptor descriptor) throws IOException {
    return admin.modifyNamespaceAsync(descriptor);
  }

  public Future<Void> deleteNamespaceAsync(String name) throws IOException {
    return admin.deleteNamespaceAsync(name);
  }

  public NamespaceDescriptor getNamespaceDescriptor(String name)
    throws NamespaceNotFoundException, IOException {
    return admin.getNamespaceDescriptor(name);
  }

  public String[] listNamespaces() throws IOException {
    return admin.listNamespaces();
  }

  public NamespaceDescriptor[] listNamespaceDescriptors() throws IOException {
    return admin.listNamespaceDescriptors();
  }

  public List<TableDescriptor> listTableDescriptorsByNamespace(byte[] name) throws IOException {
    return admin.listTableDescriptorsByNamespace(name);
  }

  public TableName[] listTableNamesByNamespace(String name) throws IOException {
    return admin.listTableNamesByNamespace(name);
  }

  public List<RegionInfo> getRegions(TableName tableName) throws IOException {
    return admin.getRegions(tableName);
  }

  public void close() {
    admin.close();
  }

  public List<TableDescriptor> listTableDescriptors(List<TableName> tableNames) throws IOException {
    return admin.listTableDescriptors(tableNames);
  }

  public Future<Boolean> abortProcedureAsync(long procId, boolean mayInterruptIfRunning)
    throws IOException {
    return admin.abortProcedureAsync(procId, mayInterruptIfRunning);
  }

  public String getProcedures() throws IOException {
    return admin.getProcedures();
  }

  public String getLocks() throws IOException {
    return admin.getLocks();
  }

  public void rollWALWriter(ServerName serverName) throws IOException, FailedLogCloseException {
    admin.rollWALWriter(serverName);
  }

  public CompactionState getCompactionState(TableName tableName) throws IOException {
    return admin.getCompactionState(tableName);
  }

  public CompactionState getCompactionState(TableName tableName, CompactType compactType)
    throws IOException {
    return admin.getCompactionState(tableName, compactType);
  }

  public CompactionState getCompactionStateForRegion(byte[] regionName) throws IOException {
    return admin.getCompactionStateForRegion(regionName);
  }

  public long getLastMajorCompactionTimestamp(TableName tableName) throws IOException {
    return admin.getLastMajorCompactionTimestamp(tableName);
  }

  public long getLastMajorCompactionTimestampForRegion(byte[] regionName) throws IOException {
    return admin.getLastMajorCompactionTimestampForRegion(regionName);
  }

  public void snapshot(SnapshotDescription snapshot)
    throws IOException, SnapshotCreationException, IllegalArgumentException {
    admin.snapshot(snapshot);
  }

  public Future<Void> snapshotAsync(SnapshotDescription snapshot)
    throws IOException, SnapshotCreationException {
    return admin.snapshotAsync(snapshot);
  }

  public boolean isSnapshotFinished(SnapshotDescription snapshot)
    throws IOException, HBaseSnapshotException, UnknownSnapshotException {
    return admin.isSnapshotFinished(snapshot);
  }

  public void restoreSnapshot(String snapshotName) throws IOException, RestoreSnapshotException {
    admin.restoreSnapshot(snapshotName);
  }

  public void restoreSnapshot(String snapshotName, boolean takeFailSafeSnapshot, boolean restoreAcl)
    throws IOException, RestoreSnapshotException {
    admin.restoreSnapshot(snapshotName, takeFailSafeSnapshot, restoreAcl);
  }

  public Future<Void> cloneSnapshotAsync(String snapshotName, TableName tableName,
    boolean restoreAcl) throws IOException, TableExistsException, RestoreSnapshotException {
    return admin.cloneSnapshotAsync(snapshotName, tableName, restoreAcl);
  }

  public void execProcedure(String signature, String instance, Map<String, String> props)
    throws IOException {
    admin.execProcedure(signature, instance, props);
  }

  public byte[] execProcedureWithReturn(String signature, String instance,
    Map<String, String> props) throws IOException {
    return admin.execProcedureWithReturn(signature, instance, props);
  }

  public boolean isProcedureFinished(String signature, String instance, Map<String, String> props)
    throws IOException {
    return admin.isProcedureFinished(signature, instance, props);
  }

  public List<SnapshotDescription> listSnapshots() throws IOException {
    return admin.listSnapshots();
  }

  public List<SnapshotDescription> listSnapshots(Pattern pattern) throws IOException {
    return admin.listSnapshots(pattern);
  }

  public List<SnapshotDescription> listTableSnapshots(Pattern tableNamePattern,
    Pattern snapshotNamePattern) throws IOException {
    return admin.listTableSnapshots(tableNamePattern, snapshotNamePattern);
  }

  public void deleteSnapshot(String snapshotName) throws IOException {
    admin.deleteSnapshot(snapshotName);
  }

  public void deleteSnapshots(Pattern pattern) throws IOException {
    admin.deleteSnapshots(pattern);
  }

  public void deleteTableSnapshots(Pattern tableNamePattern, Pattern snapshotNamePattern)
    throws IOException {
    admin.deleteTableSnapshots(tableNamePattern, snapshotNamePattern);
  }

  public void setQuota(QuotaSettings quota) throws IOException {
    admin.setQuota(quota);
  }

  public List<QuotaSettings> getQuota(QuotaFilter filter) throws IOException {
    return admin.getQuota(filter);
  }

  public CoprocessorRpcChannel coprocessorService() {
    return admin.coprocessorService();
  }

  public CoprocessorRpcChannel coprocessorService(ServerName serverName) {
    return admin.coprocessorService(serverName);
  }

  public void updateConfiguration(ServerName server) throws IOException {
    admin.updateConfiguration(server);
  }

  public void updateConfiguration() throws IOException {
    admin.updateConfiguration();
  }

  public List<SecurityCapability> getSecurityCapabilities() throws IOException {
    return admin.getSecurityCapabilities();
  }

  public boolean splitSwitch(boolean enabled, boolean synchronous) throws IOException {
    return admin.splitSwitch(enabled, synchronous);
  }

  public boolean mergeSwitch(boolean enabled, boolean synchronous) throws IOException {
    return admin.mergeSwitch(enabled, synchronous);
  }

  public boolean isSplitEnabled() throws IOException {
    return admin.isSplitEnabled();
  }

  public boolean isMergeEnabled() throws IOException {
    return admin.isMergeEnabled();
  }

  public Future<Void> addReplicationPeerAsync(String peerId, ReplicationPeerConfig peerConfig,
    boolean enabled) throws IOException {
    return admin.addReplicationPeerAsync(peerId, peerConfig, enabled);
  }

  public Future<Void> removeReplicationPeerAsync(String peerId) throws IOException {
    return admin.removeReplicationPeerAsync(peerId);
  }

  public Future<Void> enableReplicationPeerAsync(String peerId) throws IOException {
    return admin.enableReplicationPeerAsync(peerId);
  }

  public Future<Void> disableReplicationPeerAsync(String peerId) throws IOException {
    return admin.disableReplicationPeerAsync(peerId);
  }

  public ReplicationPeerConfig getReplicationPeerConfig(String peerId) throws IOException {
    return admin.getReplicationPeerConfig(peerId);
  }

  public Future<Void> updateReplicationPeerConfigAsync(String peerId,
    ReplicationPeerConfig peerConfig) throws IOException {
    return admin.updateReplicationPeerConfigAsync(peerId, peerConfig);
  }

  public List<ReplicationPeerDescription> listReplicationPeers() throws IOException {
    return admin.listReplicationPeers();
  }

  public List<ReplicationPeerDescription> listReplicationPeers(Pattern pattern) throws IOException {
    return admin.listReplicationPeers(pattern);
  }

  public Future<Void> transitReplicationPeerSyncReplicationStateAsync(String peerId,
    SyncReplicationState state) throws IOException {
    return admin.transitReplicationPeerSyncReplicationStateAsync(peerId, state);
  }

  public void decommissionRegionServers(List<ServerName> servers, boolean offload)
    throws IOException {
    admin.decommissionRegionServers(servers, offload);
  }

  public List<ServerName> listDecommissionedRegionServers() throws IOException {
    return admin.listDecommissionedRegionServers();
  }

  public void recommissionRegionServer(ServerName server, List<byte[]> encodedRegionNames)
    throws IOException {
    admin.recommissionRegionServer(server, encodedRegionNames);
  }

  public List<TableCFs> listReplicatedTableCFs() throws IOException {
    return admin.listReplicatedTableCFs();
  }

  public void enableTableReplication(TableName tableName) throws IOException {
    admin.enableTableReplication(tableName);
  }

  public void disableTableReplication(TableName tableName) throws IOException {
    admin.disableTableReplication(tableName);
  }

  public void clearCompactionQueues(ServerName serverName, Set<String> queues)
    throws IOException, InterruptedException {
    admin.clearCompactionQueues(serverName, queues);
  }

  public List<ServerName> clearDeadServers(List<ServerName> servers) throws IOException {
    return admin.clearDeadServers(servers);
  }

  public void cloneTableSchema(TableName tableName, TableName newTableName, boolean preserveSplits)
    throws IOException {
    admin.cloneTableSchema(tableName, newTableName, preserveSplits);
  }

  public boolean switchRpcThrottle(boolean enable) throws IOException {
    return admin.switchRpcThrottle(enable);
  }

  public boolean isRpcThrottleEnabled() throws IOException {
    return admin.isRpcThrottleEnabled();
  }

  public boolean exceedThrottleQuotaSwitch(boolean enable) throws IOException {
    return admin.exceedThrottleQuotaSwitch(enable);
  }

  public Map<TableName, Long> getSpaceQuotaTableSizes() throws IOException {
    return admin.getSpaceQuotaTableSizes();
  }

  public Map<TableName, ? extends SpaceQuotaSnapshotView>
    getRegionServerSpaceQuotaSnapshots(ServerName serverName) throws IOException {
    return admin.getRegionServerSpaceQuotaSnapshots(serverName);
  }

  public SpaceQuotaSnapshotView getCurrentSpaceQuotaSnapshot(String namespace) throws IOException {
    return admin.getCurrentSpaceQuotaSnapshot(namespace);
  }

  public SpaceQuotaSnapshotView getCurrentSpaceQuotaSnapshot(TableName tableName)
    throws IOException {
    return admin.getCurrentSpaceQuotaSnapshot(tableName);
  }

  public void grant(UserPermission userPermission, boolean mergeExistingPermissions)
    throws IOException {
    admin.grant(userPermission, mergeExistingPermissions);
  }

  public void revoke(UserPermission userPermission) throws IOException {
    admin.revoke(userPermission);
  }

  public List<UserPermission>
    getUserPermissions(GetUserPermissionsRequest getUserPermissionsRequest) throws IOException {
    return admin.getUserPermissions(getUserPermissionsRequest);
  }

  public List<Boolean> hasUserPermissions(String userName, List<Permission> permissions)
    throws IOException {
    return admin.hasUserPermissions(userName, permissions);
  }

  public boolean snapshotCleanupSwitch(boolean on, boolean synchronous) throws IOException {
    return admin.snapshotCleanupSwitch(on, synchronous);
  }

  public boolean isSnapshotCleanupEnabled() throws IOException {
    return admin.isSnapshotCleanupEnabled();
  }

  public void addRSGroup(String groupName) throws IOException {
    admin.addRSGroup(groupName);
    verify();
  }

  public RSGroupInfo getRSGroup(String groupName) throws IOException {
    return admin.getRSGroup(groupName);
  }

  public RSGroupInfo getRSGroup(Address hostPort) throws IOException {
    return admin.getRSGroup(hostPort);
  }

  public RSGroupInfo getRSGroup(TableName tableName) throws IOException {
    return admin.getRSGroup(tableName);
  }

  public List<RSGroupInfo> listRSGroups() throws IOException {
    return admin.listRSGroups();
  }

  @Override
  public List<TableName> listTablesInRSGroup(String groupName) throws IOException {
    return admin.listTablesInRSGroup(groupName);
  }

  @Override
  public Pair<List<String>, List<TableName>>
    getConfiguredNamespacesAndTablesInRSGroup(String groupName) throws IOException {
    return admin.getConfiguredNamespacesAndTablesInRSGroup(groupName);
  }

  public void removeRSGroup(String groupName) throws IOException {
    admin.removeRSGroup(groupName);
    verify();
  }

  public void removeServersFromRSGroup(Set<Address> servers) throws IOException {
    admin.removeServersFromRSGroup(servers);
    verify();
  }

  public void moveServersToRSGroup(Set<Address> servers, String targetGroup) throws IOException {
    admin.moveServersToRSGroup(servers, targetGroup);
    verify();
  }

  public void setRSGroup(Set<TableName> tables, String groupName) throws IOException {
    admin.setRSGroup(tables, groupName);
    verify();
  }

  public boolean balanceRSGroup(String groupName) throws IOException {
    return admin.balanceRSGroup(groupName);
  }

  @Override
  public void renameRSGroup(String oldName, String newName) throws IOException {
    admin.renameRSGroup(oldName, newName);
    verify();
  }

  @Override
  public void updateRSGroupConfig(String groupName, Map<String, String> configuration)
      throws IOException {
    admin.updateRSGroupConfig(groupName, configuration);
    verify();
  }

  private void verify() throws IOException {
    Map<String, RSGroupInfo> groupMap = Maps.newHashMap();
    Set<RSGroupInfo> zList = Sets.newHashSet();
    List<TableDescriptor> tds = new ArrayList<>();
    try (Admin admin = conn.getAdmin()) {
      tds.addAll(admin.listTableDescriptors());
      tds.addAll(admin.listTableDescriptorsByNamespace(NamespaceDescriptor.SYSTEM_NAMESPACE_NAME));
    }
    SortedSet<Address> lives = Sets.newTreeSet();
    for (ServerName sn : conn.getAdmin().getClusterMetrics().getLiveServerMetrics().keySet()) {
      lives.add(sn.getAddress());
    }
    for (ServerName sn : conn.getAdmin().listDecommissionedRegionServers()) {
      lives.remove(sn.getAddress());
    }
    try (Table table = conn.getTable(RSGroupInfoManagerImpl.RSGROUP_TABLE_NAME);
      ResultScanner scanner = table.getScanner(new Scan())) {
      for (;;) {
        Result result = scanner.next();
        if (result == null) {
          break;
        }
        RSGroupProtos.RSGroupInfo proto = RSGroupProtos.RSGroupInfo.parseFrom(result.getValue(
          RSGroupInfoManagerImpl.META_FAMILY_BYTES, RSGroupInfoManagerImpl.META_QUALIFIER_BYTES));
        RSGroupInfo rsGroupInfo = ProtobufUtil.toGroupInfo(proto);
        groupMap.put(proto.getName(), RSGroupUtil.fillTables(rsGroupInfo, tds));
        for (Address address : rsGroupInfo.getServers()) {
          lives.remove(address);
        }
      }
    }
    SortedSet<TableName> tables = Sets.newTreeSet();
    for (TableDescriptor td : conn.getAdmin().listTableDescriptors(Pattern.compile(".*"), true)) {
      String groupName = td.getRegionServerGroup().orElse(RSGroupInfo.DEFAULT_GROUP);
      if (groupName.equals(RSGroupInfo.DEFAULT_GROUP)) {
        tables.add(td.getTableName());
      }
    }

    groupMap.put(RSGroupInfo.DEFAULT_GROUP,
      new RSGroupInfo(RSGroupInfo.DEFAULT_GROUP, lives, tables));
    assertEquals(Sets.newHashSet(groupMap.values()), Sets.newHashSet(admin.listRSGroups()));
    try {
      String groupBasePath = ZNodePaths.joinZNode(zkw.getZNodePaths().baseZNode, "rsgroup");
      for (String znode : ZKUtil.listChildrenNoWatch(zkw, groupBasePath)) {
        byte[] data = ZKUtil.getData(zkw, ZNodePaths.joinZNode(groupBasePath, znode));
        if (data.length > 0) {
          ProtobufUtil.expectPBMagicPrefix(data);
          ByteArrayInputStream bis =
            new ByteArrayInputStream(data, ProtobufUtil.lengthOfPBMagic(), data.length);
          RSGroupInfo rsGroupInfo =
            ProtobufUtil.toGroupInfo(RSGroupProtos.RSGroupInfo.parseFrom(bis));
          zList.add(RSGroupUtil.fillTables(rsGroupInfo, tds));
        }
      }
      groupMap.remove(RSGroupInfo.DEFAULT_GROUP);
      assertEquals(zList.size(), groupMap.size());
      for (RSGroupInfo rsGroupInfo : zList) {
        assertTrue(groupMap.get(rsGroupInfo.getName()).equals(rsGroupInfo));
      }
    } catch (KeeperException e) {
      throw new IOException("ZK verification failed", e);
    } catch (DeserializationException e) {
      throw new IOException("ZK verification failed", e);
    } catch (InterruptedException e) {
      throw new IOException("ZK verification failed", e);
    }
  }

  @Override
  public List<OnlineLogRecord> getSlowLogResponses(Set<ServerName> serverNames,
      LogQueryFilter logQueryFilter) throws IOException {
    return null;
  }

  @Override
  public List<Boolean> clearSlowLogResponses(Set<ServerName> serverNames) throws IOException {
    return null;
  }
}