/*
 * 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.ambari.funtest.server.tests;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLNonTransientConnectionException;
import java.util.Properties;

import com.google.common.base.Charsets;
import org.apache.ambari.funtest.server.LocalAmbariServer;
import org.apache.ambari.server.audit.AuditLoggerModule;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.controller.ControllerModule;
import org.apache.ambari.server.ldap.LdapModule;
import org.apache.ambari.server.orm.DBAccessor;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.persist.PersistService;

/**
 * Base test infrastructure.
 */
public class ServerTestBase {
    private static Log LOG = LogFactory.getLog(ServerTestBase.class);

    /**
     * Run the ambari server on a thread.
     */
    protected static Thread serverThread = null;

    /**
     * Instance of the local ambari server, which wraps the actual
     * ambari server with test configuration.
     */
    protected static LocalAmbariServer server = null;

    /**
     * Server port
     */
    protected static int serverPort = 9995;

    /**
     * Server agent port
     */
    protected static int serverAgentPort = 9000;

    /**
     * Guice injector using an in-memory DB.
     */
    protected static Injector injector = null;

    /**
     * Server URL
     */
    protected static String SERVER_URL_FORMAT = "http://localhost:%d";

    /**
     * Initialize the AmbariServer and database once for the entire
     * duration of the tests since AmbariServer is a singleton.
     */
    private static boolean isInitialized;

    /**
     * Create and populate the DB. Start the AmbariServer.
     * @throws Exception
     */
    @BeforeClass
    public static void setupTest() throws Exception {
        if (!isInitialized) {
            Properties properties = readConfigFile();
            properties.setProperty(Configuration.SERVER_PERSISTENCE_TYPE.getKey(), "remote");
            properties.setProperty(Configuration.SERVER_JDBC_URL.getKey(), Configuration.JDBC_IN_MEMORY_URL);
            properties.setProperty(Configuration.SERVER_JDBC_DRIVER.getKey(), Configuration.JDBC_IN_MEMORY_DRIVER);
            properties.setProperty(Configuration.OS_VERSION.getKey(), "centos7");

            properties.setProperty(Configuration.AGENT_USE_SSL.getKey(), "false");
            properties.setProperty(Configuration.CLIENT_API_PORT.getKey(), Integer.toString(serverPort));
            properties.setProperty(Configuration.SRVR_ONE_WAY_SSL_PORT.getKey(), Integer.toString(serverAgentPort));
            String tmpDir = System.getProperty("java.io.tmpdir");
            properties.setProperty(Configuration.SRVR_KSTR_DIR.getKey(), tmpDir);

            ControllerModule testModule = new ControllerModule(properties);

            injector = Guice.createInjector(testModule, new AuditLoggerModule(), new LdapModule());
            injector.getInstance(PersistService.class).start();
            initDB();

            server = injector.getInstance(LocalAmbariServer.class);
            serverThread = new Thread(server);
            serverThread.start();
            waitForServer();

            isInitialized = true;
        }
    }

    /**
     * Creates the basic authentication string for admin:admin
     *
     * @return
     */
    protected static String getBasicAdminAuthentication() {
        String authString = getAdminUserName() + ":" + getAdminPassword();
        byte[] authEncBytes = Base64.encodeBase64(authString.getBytes());
        String authStringEnc = new String(authEncBytes);

        return "Basic " + authStringEnc;
    }

    /**
     * Creates the DB and populates it.
     *
     * @throws IOException
     * @throws SQLException
     */
    protected static void initDB() throws IOException, SQLException {
        createSourceDatabase();
    }

    /**
     * Drops the Derby DB.
     *
     * @throws ClassNotFoundException
     * @throws SQLException
     */
    protected static void dropDatabase() throws ClassNotFoundException, SQLException {
        String DROP_DERBY_URL = "jdbc:derby:memory:myDB/ambari;drop=true";
        Class.forName(Configuration.JDBC_IN_MEMORY_DRIVER);
        try {
            DriverManager.getConnection(DROP_DERBY_URL);
        } catch (SQLNonTransientConnectionException ignored) {
            LOG.info("Database dropped ", ignored); //error 08006 expected
        }
    }

    /**
     * Executes Ambari-DDL-Derby-CREATE.sql
     *
     * @throws IOException
     * @throws SQLException
     */
    private static void createSourceDatabase() throws IOException, SQLException {
        //create database
        File projectDir = new File(System.getProperty("user.dir"));
        File ddlFile = new File(projectDir.getParentFile(), "ambari-server/src/main/resources/Ambari-DDL-H2-CREATE.sql");
        String ddlFilename = ddlFile.getPath();
        DBAccessor dbAccessor = injector.getInstance(DBAccessor.class);
        dbAccessor.executeScript(ddlFilename);
    }

    /**
     * Gets the default administration user name
     *
     * @return
     */
    protected static String getAdminUserName() {
        return "admin";
    }

    /**
     * Gets the default administrator password
     *
     * @return
     */
    protected static String getAdminPassword() {
        return "admin";
    }

    /**
     * Waits for the local server until it is ready to accept requests.
     *
     * @throws Exception
     */
    private static void waitForServer() throws Exception {
        int count = 1;

        while (!isServerUp()) {
            serverThread.join(count * 10000);     // Give a few seconds for the ambari server to start up
            //count += 1; // progressive back off
            //count *= 2; // exponential back off
        }
    }

    /**
     * Attempt to query the server for the stack. If the server is up,
     * we will get a response. If not, an exception will be thrown.
     *
     * @return - True if the local server is responsive to queries.
     *           False, otherwise.
     */
    private static boolean isServerUp() throws IOException {
        String apiPath = "/api/v1/stacks";

        String apiUrl = String.format(SERVER_URL_FORMAT, serverPort) + apiPath;
        CloseableHttpClient httpClient = HttpClients.createDefault();;

        try {
            HttpGet httpGet = new HttpGet(apiUrl);
            httpGet.addHeader("Authorization", getBasicAdminAuthentication());
            httpGet.addHeader("X-Requested-By", "ambari");
            HttpResponse httpResponse = httpClient.execute(httpGet);
            int statusCode = httpResponse.getStatusLine().getStatusCode();
            HttpEntity entity = httpResponse.getEntity();
            String responseBody = entity != null ? EntityUtils.toString(entity) : null;

            return true;
        } catch (IOException ex) {

        } finally {
            httpClient.close();
        }

        return false;
    }

    /**
     * Perform common initialization for each test case.
     *
     * @throws Exception
     */
    @Before
    public void setup() throws Exception {

    }

    /**
     * Perform common clean up for each test case.
     *
     * @throws Exception
     */
    @After
    public void teardown() throws Exception {
    }

    private static Properties readConfigFile() {
        Properties properties = new Properties();
        String configFileName = "ambari.properties";
        //Get property file stream from classpath
        InputStream inputStream = Configuration.class.getClassLoader().getResourceAsStream(configFileName);
        if (inputStream == null) {
            throw new RuntimeException(configFileName + " not found in classpath");
        }
        // load the properties
        try {
            properties.load(new InputStreamReader(inputStream, Charsets.UTF_8));
            inputStream.close();
        } catch (FileNotFoundException fnf) {
            LOG.info("No configuration file " + configFileName + " found in classpath.", fnf);
        } catch (IOException ie) {
            throw new IllegalArgumentException("Can't read configuration file " +
                    configFileName, ie);
        }
        return properties;
    }
}
