package nxm.sys.test.junit.lib;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.security.KeyStore;
import java.util.Enumeration;
import java.util.Properties;

import nxm.sys.lib.Midas;
import nxm.sys.lib.SSLUtilities;
import nxm.sys.lib.Table;
import nxm.sys.test.junit.NMTestCase;

/** This will test the SSLUtilities class.
    @author  Jeanette McCall
    @since NeXtMidas 3.7.2
 */
public final class SSLUtilitiesTest extends NMTestCase {

  private static final String JSON_1 =
      "{\"properties\":{\"dn\":\"user_dn1\",\"title\":\"engineer\",\"accesses\":[\"access_a\",\"access_c\"]}}";
  private static final String JSON_2 =
      "{\"properties\":{\"parents\":{\"mother\":\"Mary\",\"father\":\"Joseph\"},\"age\":12}}";
  private static final String JSON_3 = "{\"float\":5.0,\"boolean\":true}";
  private static final String JSON_NOT_OBJ = "[1.2,3.4]";
  private static final String JSON_EMPTY = "{}";

  /** public constructor to allow running this test case directly with JUnit. */
  public SSLUtilitiesTest (String methodName) { this(methodName,null); }

  /** Creates a new instance of the class.
      @param methodName The name of the method being called.
      @param midas      The midas context testTo use when running the tests.
   */
  public SSLUtilitiesTest (String methodName, Midas midas) {
    super(methodName, midas);
  }

  //*****************************************************************************************
  //  PUBLIC METHODS (alphabetical)
  //*****************************************************************************************

  // public void getCertInfoLogLevel () {} // macro testing - for logging only

  // public void getClientAttributes () {} // only interactive macro testing

  // public void testGetUserAlias () {} // none - requires manual certificate specific testing

  // public void testGetUserDN () {} // none - requires manual certificate specific testing

  // public void testGetUserCN () {} // none - requires manual certificate specific testing

  // public void testInitSSLContext () {}  // limited JUnit through some private methods

  // public void isInitDone () {} // only interactive macro testing

