package it.com.atlassian.stash.installer.windows.upgrade;

import com.atlassian.bitbucket.async.AsyncTestUtils;
import com.atlassian.bitbucket.async.WaitCondition;
import com.atlassian.bitbucket.server.ApplicationState;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class WindowsInstaller3xUpgradeTest {
    private static final Logger log = LoggerFactory.getLogger(WindowsInstaller3xUpgradeTest.class);

    private static final String TARGET_DIR = new File("target\\").getAbsolutePath();

    private static final String BITBUCKET_INSTALL_DIR = TARGET_DIR  + "\\Bitbucket";
    private static final String BITBUCKET_INSTALLER = "target\\bitbucket-installer-x64.exe";
    private static final String BITBUCKET_HOME = TARGET_DIR  + "\\home";
    private static final String STASH_INSTALL_DIR = TARGET_DIR  + "\\Stash";
    private static final String STASH_INSTALLER = "target\\stash-installer-x64.exe";
    private static final String VARFILE_NAME = "varfile.properties";
    private static final String VARFILE_LOCATION = "target\\" + VARFILE_NAME;
    private static final String STASH_CONFIG_PROERTIES_LOCATION = BITBUCKET_HOME + "\\shared\\stash-config.properties";
    // were looking for an escaped path in the batch file so we need to double up the escape here
    private static final String HOME_SET_LINE = "set BITBUCKET_HOME=" + BITBUCKET_HOME.replace("\\", "\\\\");

    private static final Map<String, String> BITBUCKET_VARFILE = ImmutableMap.<String, String>builder()
            .put("app.install.service$Boolean", "true")
            .put("portChoice", "custom")
            .put("httpPort", "7991")
            .put("serverPort", "8007")
            .put("app.bitbucketHome", BITBUCKET_HOME.replace("\\", "\\\\"))
            .put("app.defaultInstallDir", BITBUCKET_INSTALL_DIR.replace("\\", "\\\\"))
            .put("installation.type$Integer", "1")
            .build();
    private static final Map<String, String> STASH_VARFILE = ImmutableMap.<String, String>builder()
            .put("app.install.service$Boolean", "true")
            .put("portChoice", "custom")
            .put("httpPort", "7991")
            .put("serverPort", "8007")
            .put("app.stashHome", BITBUCKET_HOME.replace("\\", "\\\\"))
            .put("app.defaultInstallDir", STASH_INSTALL_DIR.replace("\\", "\\\\"))
            .put("launch.application$Boolean", "false")
            .build();

    private static final Map<String, String> STASH_SETUP_PROPERTIES = ImmutableMap.<String, String>builder()
            .put("setup.displayName", "Stash Installer test")
            .put("setup.baseUrl", "http://localhost:7991/")
            /** 1 Hour timebomb license for 2 users */
            .put("setup.license", "AAABHg0ODAoPeNptkEFrwkAQhe/+ioWeI0mMhwoL1RipYEwwtgVv0+2kWYi7YWYj9d9X43qw9Pref" +
                    "O/NzNMHfokcSESJiKazcDKbPous2os4jOIRO+BmnJ2g7cFpa2QNLaOXl8iKdDfoe2QnWq3QMIrak" +
                    "nANigpd34lVb5S4+ixsLaor6gNSwiF1CQ7ltS8IJ0GU3F1rHCi3hSPKUiPRvTcHbRwaMAqzn07Te" +
                    "eDL6NX7F0ifUDrq8TEqu5Ct7LCzmqx7AdcCswYzVvboJze3E/bnDofitMjzbJeu55tRhXRCWi/lI" +
                    "jmsgrDK1kG6WxyC92I79XQJ5AzSQHqpoG8wmm/fm98bvbntj59IRf3GSCzjxx3+va3sSTXA+Pdlv" +
                    "zUInXUwLQIVAJPALS44YIOLoi1gP9UzA6y0IU1rAhQCZCUupoxQuFvWoD1nt9f2QUGQ+g==X02ei")
            .put("setup.sysadmin.username", "admin")
            .put("setup.sysadmin.password", "admin")
            .put("setup.sysadmin.displayName", "admin")
            .put("setup.sysadmin.emailAddress", "admin@exmaple.com")
            .put("jdbc.driver", System.getProperty("jdbc.driver"))
            .put("jdbc.url", System.getProperty("jdbc.url"))
            .put("jdbc.user", System.getProperty("jdbc.user"))
            .put("jdbc.password", System.getProperty("jdbc.password"))
            .build();

    @Test
    public void testUpgradeSuccess() throws Exception {
        assertThat("Stash installs correctly", runInstaller(STASH_INSTALLER, STASH_VARFILE), is(0));

        writePropertiesFile(STASH_CONFIG_PROERTIES_LOCATION, STASH_SETUP_PROPERTIES);

        assertThat("AtlassianStash service hasn't been started", startService("AtlassianStash"), is(0));

        waitForFirstRun();
        assertThat("Can't stop Stash", stopService("AtlassianStash"), is(0));

        waitForServiceStop("AtlassianStash");

        assertThat("atlstash user hasn't been created", checkUser("atlstash"), is(0));
        assertThat("AtlassianStash service hasn't been created", checkService("AtlassianStash"), is(0));

        assertThat("Bitbucket upgrade failed", runInstaller(BITBUCKET_INSTALLER, BITBUCKET_VARFILE), is(0));

        waitForFirstRun();
        assertThat("Can't stop Bitbucket", stopService("AtlassianBitbucket"), is(0));

        assertThat("atlbitbucket user hasn't been created", checkUser("atlbitbucket"), is(0));
        assertThat("AtlassianBitbucket service hasn't been created", checkService("AtlassianBitbucket"), is(0));

        assertThat("atlstash user hasn't been deleted", checkUser("atlstash"), not(0));
        assertThat("AtlassianStash service hasn't been deleted", checkService("AtlassianStash"), not(0));

        List<String> lines = Files.readAllLines(FileSystems.getDefault().getPath(BITBUCKET_INSTALL_DIR, "bin", "set-bitbucket-home.bat"));
        assertThat("Expected script to contain export for BITBUCKET_HOME", lines, hasItem(containsString(HOME_SET_LINE)));
    }

    private int checkService(String name) throws Exception {
        return runServiceCmd("query", name);
    }

    private int checkUser(String username) throws Exception {
        return runCmd("net", "user", username);
    }

    private String getStateOrNull() {
        try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
            HttpGet req = new HttpGet("http://localhost:7991/status");
            CloseableHttpResponse response = httpClient.execute(req);
            String rawJson = EntityUtils.toString(response.getEntity());
            JsonElement jsonElement = new JsonParser().parse(rawJson);
            if (jsonElement.isJsonNull()) {
                return null;
            }
            JsonElement stateElement = jsonElement.getAsJsonObject().get("state");
            return stateElement.getAsString();

        } catch (IOException e) {
            return null;
        }
    }

    private int runCmd(String... args) throws Exception {
        return runCmd(any(String.class), any(String.class), args);
    }

    private int runCmd(Matcher<String> outMatcher, Matcher<String> errMatcher, String... args) throws Exception {
        log.info("Running: `" + String.join(" ", args) + "`");

        ProcessBuilder pb = new ProcessBuilder();
        pb.command(args);
        Process netProc = pb.start();

        BufferedReader stdout = new BufferedReader(new InputStreamReader(netProc.getInputStream()));
        BufferedReader stderr = new BufferedReader(new InputStreamReader(netProc.getErrorStream()));
        int retCode = netProc.waitFor();

        StringBuilder stdoutSb = new StringBuilder();
        String line;

        while ((line = stdout.readLine()) != null) {
            stdoutSb.append(line).append('\n');
        }
        if (stdoutSb.length() != 0) {
            log.info("Stdout:");
            log.info(stdoutSb.toString());
        }
        assertThat(stdoutSb.toString(), outMatcher);

        StringBuilder stderrSb = new StringBuilder();
        while ((line = stderr.readLine()) != null) {
            stderrSb.append(line).append('\n');
        }
        if (stderrSb.length() != 0) {
            log.info("Stderr:");
            log.info(stderrSb.toString());
        }
        assertThat(stderrSb.toString(), errMatcher);

        log.info("Exit code: " + retCode);
        return retCode;
    }

    private int runInstaller(String installerLocation, Map<String, String> vars) throws Exception {
        writePropertiesFile(VARFILE_LOCATION, vars);

        return runCmd(installerLocation, "-q", "-console", "-Dinstall4j.debug=true", "-Dinstall4j.logToStderr=true", "-varfile", VARFILE_NAME);
    }

    private void writePropertiesFile(String location, Map<String, String> properties) throws IOException {
        File varfileLocation = new File(location);
        try (FileWriter varfileWriter = new FileWriter(varfileLocation)) {
            for (Map.Entry<String, String> var : properties.entrySet()) {
                varfileWriter.append(var.getKey()).append('=').append(var.getValue()).append('\n');
            }
        }
    }

    private int runServiceCmd(String command, String name) throws Exception {
        return runCmd("sc", command, name);
    }

    private int startService(String name) throws Exception {
        return runServiceCmd("start", name);
    }
    private int stopService(String name) throws Exception {
        return runServiceCmd("stop", name);
    }

    private void waitForFirstRun() {
        AsyncTestUtils.waitFor(new WaitCondition() {
            private String lastState;

            @Override
            public boolean test() throws Exception {
                lastState = getStateOrNull();
                return ApplicationState.RUNNING.name().equals(lastState);
            }

            @Override
            public void describeFailure(Description description) throws Exception {
                description.appendText("Expected startup to succeed. Last state was '" + lastState);
            }
        }, TimeUnit.SECONDS.toMillis(300), TimeUnit.SECONDS.toMillis(5));
    }

    private void waitForServiceStop(String serviceName) {
        AsyncTestUtils.waitFor(new WaitCondition() {
            @Override
            public boolean test() throws Exception {
                try {
                    runCmd(containsString("STOPPED"), any(String.class), "sc", "query", serviceName);
                    return true;
                } catch (AssertionError ignored) {
                    return false;
                }
            }

            @Override
            public void describeFailure(Description description) throws Exception {
                description.appendText("Expected STOP didn't happen.");
            }
        }, TimeUnit.SECONDS.toMillis(300), TimeUnit.SECONDS.toMillis(5));
    }
}
