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

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

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.hdfs.MiniDFSCluster;
import org.apache.hadoop.hdfs.tools.DFSAdmin;
import org.apache.hadoop.ipc.RefreshHandler;

import org.apache.hadoop.ipc.RefreshRegistry;
import org.apache.hadoop.ipc.RefreshResponse;
import org.junit.Test;
import org.junit.Before;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.AfterClass;
import org.mockito.Mockito;

/**
 * Before all tests, a MiniDFSCluster is spun up.
 * Before each test, mock refresh handlers are created and registered.
 * After each test, the mock handlers are unregistered.
 * After all tests, the cluster is spun down.
 */
public class TestGenericRefresh {
  private static MiniDFSCluster cluster;
  private static Configuration config;

  private static RefreshHandler firstHandler;
  private static RefreshHandler secondHandler;

  @BeforeClass
  public static void setUpBeforeClass() throws Exception {
    config = new Configuration();
    config.set("hadoop.security.authorization", "true");

    FileSystem.setDefaultUri(config, "hdfs://localhost:0");
    cluster = new MiniDFSCluster.Builder(config).build();
    cluster.waitActive();
  }

  @AfterClass
  public static void tearDownBeforeClass() throws Exception {
    if (cluster != null) {
      cluster.shutdown();
    }
  }

  @Before
  public void setUp() throws Exception {
    // Register Handlers, first one just sends an ok response
    firstHandler = Mockito.mock(RefreshHandler.class);
    Mockito.stub(firstHandler.handleRefresh(Mockito.anyString(), Mockito.any(String[].class)))
      .toReturn(RefreshResponse.successResponse());
    RefreshRegistry.defaultRegistry().register("firstHandler", firstHandler);

    // Second handler has conditional response for testing args
    secondHandler = Mockito.mock(RefreshHandler.class);
    Mockito.stub(secondHandler.handleRefresh("secondHandler", new String[]{"one", "two"}))
      .toReturn(new RefreshResponse(3, "three"));
    Mockito.stub(secondHandler.handleRefresh("secondHandler", new String[]{"one"}))
      .toReturn(new RefreshResponse(2, "two"));
    RefreshRegistry.defaultRegistry().register("secondHandler", secondHandler);
  }

  @After
  public void tearDown() throws Exception {
    RefreshRegistry.defaultRegistry().unregisterAll("firstHandler");
    RefreshRegistry.defaultRegistry().unregisterAll("secondHandler");
  }

  @Test
  public void testInvalidCommand() throws Exception {
    DFSAdmin admin = new DFSAdmin(config);
    String [] args = new String[]{"-refresh", "nn"};
    int exitCode = admin.run(args);
    assertEquals("DFSAdmin should fail due to bad args", -1, exitCode);
  }

  @Test
  public void testInvalidIdentifier() throws Exception {
    DFSAdmin admin = new DFSAdmin(config);
    String [] args = new String[]{"-refresh", "localhost:" + 
        cluster.getNameNodePort(), "unregisteredIdentity"};
    int exitCode = admin.run(args);
    assertEquals("DFSAdmin should fail due to no handler registered", -1, exitCode);
  }

  @Test
  public void testValidIdentifier() throws Exception {
    DFSAdmin admin = new DFSAdmin(config);
    String[] args = new String[]{"-refresh",
        "localhost:" + cluster.getNameNodePort(), "firstHandler"};
    int exitCode = admin.run(args);
    assertEquals("DFSAdmin should succeed", 0, exitCode);

    Mockito.verify(firstHandler).handleRefresh("firstHandler", new String[]{});
    // Second handler was never called
    Mockito.verify(secondHandler, Mockito.never())
      .handleRefresh(Mockito.anyString(), Mockito.any(String[].class));
  }

  @Test
  public void testVariableArgs() throws Exception {
    DFSAdmin admin = new DFSAdmin(config);
    String[] args = new String[]{"-refresh", "localhost:" +
        cluster.getNameNodePort(), "secondHandler", "one"};
    int exitCode = admin.run(args);
    assertEquals("DFSAdmin should return 2", 2, exitCode);

    exitCode = admin.run(new String[]{"-refresh", "localhost:" +
        cluster.getNameNodePort(), "secondHandler", "one", "two"});
    assertEquals("DFSAdmin should now return 3", 3, exitCode);

    Mockito.verify(secondHandler).handleRefresh("secondHandler", new String[]{"one"});
    Mockito.verify(secondHandler).handleRefresh("secondHandler", new String[]{"one", "two"});
  }