  /**
   * Tests the hasAttribute method which searches the JSON Object String to find
   * if the JSON Object/Array matching the attribute name contains a/the value
   * matching the specified attribute value
   */
  public void testJsonObjHasAttribute () {
    boolean hasAttr;

    // Check single level of nesting with attribute being either key value or value in array
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_1, "engineer",
        "properties,title",false);
    assertTrue("JSON nested key/value found: ",hasAttr);
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_1, "access_a",
        "properties,accesses",false);
    assertTrue("JSON nested  array/value found: ",hasAttr);
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_1, "access_b",
        "properties,accesses",false);
    assertFalse("JSON nested non-existant array/value not found: ",hasAttr);
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_1, 1.0,
        "properties,accesses",false);
    assertFalse("JSON nested non-existant float array/value not found: ",
        hasAttr);

    // Check greater nesting and attribute that is Integer
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_2, "Mary",
        "properties parents, mother",false);
    assertTrue("JSON 3 deep nested key/value found: ",hasAttr);
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_2, 12,
        "properties, age",false);
    assertTrue("JSON Integer found: ",hasAttr);

    // Check Float and Boolean values
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_3, 5.0, "float",
        false);
    assertTrue("JSON Float found: ",hasAttr);
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_3, "five", "float",
        false);
    assertFalse("JSON non-existant Float not found: ",hasAttr);
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_3, true, "boolean",
        false);
    assertTrue("JSON Boolean not found: ",hasAttr);
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_3, "hi", "boolean",
        false);
    assertFalse("JSON non-existant boolean not found: ",hasAttr);

    // Check JSON Array (not a valid JSON object)
    hasAttr = false;
    try {
      hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_NOT_OBJ, 1.2, "array",false);
    }
    catch (Exception e) {
      // Exception expected
    }
    finally {
      assertFalse("JSON array invalid: ",hasAttr);
    }

    // Check empty JSON object)
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_EMPTY, 1.2, "array",false);
    assertFalse("JSON empty not have object: ",hasAttr);

    // Check partial patch case
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_1, "ss_a",
        "properties,accesses",true);
    assertTrue("JSON array/value partial match found: ",hasAttr);
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_1, "dn1",
        "properties,dn",true);
    assertTrue("JSON nested key/value partial match found: ",hasAttr);
    hasAttr = SSLUtilities.jsonObjHasAttribute(JSON_1, "ss_b",
        "properties,accesses",true);
    assertFalse("JSON array/value partial match not found: ",hasAttr);

  }

  /** Test method that directly validates agains a JSON response */
  public void testVerifyJsonHasAttributes () {
    String jsonPathProps = urlSysTest + "JsonKeyPaths.properties";
    SSLUtilities.overideJsonKeyPaths(jsonPathProps);
    boolean allowPartialMatch = true;
    Table attrValueTbl = new Table("{title=engineer,state=VA}", Table.CASESENSITIVE);
    String jsonResponse =
        "{\"title\": \"engineer\",\"address\": {\"street\": \"Conference Center\",\"state\" : \"VA\"}}";
    boolean authorized =
        SSLUtilities.verifyJsonHasAttributes(jsonResponse,attrValueTbl,allowPartialMatch);
    assertTrue("SSLUtilities.verifyJsonHasAttributes says authorized:",authorized);
    jsonResponse =
        "{\"title\": \"manager\",\"address\": {\"street\": \"Conference Center\",\"state\" : \"VA\"}}";
    authorized =
        SSLUtilities.verifyJsonHasAttributes(jsonResponse,attrValueTbl,allowPartialMatch);
    assertFalse("SSLUtilities.verifyJsonHasAttributes says not authorized:",authorized);
  }

  /**
   * Tests the passwdFromFile on various password file types:
   * 1) no spaces in password
   * 2) spaces in password
   * 3) multiple passwords on multiple lines (just reads first line)
   */
  public void testPasswdFromFile () {

    String pwd1 = new String(SSLUtilities.passwdFromFile(urlSysTest+"test_password_one_line.txt"));
    String pwd2 = new String(SSLUtilities.passwdFromFile(urlSysTest+"test_password_with_space.txt"));
    String pwd3 = new String(SSLUtilities.passwdFromFile(urlSysTest+"test_password_multiple_lines.txt"));

    String expected = "myPassword";
    assertEquals("Password without white space",expected,pwd1);
    assertEquals("Password has white space removed",expected,pwd2);
    assertEquals("Password read only from first line of file",expected,pwd3);

  }

  // public void setCertInfoLogLevel () {} // macro testing - for logging only

  // public void signJarFile () {} // TODO - currently only manual testing

  // public void verifyGroupProjects () {} // partial testing through testJsonObjHasAttribute

  // public void verifyUserAttributess () {} // extensive testing through testJsonObjHasAttribute


  //*****************************************************************************************
  //  PRIVATE METHODS (alphabetical)
  //*****************************************************************************************

  /**
   * Tests findFile
   * (first file that exists will be chosen)
   */
  public void testFindFile () {
    // only last file should be found
    String files = "${HOME}/home_file_that_does_not_exist.txt," +
                   "/home/${USER}/user_file_that_does_not_exist.txt," +
                   "${HOST}/host_file_that_does_not_exist.txt," +
                   urlSysTest+"SSLUtilities.properties";
    try {
      Method method = SSLUtilities.class.getDeclaredMethod("findFile", String.class);
      method.setAccessible(true);
      String fileFound = (String)method.invoke(SSLUtilities.class, files);
      if (fileFound == null) {
        fail("SSLUtilities.findFile did not find file");
      }
      else {
        assertTrue("SSLUtilities.findFile", fileFound.endsWith("SSLUtilities.properties"));
      }
    } catch (Exception e) {
      fail("SSLUtilities.findFile exception:" +e.getMessage());
    }
  }

  /**
   * Tests private loadPropsFile method<br>
   * 1) Tests that the loadPropsFile method throws an informative exception
   *    when the properties file specified does not contain the right properties
   * 2) Tests valid properties file
   * @since 3.7.3
   */
  public void testLoadPropsFile () {

    // test bad properties file
    try {
      Method method = SSLUtilities.class.getDeclaredMethod("loadPropsFile", Object.class, boolean.class);
      method.setAccessible(true);
      Object propFile = urlSysTest+"BadProperties.properties";
      method.invoke(SSLUtilities.class,propFile, true);
      fail("Bad properties file triggers Exception");
    }
    catch (Exception e) {
      String causeStr = e.getCause().toString();
      boolean correctException = causeStr.contains("Error loading") &&
          causeStr.contains("File does not contain any of the required SSLUtilities.* keys");
      assertTrue("Bad properties file triggers 'Error Loading' Exception",correctException);
    }

    // test valid properties file
    try {
      Method method = SSLUtilities.class.getDeclaredMethod("loadPropsFile", Object.class, boolean.class);
      method.setAccessible(true);
      Object propFile = urlSysTest+"SSLUtilities.properties";
      Properties props = (Properties)method.invoke(SSLUtilities.class,propFile, false);
      String capiEnabled = props.getProperty("SSLUtilities.capiEnabled");
      assertEquals("SSLUtilities.loadPropsFile loaded example SSLUtiltities.properties","true",capiEnabled);
    }
    catch (Exception e) {
      fail("SSLUtilities.loadPropsFile unexpected exception:"+e);
    }
  }

  /**
   * Test creation of KeyStore from PEM file with multiple certificates
   * 1) PEM file with certificates stored first (within certificate(s)/key pairs)
   * 2) PEM file with keys stored first (within key/certificate(s) pairs)
   * Check that the proper number of cert/key pairs are loaded and given distinct
   * aliases
   */
  public void testReadPemFile () {

    try {
      String pemCertsFirst = urlSysTest+"test_multiple_cert_first.pem";
      String pemKeyFirstFile = "test_multiple_key_first.pem";
      String pemKeyFirstPath = urlSysTest+"test_multiple_key_first.pem";

      // Create KeyStores and check that the resulting KeyStores are the correct size
      KeyStore keyStore1 = SSLUtilities.readPemFile(pemCertsFirst, null).getKeyStore();
      assertEquals("PEM Key Store with multiple certificates (cert first) correct size",
          1, keyStore1.size());
      KeyStore keyStore2 = SSLUtilities.readPemFile(pemKeyFirstPath, null).getKeyStore();
      assertEquals("PEM Key Store with multiple certificates (key first) correct size",
          1, keyStore2.size());

      // check that the assigned aliases are as expected
      int certNum = 1;
      for (Enumeration<String> e = keyStore2.aliases(); e.hasMoreElements();) {
        String actual = e.nextElement();
        String expected = "pem-file-"+pemKeyFirstFile;
        if (certNum > 1) {
          expected = expected + "_" + certNum;
        }
        assertEquals("PEM Key Store with multiple certificates given distinct aliases",
            expected, actual);
        certNum++;
      }
    }
    catch (Exception e) {
      Throwable cause = e.getCause();
      if (cause==null) {
        fail("testReadPemFile test threw Exception:"+e);
      }
      else {
        fail("testReadPemFile test threw Exception:"+cause.toString());
      }
    }
  }


  /**
   * Helper method to read a buffered reader and see if it contains a keyword.
   * @param br BufferedReader that will provide the input text to be validated.
   * @param keyword The word or phrase that needs to be read in by the Buffered Reader
   * @return True if the keyword was there, false otherwise.
   * @throws IOException
   */
  private static boolean containsKeyword(BufferedReader br, String keyword) throws IOException {
    String line = null;
    while ((line = br.readLine()) != null) {
      if (line.toLowerCase().contains(keyword)) return true;
    }
    return false;

  }

  /**
   * This method will validate that the given jar parameter is signed.
   * @param filePath - the file path of the jar to be tested.
   * @return true if the jar is signed and false if it it unsigned or can't be verified.
   */
  public static boolean validateJarSigning (String filePath) {
    // String command = "/usr/bin/jarsigner -verify " + filePath;
    String javaHome = System.getenv("JAVAHOME");
    ProcessBuilder pb = new ProcessBuilder(javaHome + "/bin/jarsigner", "-verify", filePath);
    int returnVal = 1;
    Process p;
    BufferedReader br;
    try {
      p = pb.start();
      returnVal = p.waitFor();
      if (returnVal != 0) return false;
      br = new BufferedReader(new InputStreamReader(p.getInputStream()));
      return containsKeyword(br, "verified");

    }
    catch (IOException e) {

      e.printStackTrace();
    }
    catch (InterruptedException e) {
      e.printStackTrace();
    }
    return false;

  }
}