/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2016, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
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.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 org.jboss.as.patching.Constants;
import org.jboss.as.patching.HashUtils;
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.patching.metadata.PatchMerger;
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, updated for WildFly 10 by Jan Martiska
 */
@RunWith(WildflyTestRunner.class)
@ServerControl(manual = true)
public class MergedPatchesTestCase extends AbstractPatchingTestCase {

    /**
     * creates 4 CPs
     * creates bundle containing 4 CPs
     * applies bundle
     * rolls back back to base version
     *
     * @throws Exception
     */
    @Test
    public void testApplyMergedPatch() throws Exception {
        File patchTmpDir = mkdir(tempDir, randomString("patchdir"));

        final String cpPatchID = randomString("cp1");
        final String cpPatchID2 = randomString("cp2");
        final String cpPatchID3 = randomString("cp3");
        final String cpPatchID4 = randomString("cp4");
        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";

        File cpZip = createCP1(cpPatchID, AS_VERSION, eapWithCP, patchTmpDir);
        File cpZip2 = createCP2(cpPatchID2, eapWithCP, cpPatchID, eapWithCP2, patchTmpDir);
        File cpZip3 = createCP3(cpPatchID3, eapWithCP2, cpPatchID2, eapWithCP3, patchTmpDir);
        File cpZip4 = createCP2(cpPatchID4, eapWithCP3, cpPatchID3, eapWithCP4, patchTmpDir);

        final File mergedCp1Cp2 = PatchMerger.merge(cpZip, cpZip2, new File("merged-cp1-cp2.zip"));
        mergedCp1Cp2.deleteOnExit();
        final File mergedCp1Cp2Cp3 = PatchMerger
                .merge(mergedCp1Cp2, cpZip3, new File("merged-cp1-cp2-cp3.zip"));
        mergedCp1Cp2Cp3.deleteOnExit();
        final File mergedCp1Cp2Cp3Cp4 = PatchMerger
                .merge(mergedCp1Cp2Cp3, cpZip4, new File("merged-cp1-cp2-cp3-cp4.zip"));
        mergedCp1Cp2Cp3Cp4.deleteOnExit();

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

        // verify installed cp version
        controller.start();
        Assert.assertTrue("The patch " + cpPatchID4 + " should be listed as installed",
                CliUtilsForPatching.getCumulativePatchId().equalsIgnoreCase(cpPatchID4));
        Assert.assertTrue("Module " + cp1AddedModuleName + " should exist", CliUtilsForPatching.doesModuleExist(cp1AddedModuleName));


        // rollback to base
        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));
        Assert.assertFalse("Module " + cp1AddedModuleName + " should not exist", CliUtilsForPatching.doesModuleExist(cp1AddedModuleName));
        controller.stop();
    }

    /**
     * creates 4 CPs
     * creates bundle containing 4 CPs (merging them together)
     * applies CP1
     * applies CP2
     * applies one-off
     * applies bundle
     * rolls back to CP2, verifies the one-off is still there
     * rolls back to CP1
     * rolls back to base
     *
     * @throws Exception
     */
    @Test
    public void testApplyBundleSkipTwoCPs() throws Exception {
        File patchTmpDir = mkdir(tempDir, randomString("patchdir"));

        final String cpPatchID = randomString("cp1");
        final String cpPatchID2 = randomString("cp2");
        final String cpPatchID3 = randomString("cp3");
        final String cpPatchID4 = randomString("cp4");
        final String oneOffId = randomString("oneoff");
        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";


        File cpZip = createCP1(cpPatchID, AS_VERSION, eapWithCP, patchTmpDir);
        File cpZip2 = createCP2(cpPatchID2, eapWithCP, cpPatchID, eapWithCP2, patchTmpDir);
        File cpZip3 = createCP3(cpPatchID3, eapWithCP2, cpPatchID2, eapWithCP3, patchTmpDir);
        File cpZip4 = createCP2(cpPatchID4, eapWithCP3, cpPatchID3, eapWithCP4, patchTmpDir);
        File oneOffZip = createOneOffPatchAddingMiscFile(oneOffId, eapWithCP2);

        final File mergedCp1Cp2 = PatchMerger.merge(cpZip, cpZip2, new File("merged-cp1-cp2.zip"));
        mergedCp1Cp2.deleteOnExit();
        final File mergedCp1Cp2Cp3 = PatchMerger
                .merge(mergedCp1Cp2, cpZip3, new File("merged-cp1-cp2-cp3.zip"));
        mergedCp1Cp2Cp3.deleteOnExit();
        final File mergedCp1Cp2Cp3Cp4 = PatchMerger
                .merge(mergedCp1Cp2Cp3, cpZip4, new File("merged-cp1-cp2-cp3-cp4.zip"));
        mergedCp1Cp2Cp3Cp4.deleteOnExit();

        // 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 cp1 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 cp2 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 cp2 + oneoff and apply bundle to cp4
        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(mergedCp1Cp2Cp3Cp4.getAbsolutePath()));
        Assert.assertTrue("server should be in restart-required mode",
                CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

        // verify cp4 and rollback cp4 back to cp2
        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 cp2 + oneoff and roll back the oneoff
        controller.start();
        Assert.assertEquals("The patch " + cpPatchID2 + " should be listed as installed",
                cpPatchID2, CliUtilsForPatching.getCumulativePatchId());
        Assert.assertTrue("The patch " + oneOffId + " should be listed as installed",
                CliUtilsForPatching.getInstalledPatches().contains(oneOffId));
        Assert.assertTrue("Rollback should be accepted", CliUtilsForPatching.rollbackPatch(oneOffId));
        controller.stop();

        // verify oneoff is not installed, rollback to cp1
        controller.start();
        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 cp1 and roll back to base
        controller.start();
        Assert.assertEquals("The patch " + cpPatchID + " should be listed as installed",
                cpPatchID, CliUtilsForPatching.getCumulativePatchId());
        Assert.assertTrue("Rollback should be accepted", CliUtilsForPatching.rollbackCumulativePatch(true));
        Assert.assertTrue("server should be in restart-required mode",
                CliUtilsForPatching.doesServerRequireRestart());
        controller.stop();

    }

    /*    -------------- THINGS RELATED TO CP1 ------------- */

    private String cp1AddedModuleName;
    private String cp1LayerPatchID;
    private ResourceItem cp1ResourceItem1;
    private ResourceItem cp1ResourceItem2;
    private String cp1Slot;

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

        cp1AddedModuleName = "org.wildfly.test." + randomString("cp1");

        cp1ResourceItem1 = new ResourceItem("testFile1", "content1".getBytes(StandardCharsets.UTF_8));
        cp1ResourceItem2 = new ResourceItem("testFile2", "content2".getBytes(StandardCharsets.UTF_8));

        Module newModule = new Module.Builder(cp1AddedModuleName)
                .miscFile(cp1ResourceItem1)
                .miscFile(cp1ResourceItem2)
                .build();

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

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

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

    private File createCP2(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("cp2");

        // 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 createCP3(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);
    }

}
