package org.jboss.as.test.patching;

import static org.jboss.as.patching.IoUtils.mkdir;
import static org.jboss.as.patching.IoUtils.newFile;
import static org.jboss.as.test.patching.PatchingTestUtil.AS_DISTRIBUTION;
import static org.jboss.as.test.patching.PatchingTestUtil.AS_VERSION;
import static org.jboss.as.test.patching.PatchingTestUtil.FILE_SEPARATOR;
import static org.jboss.as.test.patching.PatchingTestUtil.MODULES_PATH;
import static org.jboss.as.test.patching.PatchingTestUtil.PRODUCT;
import static org.jboss.as.test.patching.PatchingTestUtil.createPatchBundleXMLFile;
import static org.jboss.as.test.patching.PatchingTestUtil.createPatchXMLFile;
import static org.jboss.as.test.patching.PatchingTestUtil.createZippedPatchFile;
import static org.jboss.as.test.patching.PatchingTestUtil.randomString;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import org.jboss.as.patching.Constants;
import org.jboss.as.patching.HashUtils;
import org.jboss.as.patching.metadata.BundledPatch;
import org.jboss.as.patching.metadata.ContentModification;
import org.jboss.as.patching.metadata.Patch;
import org.jboss.as.patching.metadata.PatchBuilder;
import org.jboss.as.test.patching.util.module.Module;
import org.jboss.as.version.ProductConfig;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.wildfly.core.testrunner.ServerControl;
import org.wildfly.core.testrunner.WildflyTestRunner;

/**
 * @author Martin Simka
 */
@RunWith(WildflyTestRunner.class)
@ServerControl(manual = true)
public class PatchBundleTestCase extends AbstractPatchingTestCase {


