package org.jboss.windup.addon.ui;

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;

import javax.inject.Inject;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.io.FileUtils;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.forge.addon.dependencies.Coordinate;
import org.jboss.forge.addon.dependencies.DependencyResolver;
import org.jboss.forge.addon.dependencies.builder.CoordinateBuilder;
import org.jboss.forge.addon.dependencies.builder.DependencyQueryBuilder;
import org.jboss.forge.addon.ui.controller.CommandController;
import org.jboss.forge.addon.ui.result.Failed;
import org.jboss.forge.addon.ui.result.Result;
import org.jboss.forge.addon.ui.test.UITestHarness;
import org.jboss.forge.arquillian.AddonDependencies;
import org.jboss.forge.arquillian.AddonDependency;
import org.jboss.forge.arquillian.archive.AddonArchive;
import org.jboss.forge.furnace.Furnace;
import org.jboss.forge.furnace.addons.Addon;
import org.jboss.forge.furnace.addons.AddonId;
import org.jboss.forge.furnace.manager.AddonManager;
import org.jboss.forge.furnace.manager.request.InstallRequest;
import org.jboss.forge.furnace.manager.request.RemoveRequest;
import org.jboss.forge.furnace.util.Addons;
import org.jboss.forge.furnace.util.OperatingSystemUtils;
import org.jboss.forge.furnace.versions.SingleVersion;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.windup.exec.configuration.options.InputPathOption;
import org.jboss.windup.exec.configuration.options.OutputPathOption;
import org.jboss.windup.exec.updater.RulesetsUpdater;
import org.jboss.windup.ui.DistributionUpdater;
import org.jboss.windup.ui.WindupCommand;
import org.jboss.windup.ui.WindupUpdateDistributionCommand;
import org.jboss.windup.util.Logging;
import org.jboss.windup.util.PathUtil;
import org.jboss.windup.util.ZipUtil;
import org.jboss.windup.util.exception.WindupException;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * @author Ondrej Zizka, ozizka at redhat.com
 */
@RunWith(Arquillian.class)
public class WindupUpdateDistributionCommandTest {
    private static final Logger log = Logging.get(WindupUpdateDistributionCommandTest.class);

    private static final String WINDUP_UI_ADDON_NAME = "org.jboss.windup.ui:windup-ui";

    private static final String WINDUP_OLD_VERSION = "2.2.0.Final";

    @Deployment
    @AddonDependencies({
            @AddonDependency(name = "org.jboss.forge.furnace.container:cdi"),
            @AddonDependency(name = "org.jboss.windup.utils:windup-utils"),
            @AddonDependency(name = WINDUP_UI_ADDON_NAME),
            @AddonDependency(name = "org.jboss.windup.exec:windup-exec"),
            @AddonDependency(name = "org.jboss.windup.graph:windup-graph"),
            @AddonDependency(name = "org.jboss.windup.rules.apps:windup-rules-java"),
            @AddonDependency(name = "org.jboss.forge.addon:maven"),
            @AddonDependency(name = "org.jboss.forge.addon:addon-manager"),
            @AddonDependency(name = "org.jboss.forge.addon:ui-test-harness"),
    })
    public static AddonArchive getDeployment() {
        AddonArchive archive = ShrinkWrap
                .create(AddonArchive.class)
                .addBeansXML()
                .addAsResource(WindupCommandTest.class.getResource(TEST_RULESET_ZIP), TEST_RULESET_ZIP);
        return archive;
    }

    private static String TEST_RULESET_ZIP = "/windup-old-ruleset.zip";

    @Inject
    private DependencyResolver resolver;

    @Inject
    private Addon addon;

    @Inject
    private Furnace furnace;

    @Inject
    private UITestHarness uiTestHarness;

    @Inject
    private RulesetsUpdater updater;

    @Inject
    private DistributionUpdater distUpdater;

    @Inject
    private AddonManager manager;

