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

import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.registry.client.api.RegistryConstants;
import org.apache.hadoop.registry.client.binding.RegistryUtils;
import org.apache.hadoop.registry.client.binding.RegistryTypeUtils;
import org.apache.hadoop.registry.client.types.AddressTypes;
import org.apache.hadoop.registry.client.types.Endpoint;
import org.apache.hadoop.registry.client.types.ProtocolTypes;
import org.apache.hadoop.registry.client.types.ServiceRecord;
import org.apache.hadoop.registry.client.types.yarn.YarnRegistryAttributes;
import org.apache.hadoop.registry.secure.AbstractSecureRegistryTest;
import org.apache.zookeeper.common.PathUtils;
import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;

import static org.apache.hadoop.registry.client.binding.RegistryTypeUtils.*;

/**
 * This is a set of static methods to aid testing the registry operations.
 * The methods can be imported statically —or the class used as a base
 * class for tests.
 */
public class RegistryTestHelper extends Assert {
  public static final String SC_HADOOP = "org-apache-hadoop";
  public static final String USER = "devteam/";
  public static final String NAME = "hdfs";
  public static final String API_WEBHDFS = "classpath:org.apache.hadoop.namenode.webhdfs";
  public static final String API_HDFS = "classpath:org.apache.hadoop.namenode.dfs";
  public static final String USERPATH = RegistryConstants.PATH_USERS + USER;
  public static final String PARENT_PATH = USERPATH + SC_HADOOP + "/";
  public static final String ENTRY_PATH = PARENT_PATH + NAME;
  public static final String NNIPC = "uuid:423C2B93-C927-4050-AEC6-6540E6646437";
  public static final String IPC2 = "uuid:0663501D-5AD3-4F7E-9419-52F5D6636FCF";
  private static final Logger LOG =
      LoggerFactory.getLogger(RegistryTestHelper.class);
  private static final RegistryUtils.ServiceRecordMarshal recordMarshal =
      new RegistryUtils.ServiceRecordMarshal();
  public static final String HTTP_API = "http://";

  /**
   * Assert the path is valid by ZK rules
   * @param path path to check
   */
  public static void assertValidZKPath(String path) {
    try {
      PathUtils.validatePath(path);
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException("Invalid Path " + path + ": " + e, e);
    }
  }

  /**
   * Assert that a string is not empty (null or "")
   * @param message message to raise if the string is empty
   * @param check string to check
   */
  public static void assertNotEmpty(String message, String check) {
    if (StringUtils.isEmpty(check)) {
      fail(message);
    }
  }

  /**
   * Assert that a string is empty (null or "")
   * @param check string to check
   */
  public static void assertNotEmpty(String check) {
    if (StringUtils.isEmpty(check)) {
      fail("Empty string");
    }
  }

  /**
   * Log the details of a login context
   * @param name name to assert that the user is logged in as
   * @param loginContext the login context
   */
  public static void logLoginDetails(String name,
      LoginContext loginContext) {
    assertNotNull("Null login context", loginContext);
    Subject subject = loginContext.getSubject();
    LOG.info("Logged in as {}:\n {}", name, subject);
  }

  /**
   * Set the JVM property to enable Kerberos debugging
   */
  public static void enableKerberosDebugging() {
    System.setProperty(AbstractSecureRegistryTest.SUN_SECURITY_KRB5_DEBUG,
        "true");
  }
  /**
   * Set the JVM property to enable Kerberos debugging
   */
  public static void disableKerberosDebugging() {
    System.setProperty(AbstractSecureRegistryTest.SUN_SECURITY_KRB5_DEBUG,
        "false");
  }

  /**
   * General code to validate bits of a component/service entry built iwth
   * {@link #addSampleEndpoints(ServiceRecord, String)}
   * @param record instance to check
   */
  public static void validateEntry(ServiceRecord record) {
    assertNotNull("null service record", record);
    List<Endpoint> endpoints = record.external;
    assertEquals(2, endpoints.size());

    Endpoint webhdfs = findEndpoint(record, API_WEBHDFS, true, 1, 1);
    assertEquals(API_WEBHDFS, webhdfs.api);
    assertEquals(AddressTypes.ADDRESS_URI, webhdfs.addressType);
    assertEquals(ProtocolTypes.PROTOCOL_REST, webhdfs.protocolType);
    List<Map<String, String>> addressList = webhdfs.addresses;
    Map<String, String> url = addressList.get(0);
    String addr = url.get("uri");
    assertTrue(addr.contains("http"));
    assertTrue(addr.contains(":8020"));

    Endpoint nnipc = findEndpoint(record, NNIPC, false, 1,2);
    assertEquals("wrong protocol in " + nnipc, ProtocolTypes.PROTOCOL_THRIFT,
        nnipc.protocolType);

    Endpoint ipc2 = findEndpoint(record, IPC2, false, 1,2);
    assertNotNull(ipc2);

    Endpoint web = findEndpoint(record, HTTP_API, true, 1, 1);
    assertEquals(1, web.addresses.size());
    assertEquals(1, web.addresses.get(0).size());
  }

  /**
   * Assert that an endpoint matches the criteria
   * @param endpoint endpoint to examine
   * @param addressType expected address type
   * @param protocolType expected protocol type
   * @param api API
   */
  public static void assertMatches(Endpoint endpoint,
      String addressType,
      String protocolType,
      String api) {
    assertNotNull(endpoint);
    assertEquals(addressType, endpoint.addressType);
    assertEquals(protocolType, endpoint.protocolType);
    assertEquals(api, endpoint.api);
  }