    /**
     * creates 4 CPs
     * creates bundle containing 4 CPs
     * applies bundle
     * rollbacks CP one by one
     *
     * @throws Exception
     */
    @Test
    public void testApplyBundle() throws Exception {
        final String bundleName = "bundle_" + randomString();
        File patchBundleDir = mkdir(tempDir, bundleName);

        final String cpPatchID = randomString();
        final String cpPatchID2 = randomString();
        final String cpPatchID3 = randomString();
        final String cpPatchID4 = randomString();
        final String eapWithCP = "EAP with cp patch";
        final String eapWithCP2 = "EAP with cp patch 2";
        final String eapWithCP3 = "EAP with cp patch 3";
        final String eapWithCP4 = "EAP with cp patch 4";

        List<BundledPatch.BundledPatchEntry> patches = new ArrayList<BundledPatch.BundledPatchEntry>();

        File cpZip = createCumulativePatchAddingARandomModule(cpPatchID, AS_VERSION, eapWithCP, patchBundleDir);
        patches.add(new BundledPatch.BundledPatchEntry(cpPatchID, cpZip.getName()));
        File cpZip2 = createNextCumulativePatchAddingRandomModule(cpPatchID2, eapWithCP, cpPatchID, eapWithCP2, patchBundleDir);
        patches.add(new BundledPatch.BundledPatchEntry(cpPatchID2, cpZip2.getName()));
        File cpZip3 = createNextCumulativePatchModyfyingJbossModules(cpPatchID3, eapWithCP2, cpPatchID2, eapWithCP3, patchBundleDir);
        patches.add(new BundledPatch.BundledPatchEntry(cpPatchID3, cpZip3.getName()));
        File cpZip4 = createNextCumulativePatchAddingRandomModule(cpPatchID4, eapWithCP3, cpPatchID3, eapWithCP4, patchBundleDir);
        patches.add(new BundledPatch.BundledPatchEntry(cpPatchID4, cpZip4.getName()));

        createPatchBundleXMLFile(patchBundleDir, patches);
        File patchBundleZip = createZippedPatchFile(patchBundleDir, bundleName);

        // apply bundle
        controller.start();
        Assert.assertTrue("Patch should be accepted", CliUtilsForPatching.applyPatch(patchBundleZip.getAbsolutePath()));
        Assert.assertTrue("server should be in restart-required mode", CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp version and rollback cp4
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID4 + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID4));
        Assert.assertTrue("Rollback should be accepted", CliUtilsForPatching.rollbackCumulativePatch(true));
        Assert.assertTrue("server should be in restart-required mode",
                CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp version and rollback cp4
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID3 + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID3));
        Assert.assertTrue("Rollback should be accepted", CliUtilsForPatching.rollbackCumulativePatch(true));
        Assert.assertTrue("server should be in restart-required mode",
                CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp version and rollback cp2
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID2 + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID2));
        Assert.assertTrue("Rollback should be accepted", CliUtilsForPatching.rollbackCumulativePatch(true));
        Assert.assertTrue("server should be in restart-required mode",
                CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp version and rollback cp
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID));
        Assert.assertTrue("Rollback should be accepted", CliUtilsForPatching.rollbackCumulativePatch(true));
        Assert.assertTrue("server should be in restart-required mode",
                CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify base
        controller.start();
        Assert.assertTrue("The patch " + Constants.BASE + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(Constants.BASE));
        controller.stop();
    }

    /**
     * creates 4 CPs
     * creates bundle containing 4 CPs
     * applies CP1
     * applies CP2
     * applies one-off
     * applies bundle
     * rollbacks CPs and one-off one by one
     *
     * @throws Exception
     */
    @Test
    public void testApplyBundleSkipTwoCPs() throws Exception {
        final String bundleName = "bundle_" + randomString();
        File patchBundleDir = mkdir(tempDir, bundleName);

        final String cpPatchID = randomString();
        final String cpPatchID2 = randomString();
        final String cpPatchID3 = randomString();
        final String cpPatchID4 = randomString();
        final String oneOffId = randomString();
        final String eapWithCP = "EAP with cp patch";
        final String eapWithCP2 = "EAP with cp patch 2";
        final String eapWithCP3 = "EAP with cp patch 3";
        final String eapWithCP4 = "EAP with cp patch 4";

        List<BundledPatch.BundledPatchEntry> patches = new ArrayList<BundledPatch.BundledPatchEntry>();

        File cpZip = createCumulativePatchAddingARandomModule(cpPatchID, AS_VERSION, eapWithCP, patchBundleDir);
        patches.add(new BundledPatch.BundledPatchEntry(cpPatchID, cpZip.getName()));
        File cpZip2 = createNextCumulativePatchAddingRandomModule(cpPatchID2, eapWithCP, cpPatchID, eapWithCP2, patchBundleDir);
        patches.add(new BundledPatch.BundledPatchEntry(cpPatchID2, cpZip2.getName()));
        File cpZip3 = createNextCumulativePatchModyfyingJbossModules(cpPatchID3, eapWithCP2, cpPatchID2, eapWithCP3, patchBundleDir);
        patches.add(new BundledPatch.BundledPatchEntry(cpPatchID3, cpZip3.getName()));
        File cpZip4 = createNextCumulativePatchAddingRandomModule(cpPatchID4, eapWithCP3, cpPatchID3, eapWithCP4, patchBundleDir);
        patches.add(new BundledPatch.BundledPatchEntry(cpPatchID4, cpZip4.getName()));
        File oneOffZip = createOneOffPatchAddingMiscFile(oneOffId, eapWithCP2);

        createPatchBundleXMLFile(patchBundleDir, patches);
        File patchBundleZip = createZippedPatchFile(patchBundleDir, bundleName);

        // apply cp1
        controller.start();
        Assert.assertTrue("Patch should be accepted", CliUtilsForPatching.applyPatch(cpZip.getAbsolutePath()));
        Assert.assertTrue("server should be in restart-required mode", CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp and apply cp2
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID));
        Assert.assertTrue("Patch should be accepted", CliUtilsForPatching.applyPatch(cpZip2.getAbsolutePath()));
        Assert.assertTrue("server should be in restart-required mode", CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp and apply oneoff
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID2 + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID2));
        Assert.assertTrue("Patch should be accepted", CliUtilsForPatching.applyPatch(oneOffZip.getAbsolutePath()));
        Assert.assertTrue("server should be in restart-required mode", CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp and oneoff and apply bundle
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID2 + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID2));
        Assert.assertTrue("The patch " + oneOffId + " should be listed as installed",
                CliUtilsForPatching.getInstalledPatches().contains(oneOffId));
        Assert.assertTrue("Patch should be accepted", CliUtilsForPatching.applyPatch(patchBundleZip.getAbsolutePath()));
        Assert.assertTrue("server should be in restart-required mode", CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp and rollback cp4
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID4 + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID4));
        Assert.assertTrue("Rollback should be accepted", CliUtilsForPatching.rollbackCumulativePatch(true));
        Assert.assertTrue("server should be in restart-required mode",
                CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp and rollback cp3
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID3 + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID3));
        Assert.assertTrue("Rollback should be accepted", CliUtilsForPatching.rollbackCumulativePatch(true));
        Assert.assertTrue("server should be in restart-required mode",
                CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp and rollback oneoff
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID2 + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID2));
        Assert.assertTrue("The patch " + oneOffId + " should be listed as installed",
                CliUtilsForPatching.getInstalledPatches().contains(oneOffId));
        Assert.assertTrue("Rollback should be accepted", CliUtilsForPatching.rollbackPatch(oneOffId));
        Assert.assertTrue("server should be in restart-required mode",
                CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp and rollback cp2
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID2 + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID2));
        Assert.assertFalse("The patch " + oneOffId + " should not be listed as installed",
                CliUtilsForPatching.getInstalledPatches().contains(oneOffId));
        Assert.assertTrue("Rollback should be accepted", CliUtilsForPatching.rollbackCumulativePatch(true));
        Assert.assertTrue("server should be in restart-required mode",
                CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp and rollback cp
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID));
        Assert.assertTrue("Rollback should be accepted", CliUtilsForPatching.rollbackCumulativePatch(true));
        Assert.assertTrue("server should be in restart-required mode",
                CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify base
        controller.start();
        Assert.assertTrue("The patch " + Constants.BASE + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(Constants.BASE));
        controller.stop();
    }


    private File createCumulativePatchAddingARandomModule(String patchID, String asVersion, final String targetAsVersion, File targetDir) throws Exception {
        String layerPatchID = "layer" + patchID;
        File cpPatchDir = mkdir(tempDir, patchID);

        final String moduleName = "org.wildfly.test." + randomString();

        final ResourceItem resourceItem1 = new ResourceItem("testFile1", "content1".getBytes(StandardCharsets.UTF_8));
        final ResourceItem resourceItem2 = new ResourceItem("testFile2", "content2".getBytes(StandardCharsets.UTF_8));

        Module newModule = new Module.Builder(moduleName)
                .miscFile(resourceItem1)
                .miscFile(resourceItem2)
                .build();

        // Create the version module
        final String versionModuleName = ProductInfo.getVersionModule();
        final String slot = ProductInfo.getVersionModuleSlot();
        final String originalVersionModulePath = MODULES_PATH + FILE_SEPARATOR + versionModuleName.replace(".", FILE_SEPARATOR) + FILE_SEPARATOR + slot;
        final Module modifiedModule = PatchingTestUtil.createVersionModule(targetAsVersion);

        ContentModification moduleAdded = ContentModificationUtils.addModule(cpPatchDir, layerPatchID, newModule);
        ContentModification versionModuleModified = ContentModificationUtils.modifyModule(cpPatchDir, layerPatchID, HashUtils.hashFile(new File(originalVersionModulePath)), modifiedModule);

        Patch cpPatch = PatchBuilder.create()
                .setPatchId(patchID)
                .setDescription("A cp patch.")
                .upgradeIdentity(PRODUCT, asVersion, targetAsVersion)
                .getParent()
                .upgradeElement(layerPatchID, "base", false)
                .addContentModification(moduleAdded)
                .addContentModification(versionModuleModified)
                .getParent()
                .build();
        createPatchXMLFile(cpPatchDir, cpPatch);
        return createZippedPatchFile(cpPatchDir, patchID, targetDir);
    }

    private File createNextCumulativePatchAddingRandomModule(String patchID, String asVersion, final String currentPatch, final String targetAsVersion, File targetDir) throws Exception {
        String layerPatchID = "layer" + patchID;
        File cpPatchDir = mkdir(tempDir, patchID);
        final String moduleName = "org.wildfly.test." + randomString();

        // Create the version module
        final String versionModuleName = ProductInfo.getVersionModule();
        final Module modifiedModule = PatchingTestUtil.createVersionModule(targetAsVersion);

        // Calculate the target hash of the currently active module
        final String currentLayerPatchID = "layer" + currentPatch;
        File originalVersionModulePath = new File(tempDir, currentPatch);
        originalVersionModulePath = new File(originalVersionModulePath, currentLayerPatchID);
        originalVersionModulePath = new File(originalVersionModulePath, Constants.MODULES);
        originalVersionModulePath = newFile(originalVersionModulePath, versionModuleName.split("\\."));
        originalVersionModulePath = new File(originalVersionModulePath, ProductInfo.getVersionModuleSlot());
        byte[] patchedAsVersionHash = HashUtils.hashFile(originalVersionModulePath);
        assert patchedAsVersionHash != null;

        final ResourceItem resourceItem1 = new ResourceItem("testFile1", "content1".getBytes(StandardCharsets.UTF_8));
        final ResourceItem resourceItem2 = new ResourceItem("testFile2", "content2".getBytes(StandardCharsets.UTF_8));

        Module newModule = new Module.Builder(moduleName)
                .miscFile(resourceItem1)
                .miscFile(resourceItem2)
                .build();

        ContentModification moduleAdded = ContentModificationUtils.addModule(cpPatchDir, layerPatchID, newModule);
        ContentModification versionModuleModified = ContentModificationUtils.modifyModule(cpPatchDir, layerPatchID, patchedAsVersionHash, modifiedModule);

        ProductConfig productConfig = new ProductConfig(PRODUCT, asVersion, "main");
        Patch cpPatch = PatchBuilder.create()
                .setPatchId(patchID)
                .setDescription("A cp patch.")
                .upgradeIdentity(productConfig.getProductName(), productConfig.getProductVersion(), targetAsVersion)
                .getParent()
                .upgradeElement(layerPatchID, "base", false)
                .addContentModification(versionModuleModified)
                .addContentModification(moduleAdded)
                .getParent()
                .build();
        createPatchXMLFile(cpPatchDir, cpPatch);
        return createZippedPatchFile(cpPatchDir, patchID, targetDir);
    }

    private File createNextCumulativePatchModyfyingJbossModules(String patchID, String asVersion, final String currentPatch, final String targetAsVersion, File targetDir) throws Exception {
        String layerPatchID = "layer" + patchID;
        File cpPatchDir = mkdir(tempDir, patchID);

        // Also see if we can update jboss-modules
        final File installation = new File(AS_DISTRIBUTION);
        final File patchDir = new File(cpPatchDir, patchID);
        final ContentModification jbossModulesModification = PatchingTestUtil.updateModulesJar(installation, patchDir);

        // Create the version module
        final String versionModuleName = ProductInfo.getVersionModule();
        final Module modifiedModule = PatchingTestUtil.createVersionModule(targetAsVersion);

        // Calculate the target hash of the currently active module
        final String currentLayerPatchID = "layer" + currentPatch;
        File originalVersionModulePath = new File(tempDir, currentPatch);
        originalVersionModulePath = new File(originalVersionModulePath, currentLayerPatchID);
        originalVersionModulePath = new File(originalVersionModulePath, Constants.MODULES);
        originalVersionModulePath = newFile(originalVersionModulePath, versionModuleName.split("\\."));
        originalVersionModulePath = new File(originalVersionModulePath, ProductInfo.getVersionModuleSlot());
        byte[] patchedAsVersionHash = HashUtils.hashFile(originalVersionModulePath);
        assert patchedAsVersionHash != null;

        ContentModification versionModuleModified = ContentModificationUtils.modifyModule(cpPatchDir, layerPatchID, patchedAsVersionHash, modifiedModule);

        Patch cpPatch = PatchBuilder.create()
                .setPatchId(patchID)
                .setDescription("A cp patch.")
                .upgradeIdentity(PRODUCT, asVersion, targetAsVersion)
                .getParent()
                .upgradeElement(layerPatchID, "base", false)
                .addContentModification(versionModuleModified)
                .getParent()
                .addContentModification(jbossModulesModification)
                .build();
        createPatchXMLFile(cpPatchDir, cpPatch);
        return createZippedPatchFile(cpPatchDir, patchID, targetDir);
    }

    private File createOneOffPatchAddingMiscFile(String patchID, String asVersion) throws Exception {
        File oneOffPatchDir = mkdir(tempDir, patchID);
        ContentModification miscFileAdded = ContentModificationUtils.addMisc(oneOffPatchDir, patchID,
                "test content", "awesomeDirectory", "awesomeFile");
        ProductConfig productConfig = new ProductConfig(PRODUCT, asVersion, "main");
        Patch oneOffPatch = PatchBuilder.create()
                .setPatchId(patchID)
                .setDescription("A one-off patch adding a misc file.")
                .oneOffPatchIdentity(productConfig.getProductName(), productConfig.getProductVersion())
                .getParent()
                .addContentModification(miscFileAdded)
                .build();
        createPatchXMLFile(oneOffPatchDir, oneOffPatch);
        return createZippedPatchFile(oneOffPatchDir, patchID);
    }

}