  @Test
  public void testUnregistration() throws Exception {
    RefreshRegistry.defaultRegistry().unregisterAll("firstHandler");

    // And now this should fail
    DFSAdmin admin = new DFSAdmin(config);
    String[] args = new String[]{"-refresh", "localhost:" +
        cluster.getNameNodePort(), "firstHandler"};
    int exitCode = admin.run(args);
    assertEquals("DFSAdmin should return -1", -1, exitCode);
  }

  @Test
  public void testUnregistrationReturnValue() {
    RefreshHandler mockHandler = Mockito.mock(RefreshHandler.class);
    RefreshRegistry.defaultRegistry().register("test", mockHandler);
    boolean ret = RefreshRegistry.defaultRegistry().unregister("test", mockHandler);
    assertTrue(ret);
  }

  @Test
  public void testMultipleRegistration() throws Exception {
    RefreshRegistry.defaultRegistry().register("sharedId", firstHandler);
    RefreshRegistry.defaultRegistry().register("sharedId", secondHandler);

    // this should trigger both
    DFSAdmin admin = new DFSAdmin(config);
    String[] args = new String[]{"-refresh", "localhost:" +
        cluster.getNameNodePort(), "sharedId", "one"};
    int exitCode = admin.run(args);
    assertEquals(-1, exitCode); // -1 because one of the responses is unregistered

    // verify we called both
    Mockito.verify(firstHandler).handleRefresh("sharedId", new String[]{"one"});
    Mockito.verify(secondHandler).handleRefresh("sharedId", new String[]{"one"});

    RefreshRegistry.defaultRegistry().unregisterAll("sharedId");
  }

  @Test
  public void testMultipleReturnCodeMerging() throws Exception {
    // Two handlers which return two non-zero values
    RefreshHandler handlerOne = Mockito.mock(RefreshHandler.class);
    Mockito.stub(handlerOne.handleRefresh(Mockito.anyString(), Mockito.any(String[].class)))
      .toReturn(new RefreshResponse(23, "Twenty Three"));

    RefreshHandler handlerTwo = Mockito.mock(RefreshHandler.class);
    Mockito.stub(handlerTwo.handleRefresh(Mockito.anyString(), Mockito.any(String[].class)))
      .toReturn(new RefreshResponse(10, "Ten"));

    // Then registered to the same ID
    RefreshRegistry.defaultRegistry().register("shared", handlerOne);
    RefreshRegistry.defaultRegistry().register("shared", handlerTwo);

    // We refresh both
    DFSAdmin admin = new DFSAdmin(config);
    String[] args = new String[]{"-refresh", "localhost:" +
        cluster.getNameNodePort(), "shared"};
    int exitCode = admin.run(args);
    assertEquals(-1, exitCode); // We get -1 because of our logic for melding non-zero return codes

    // Verify we called both
    Mockito.verify(handlerOne).handleRefresh("shared", new String[]{});
    Mockito.verify(handlerTwo).handleRefresh("shared", new String[]{});

    RefreshRegistry.defaultRegistry().unregisterAll("shared");
  }

  @Test
  public void testExceptionResultsInNormalError() throws Exception {
    // In this test, we ensure that all handlers are called even if we throw an exception in one
    RefreshHandler exceptionalHandler = Mockito.mock(RefreshHandler.class);
    Mockito.stub(exceptionalHandler.handleRefresh(Mockito.anyString(), Mockito.any(String[].class)))
      .toThrow(new RuntimeException("Exceptional Handler Throws Exception"));

    RefreshHandler otherExceptionalHandler = Mockito.mock(RefreshHandler.class);
    Mockito.stub(otherExceptionalHandler.handleRefresh(Mockito.anyString(), Mockito.any(String[].class)))
      .toThrow(new RuntimeException("More Exceptions"));

    RefreshRegistry.defaultRegistry().register("exceptional", exceptionalHandler);
    RefreshRegistry.defaultRegistry().register("exceptional", otherExceptionalHandler);

    DFSAdmin admin = new DFSAdmin(config);
    String[] args = new String[]{"-refresh", "localhost:" +
        cluster.getNameNodePort(), "exceptional"};
    int exitCode = admin.run(args);
    assertEquals(-1, exitCode); // Exceptions result in a -1

    Mockito.verify(exceptionalHandler).handleRefresh("exceptional", new String[]{});
    Mockito.verify(otherExceptionalHandler).handleRefresh("exceptional", new String[]{});

    RefreshRegistry.defaultRegistry().unregisterAll("exceptional");
  }
}