    @Test
    @Ignore("The current implementation doesn't work as it tampers with addons loaded at the time."
            + " Even if it replaced all Windup addons, it would still fail on Windows as they keep the .jar's locked.")
    public void testUpdateDistribution() throws Exception {
        // Download and unzip an old distribution.
        final CoordinateBuilder coords = CoordinateBuilder.create()
                .setGroupId("org.jboss.windup")
                .setArtifactId("windup-distribution")
                .setClassifier("offline")
                .setVersion("2.2.0.Final")
                .setPackaging("zip");
        System.out.println("Downloading " + coords + ", may take a while.");
        List<Coordinate> results = resolver.resolveVersions(DependencyQueryBuilder.create(coords));

        File windupDir = OperatingSystemUtils.createTempDir();
        this.updater.extractArtifact(results.get(0), windupDir);
        windupDir = DistributionUpdater.getWindupDistributionSubdir(windupDir);
        Assert.assertTrue(windupDir.exists());
        System.setProperty(PathUtil.WINDUP_HOME, windupDir.getAbsolutePath());

        // Run the upgrader.
        distUpdater.replaceWindupDirectoryWithLatestDistribution();

        // Check the new version.
        String newUiVersion = getInstalledAddonVersion(windupDir.toPath().resolve("addons").toString(), WINDUP_UI_ADDON_NAME);
        Assert.assertTrue(SingleVersion.valueOf(newUiVersion).compareTo(SingleVersion.valueOf("2.2.0.Final")) > 0);

        // Try to run Windup from there.
        // TODO: I need to set the harness addons directory to the freshly created dir.
        UITestHarness harness = furnace.getAddonRegistry().getServices(UITestHarness.class).get();
        try (CommandController controller = harness.createCommandController(WindupCommand.class)) {
            controller.initialize();
            controller.setValueFor(InputPathOption.NAME, Collections.singletonList(new File("src/test/resources/test.jar").getAbsolutePath()));
            final File resultDir = new File("target/testRunFromUpgraded");
            resultDir.mkdirs();
            controller.setValueFor(OutputPathOption.NAME, resultDir.getAbsolutePath());

            Result result = controller.execute();
            Assert.assertTrue(result.getMessage(), !(result instanceof Failed));
        } catch (Throwable ex) {
            throw new WindupException("Failed running Windup from the upgraded directory: " + ex.getMessage(), ex);
        }
    }

    @Test
    @Ignore("Completely broken now. New Furnace doesn't deal well with setting windup.home like this.")
    public void testUpdateDistributionCommand() throws Exception {

        // Unzip the rulesets from a .zip in resources.

        File tempDir = OperatingSystemUtils.createTempDir();
        tempDir.deleteOnExit();
        File extractedPath = new File(tempDir, "extracted-rulesets");
        ZipUtil.unzipFromClassResource(getClass(), WindupUpdateDistributionCommandTest.TEST_RULESET_ZIP, extractedPath);

        String windupDir = extractedPath + "/windup-old-ruleset";
        System.setProperty(PathUtil.WINDUP_RULESETS_DIR_SYSPROP, windupDir);
        File addonsDir = new File(windupDir, "addons");
        addonsDir.mkdirs();

        String currentUiVersion = getInstalledAddonVersion(addon.getRepository().getRootDirectory().getPath(), WINDUP_UI_ADDON_NAME);
        installOldAddonVersion(currentUiVersion); // changeUiAddonVersion(addon.getRepository().getRootDirectory(),
        // currentUiVersion);
        waitForOldWindupUIAddon(furnace);

        boolean rulesetNeedUpdate = updater.rulesetsNeedUpdate(true);
        Assert.assertTrue(rulesetNeedUpdate);
        try (CommandController controller = uiTestHarness.createCommandController("Windup Update Distribution")) {
            try {
                controller.initialize();
                Assert.assertTrue(controller.isEnabled());
                // Actually runs the command.
                Result result = controller.execute();
                Assert.assertFalse("Windup Update Distribution command should suceed, but it failed.", result instanceof Failed);
                rulesetNeedUpdate = updater.rulesetsNeedUpdate(true);
                Assert.assertFalse("Ruleset should have already been updated to the latest version and as such should not need another update.",
                        rulesetNeedUpdate);

                checkWindupDirectory(windupDir);
            } finally {
                FileUtils.deleteDirectory(tempDir);
            }
        }
    }

    private void checkWindupDirectory(String windupDir) {
        File addonsHomeNew = new File(windupDir, "addons");
        Assert.assertTrue("Addons folder was not updated sucessfully", addonsHomeNew.exists());
        Assert.assertTrue("Addons folder does not contain enough addons (at least 6)", addonsHomeNew.listFiles().length > 5);

        File binNew = new File(windupDir, "bin");
        Assert.assertTrue("Bin folder was not updated sucessfully", binNew.exists());
        Assert.assertTrue("Binary folder does not contain enough items (at least 2)", binNew.listFiles().length > 1);

        File libNew = new File(windupDir, "lib");
        Assert.assertTrue("Library folder was not updated sucessfully", libNew.exists());
        Assert.assertTrue("Library folder does not contain enough libraries (at least 8)", libNew.listFiles().length > 7);
    }

    private void waitForOldWindupUIAddon(Furnace furnace) throws InterruptedException {
        Addon addon = furnace.getAddonRegistry().getAddon(AddonId.from(WINDUP_UI_ADDON_NAME, WINDUP_OLD_VERSION));
        Addons.waitUntilStarted(addon);
        do {
            // We need to wait till furnace will process all the information changed in the directories.
            Thread.sleep(500);
        }
        while (furnace.getAddonRegistry().getServices(WindupUpdateDistributionCommand.class).isUnsatisfied());
    }