  /**
   * Assert the records match.
   * @param source record that was written
   * @param resolved the one that resolved.
   */
  public static void assertMatches(ServiceRecord source, ServiceRecord resolved) {
    assertNotNull("Null source record ", source);
    assertNotNull("Null resolved record ", resolved);
    assertEquals(source.description, resolved.description);

    Map<String, String> srcAttrs = source.attributes();
    Map<String, String> resolvedAttrs = resolved.attributes();
    String sourceAsString = source.toString();
    String resolvedAsString = resolved.toString();
    assertEquals("Wrong count of attrs in \n" + sourceAsString
                 + "\nfrom\n" + resolvedAsString,
        srcAttrs.size(),
        resolvedAttrs.size());
    for (Map.Entry<String, String> entry : srcAttrs.entrySet()) {
      String attr = entry.getKey();
      assertEquals("attribute "+ attr, entry.getValue(), resolved.get(attr));
    }
    assertEquals("wrong external endpoint count",
        source.external.size(), resolved.external.size());
    assertEquals("wrong external endpoint count",
        source.internal.size(), resolved.internal.size());
  }

  /**
   * Find an endpoint in a record or fail,
   * @param record record
   * @param api API
   * @param external external?
   * @param addressElements expected # of address elements?
   * @param addressTupleSize expected size of a type
   * @return the endpoint.
   */
  public static Endpoint findEndpoint(ServiceRecord record,
      String api, boolean external, int addressElements, int addressTupleSize) {
    Endpoint epr = external ? record.getExternalEndpoint(api)
                            : record.getInternalEndpoint(api);
    if (epr != null) {
      assertEquals("wrong # of addresses",
          addressElements, epr.addresses.size());
      assertEquals("wrong # of elements in an address tuple",
          addressTupleSize, epr.addresses.get(0).size());
      return epr;
    }
    List<Endpoint> endpoints = external ? record.external : record.internal;
    StringBuilder builder = new StringBuilder();
    for (Endpoint endpoint : endpoints) {
      builder.append("\"").append(endpoint).append("\" ");
    }
    fail("Did not find " + api + " in endpoints " + builder);
    // never reached; here to keep the compiler happy
    return null;
  }

  /**
   * Log a record
   * @param name record name
   * @param record details
   * @throws IOException only if something bizarre goes wrong marshalling
   * a record.
   */
  public static void logRecord(String name, ServiceRecord record) throws
      IOException {
    LOG.info(" {} = \n{}\n", name, recordMarshal.toJson(record));
  }

  /**
   * Create a service entry with the sample endpoints
   * @param persistence persistence policy
   * @return the record
   * @throws IOException on a failure
   */
  public static ServiceRecord buildExampleServiceEntry(String persistence) throws
      IOException,
      URISyntaxException {
    ServiceRecord record = new ServiceRecord();
    record.set(YarnRegistryAttributes.YARN_ID, "example-0001");
    record.set(YarnRegistryAttributes.YARN_PERSISTENCE, persistence);
    addSampleEndpoints(record, "namenode");
    return record;
  }

  /**
   * Add some endpoints
   * @param entry entry
   */
  public static void addSampleEndpoints(ServiceRecord entry, String hostname)
      throws URISyntaxException {
    assertNotNull(hostname);
    entry.addExternalEndpoint(webEndpoint(HTTP_API,
        new URI("http", hostname + ":80", "/")));
    entry.addExternalEndpoint(
        restEndpoint(API_WEBHDFS,
            new URI("http", hostname + ":8020", "/")));

    Endpoint endpoint = ipcEndpoint(API_HDFS, null);
    endpoint.addresses.add(RegistryTypeUtils.hostnamePortPair(hostname, 8030));
    entry.addInternalEndpoint(endpoint);
    InetSocketAddress localhost = new InetSocketAddress("localhost", 8050);
    entry.addInternalEndpoint(
        inetAddrEndpoint(NNIPC, ProtocolTypes.PROTOCOL_THRIFT, "localhost",
            8050));
    entry.addInternalEndpoint(
        RegistryTypeUtils.ipcEndpoint(
            IPC2, localhost));
  }

  /**
   * Describe the stage in the process with a box around it -so as
   * to highlight it in test logs
   * @param log log to use
   * @param text text
   * @param args logger args
   */
  public static void describe(Logger log, String text, Object...args) {
    log.info("\n=======================================");
    log.info(text, args);
    log.info("=======================================\n");
  }

  /**
   * log out from a context if non-null ... exceptions are caught and logged
   * @param login login context
   * @return null, always
   */
  public static LoginContext logout(LoginContext login) {
    try {
      if (login != null) {
        LOG.debug("Logging out login context {}", login.toString());
        login.logout();
      }
    } catch (LoginException e) {
      LOG.warn("Exception logging out: {}", e, e);
    }
    return null;
  }

  /**
   * Login via a UGI. Requres UGI to have been set up
   * @param user username
   * @param keytab keytab to list
   * @return the UGI
   * @throws IOException
   */
  public static UserGroupInformation loginUGI(String user, File keytab) throws
      IOException {
    LOG.info("Logging in as {} from {}", user, keytab);
    return UserGroupInformation.loginUserFromKeytabAndReturnUGI(user,
        keytab.getAbsolutePath());
  }

  public static ServiceRecord createRecord(String persistence) {
    return createRecord("01", persistence, "description");
  }

  public static ServiceRecord createRecord(String id, String persistence,
      String description) {
    ServiceRecord serviceRecord = new ServiceRecord();
    serviceRecord.set(YarnRegistryAttributes.YARN_ID, id);
    serviceRecord.description = description;
    serviceRecord.set(YarnRegistryAttributes.YARN_PERSISTENCE, persistence);
    return serviceRecord;
  }

  public static ServiceRecord createRecord(String id, String persistence,
      String description, String data) {
    return createRecord(id, persistence, description);
  }
}