    /**
     * Uninstalls the current addon and installs the other one. This fails because we would need to replace all the
     * dependencies as well, effectively, the whole Windup.
     */
    private void installOldAddonVersion(String currentUiVersion) {
        RemoveRequest remove = manager.remove(AddonId.from(WINDUP_UI_ADDON_NAME, currentUiVersion));
        remove.perform();
        final AddonId olderAddonId = AddonId.from(WINDUP_UI_ADDON_NAME, WINDUP_OLD_VERSION);
        log.info("Downgrading to " + olderAddonId + ". This may take a while to download.");
        InstallRequest install = manager.install(olderAddonId);
        install.perform();
    }

    /**
     * Changes the org-jboss-windup-ui-windup-ui-* dir name to old version and rewrites the version in installed.xml. It
     * is a hack to fool Furnace into thinking a new addon was installed.
     */
    private void changeUiAddonVersion(File addonsDir, String currentUiVersion) {
        File currentAddonDir = new File(addonsDir,
                "org-jboss-windup-ui-windup-ui-" + currentUiVersion.replaceAll("\\.", "-"));
        File olderVersionAddonDir = new File(addonsDir,
                "org-jboss-windup-ui-windup-ui-" + WINDUP_OLD_VERSION.replaceAll("\\.", "-"));
        olderVersionAddonDir.mkdirs();

        log.warning("Replacing the addon: \n  " + currentAddonDir + System.lineSeparator() + olderVersionAddonDir);
        try {
            FileUtils.copyDirectory(currentAddonDir, olderVersionAddonDir);
            changeUiAddonVersionInInstallXml(addonsDir.getPath());
            FileUtils.deleteDirectory(currentAddonDir);
        } catch (IOException ex) {
            throw new RuntimeException("Failed replacing the addon: ", ex);
        }
    }

    /**
     * Reads the version of the windup-ui addon from addons/installed.xml .
     */
    private String getInstalledAddonVersion(String addonsRootDir, String addonName) {
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder;
        try {
            String oldUiAddonVersion = "";
            File installedXml = new File(addonsRootDir, "installed.xml");
            if (!installedXml.exists())
                throw new WindupException("installed.xml doesn't exist: " + installedXml.getAbsolutePath());

            dBuilder = dbFactory.newDocumentBuilder();
            Document doc = dBuilder.parse(installedXml);
            // this does not work properly
            Element documentElement = doc.getDocumentElement();
            NodeList childNodes = documentElement.getElementsByTagName("addon");
            for (int i = 0; i <= childNodes.getLength() - 1; i++) {
                Element item = (Element) childNodes.item(i);
                if (item.getNodeName().equals("addon")) {
                    String addonNameAttr = item.getAttribute("name");
                    if (addonNameAttr.equals(addonName)) {
                        oldUiAddonVersion = item.getAttribute("version");
                        return oldUiAddonVersion;
                    }
                }
            }
        } catch (ParserConfigurationException ex) {
            throw new RuntimeException("Failed parsing installed.xml: " + ex.getMessage(), ex);
        } catch (WindupException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new RuntimeException("Unknown exception: " + ex.getMessage(), ex);
        }
        return null;
    }

    /**
     * Writes the version WINDUP_OLD_VERSION into installed.xml in given dir.
     */
    private void changeUiAddonVersionInInstallXml(String homeAddonsDirPath) {
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder;
        try {
            dBuilder = dbFactory.newDocumentBuilder();
            File installedXml = new File(homeAddonsDirPath, "installed.xml");
            Document doc = dBuilder.parse(installedXml);
            // this does not work properly
            Element documentElement = doc.getDocumentElement();
            NodeList childNodes = documentElement.getElementsByTagName("addon");
            for (int i = 0; i <= childNodes.getLength() - 1; i++) {
                Element item = (Element) childNodes.item(i);
                if (item.getNodeName().equals("addon")) {
                    String addonName = item.getAttribute("name");
                    if (addonName.equals(WINDUP_UI_ADDON_NAME)) {
                        item.setAttribute("version", WINDUP_OLD_VERSION);
                        TransformerFactory transformerFactory = TransformerFactory.newInstance();
                        Transformer transformer = transformerFactory.newTransformer();
                        DOMSource source = new DOMSource(doc);
                        StreamResult result = new StreamResult(installedXml);
                        transformer.transform(source, result);
                        return;
                    }
                }
            }
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (Exception e) {
            System.out.println("Why?");
        }
        return;
    }

}