/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014, 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.patching.validation;

import static org.jboss.as.patching.HashUtils.hashFile;
import static org.jboss.as.patching.runner.TestUtils.dump;
import static org.jboss.as.patching.runner.TestUtils.randomString;
import static org.jboss.as.patching.runner.TestUtils.touch;
import static org.jboss.as.patching.validation.PatchHistoryValidations.validateRollbackState;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.jboss.as.patching.Constants;
import org.jboss.as.patching.IoUtils;
import org.jboss.as.patching.PatchingException;
import org.jboss.as.patching.tests.AbstractPatchingTest;
import org.jboss.as.patching.tests.PatchingTestBuilder;
import org.jboss.as.patching.tests.PatchingTestStepBuilder;
import org.junit.Assert;
import org.junit.Test;

/**
 * @author Alexey Loubyansky
 */
public class PatchingHistoryUnitTestCase extends AbstractPatchingTest {

    static final String[] FILE_ONE = {"bin", "standalone.sh"};
    static final String[] FILE_TWO = {"bin", "standalone.conf"};
    static final String[] FILE_EXISTING = {"bin", "test"};
    private static final String CP_1_ID = "cp1";
    private static final String ONE_OFF_1_ID = "oneOff1";
    private static final String ONE_OFF_2_ID = "oneOff2";

    @Test
    public void testTreeHandleAll() throws Exception {
        installOneOffCpOneOff();
        final List<String> historyDirs = new ArrayList<String>();
        final List<String> moduleDirs = new ArrayList<String>();
        final List<String> bundleDirs = new ArrayList<String>();

        final PatchHistoryIterator.Builder builder = PatchHistoryIterator.Builder.create(updateInstallationManager().getDefaultIdentity());
        builder.addStateHandler(PatchingArtifacts.HISTORY_DIR, new PatchingArtifactStateHandler<PatchingFileArtifact.DirectoryArtifactState>() {
            @Override
            public void handleValidatedState(PatchingFileArtifact.DirectoryArtifactState state) {
                historyDirs.add(state.getFile().getName());
            }
        });
        builder.addStateHandler(PatchingArtifacts.MODULE_OVERLAY, new PatchingArtifactStateHandler<PatchingFileArtifact.DirectoryArtifactState>() {
            @Override
            public void handleValidatedState(PatchingFileArtifact.DirectoryArtifactState state) {
                moduleDirs.add(state.getFile().getName());
            }
        });
        builder.addStateHandler(PatchingArtifacts.BUNDLE_OVERLAY, new PatchingArtifactStateHandler<PatchingFileArtifact.DirectoryArtifactState>() {
            @Override
            public void handleValidatedState(PatchingFileArtifact.DirectoryArtifactState state) {
                bundleDirs.add(state.getFile().getName());
            }
        });

        final PatchHistoryIterator iterator = builder.iterator();
        while (iterator.hasNext()) {
            iterator.next();
        }

        assertEquals(Arrays.asList(new String[]{"oneOff2", "cp1", "oneOff1"}), historyDirs);
        assertEquals(Arrays.asList(new String[]{"base-oneOff2", "base-cp1", "base-oneOff1"}), moduleDirs);
        assertTrue(bundleDirs.isEmpty());
    }


    @Test
    public void testTreeIterator() throws Exception {

        installOneOffCpOneOff();

        final List<String> historyDirs = new ArrayList<String>();
        final List<String> moduleDirs = new ArrayList<String>();
        final List<String> bundleDirs = new ArrayList<String>();
        final PatchHistoryIterator.Builder builder = PatchHistoryIterator.Builder.create(updateInstallationManager().getDefaultIdentity());
        builder.addStateHandler(PatchingArtifacts.HISTORY_DIR, new PatchingArtifactStateHandler<PatchingFileArtifact.DirectoryArtifactState>() {
            @Override
            public void handleValidatedState(PatchingFileArtifact.DirectoryArtifactState state) {
                historyDirs.add(state.getFile().getName());
            }
        });
        builder.addStateHandler(PatchingArtifacts.MODULE_OVERLAY, new PatchingArtifactStateHandler<PatchingFileArtifact.DirectoryArtifactState>() {
            @Override
            public void handleValidatedState(PatchingFileArtifact.DirectoryArtifactState state) {
                moduleDirs.add(state.getFile().getName());
            }
        });
        builder.addStateHandler(PatchingArtifacts.BUNDLE_OVERLAY, new PatchingArtifactStateHandler<PatchingFileArtifact.DirectoryArtifactState>() {
            @Override
            public void handleValidatedState(PatchingFileArtifact.DirectoryArtifactState state) {
                bundleDirs.add(state.getFile().getName());
            }
        });

        final PatchHistoryIterator tree = builder.iterator();

        assertTrue(tree.hasNext());
        assertTrue(historyDirs.isEmpty());
        assertTrue(moduleDirs.isEmpty());
        assertTrue(bundleDirs.isEmpty());

        assertTrue(tree.hasNext());
        tree.next();
        assertEquals(1, historyDirs.size());
        assertTrue(historyDirs.contains("oneOff2"));
        assertTrue(moduleDirs.contains("base-oneOff2"));
        assertTrue(bundleDirs.isEmpty());

        assertTrue(tree.hasNext());
        tree.next();
        assertEquals(2, historyDirs.size());
        assertTrue(historyDirs.contains("cp1"));
        assertEquals(2, moduleDirs.size());
        assertTrue(moduleDirs.contains("base-cp1"));
        assertTrue(bundleDirs.isEmpty());

        assertTrue(tree.hasNext());
        tree.next();
        assertEquals(3, historyDirs.size());
        assertTrue(historyDirs.contains("oneOff1"));
        assertTrue(moduleDirs.contains("base-oneOff1"));
        assertTrue(bundleDirs.isEmpty());

        assertFalse(tree.hasNext());
    }

    @Test
    public void testSimpleValidRollbackOneOff() throws Exception {
        installOneOff();
        validateRollbackState(ONE_OFF_1_ID, updateInstallationManager().getDefaultIdentity());
    }

    @Test
    public void testSimpleMissingHistory() throws Exception {
        final PatchingTestBuilder builder = installOneOff();
        // Delete complete history
        final File history = getHistory(builder, ONE_OFF_1_ID);
        IoUtils.recursiveDelete(history);
        cannotRollbackPatch(ONE_OFF_1_ID);
    }

    @Test
    public void testSimpleMissingMiscFiles() throws Exception {

        final PatchingTestBuilder builder = installOneOff();
        // Delete misc
        final File history = getHistory(builder, ONE_OFF_1_ID);
        final File misc = new File(history, Constants.MISC);
        IoUtils.recursiveDelete(misc);
        cannotRollbackPatch(ONE_OFF_1_ID);
    }

    @Test
    public void testSimpleMissingRollbackXml() throws Exception {
        final PatchingTestBuilder builder = installOneOff();
        // Delete misc
        final File history = getHistory(builder, ONE_OFF_1_ID);
        final File rollbackXml = new File(history, Constants.ROLLBACK_XML);
        rollbackXml.delete();
        cannotRollbackPatch(ONE_OFF_1_ID);
    }

    @Test
    public void testMissingHistoryOneOff() throws Exception {
        final PatchingTestBuilder builder = installOneOffCpOneOff();

        // Can rollback incl. the first one off
        validateRollbackState(ONE_OFF_1_ID, updateInstallationManager().getDefaultIdentity());

        // Remove one off history
        final File oneOffHistory = getHistory(builder, ONE_OFF_1_ID);
        IoUtils.recursiveDelete(oneOffHistory);
        cannotRollbackPatch(ONE_OFF_1_ID);

        // Can rollback CP1
        validateRollbackState(CP_1_ID, updateInstallationManager().getDefaultIdentity());

        // Remove cp1 history
        final File cpHistory = getHistory(builder, CP_1_ID);
        IoUtils.recursiveDelete(cpHistory);
        cannotRollbackPatch(CP_1_ID);

        // Could still rollback 2nd one off
        validateRollbackState(ONE_OFF_2_ID, updateInstallationManager().getDefaultIdentity());
    }

    @Test
    public void testMissingOverlay() throws Exception {

        final PatchingTestBuilder builder = installOneOffCpOneOff();

        // Can rollback incl. the first one off
        validateRollbackState(ONE_OFF_1_ID, updateInstallationManager().getDefaultIdentity());
        final File overlays = getOverlays(builder, "base", "base-" + ONE_OFF_1_ID);
        IoUtils.recursiveDelete(overlays);

        cannotRollbackPatch(ONE_OFF_1_ID);
        cannotRollbackPatch(CP_1_ID);

    }

    protected void cannotRollbackPatch(final String patchID) throws Exception {
        try {
            validateRollbackState(patchID, updateInstallationManager().getDefaultIdentity());
            Assert.fail("should not be able to rollback " + patchID);
        } catch (PatchingException e) {
            // ok
        } catch (Exception e) {
            throw e;
        }
    }

    protected PatchingTestBuilder installOneOffCpOneOff() throws IOException, PatchingException {
        final PatchingTestBuilder builder = createDefaultBuilder();
        final byte[] standaloneHash = new byte[20];
        final byte[] moduleHash = new byte[20];

        // Create a file
        final File existing = builder.getFile(FILE_EXISTING);
        touch(existing);
        dump(existing, randomString());

        final byte[] existingHash = hashFile(existing);
        final byte[] initialHash = Arrays.copyOf(existingHash, existingHash.length);

        final PatchingTestStepBuilder oneOff1 = builder.createStepBuilder();
        oneOff1.setPatchId(ONE_OFF_1_ID)
                .oneOffPatchIdentity(PRODUCT_VERSION)
                .oneOffPatchElement("base-" + ONE_OFF_1_ID, "base", false)
                .addModuleWithRandomContent("org.jboss.test", moduleHash)
                .getParent()
                .addFileWithRandomContent(standaloneHash, FILE_ONE)
                .updateFileWithRandomContent(Arrays.copyOf(existingHash, existingHash.length), existingHash, FILE_EXISTING);

        // Apply oneOff1
        apply(oneOff1);

        final PatchingTestStepBuilder cp1 = builder.createStepBuilder();
        cp1.setPatchId(CP_1_ID)
                .upgradeIdentity(PRODUCT_VERSION, PRODUCT_VERSION)
                .upgradeElement("base-" + CP_1_ID, "base", false)
                .updateModuleWithRandomContent("org.jboss.test", Arrays.copyOf(moduleHash, moduleHash.length), moduleHash)
                .getParent()
                .updateFileWithRandomContent(Arrays.copyOf(standaloneHash, standaloneHash.length), standaloneHash, FILE_ONE)
                .updateFileWithRandomContent(initialHash, existingHash, FILE_EXISTING);

        // Apply CP1
        apply(cp1);

        final PatchingTestStepBuilder oneOff2 = builder.createStepBuilder();
        oneOff2.setPatchId(ONE_OFF_2_ID)
                .oneOffPatchIdentity(PRODUCT_VERSION)
                .oneOffPatchElement("base-" + ONE_OFF_2_ID, "base", false)
                .updateModuleWithRandomContent("org.jboss.test", moduleHash, null)
                .getParent()
                .updateFileWithRandomContent(standaloneHash, null, FILE_ONE)
                .updateFileWithRandomContent(Arrays.copyOf(existingHash, existingHash.length), existingHash, FILE_EXISTING);

        // Apply oneOff1
        apply(oneOff2);
        return builder;
    }


    protected PatchingTestBuilder installOneOff() throws Exception {
        final PatchingTestBuilder builder = createDefaultBuilder();
        final byte[] standaloneHash = new byte[20];
        final byte[] moduleHash = new byte[20];

        // Create a file
        final File existing = builder.getFile(FILE_EXISTING);
        touch(existing);
        dump(existing, randomString());

        final byte[] existingHash = hashFile(existing);
        final byte[] initialHash = Arrays.copyOf(existingHash, existingHash.length);

        final PatchingTestStepBuilder oneOff1 = builder.createStepBuilder();
        oneOff1.setPatchId(ONE_OFF_1_ID)
                .oneOffPatchIdentity(PRODUCT_VERSION)
                .oneOffPatchElement("base-" + ONE_OFF_1_ID, "base", false)
                .addModuleWithRandomContent("org.jboss.test", moduleHash)
                .getParent()
                .addFileWithRandomContent(standaloneHash, FILE_ONE)
                .updateFileWithRandomContent(Arrays.copyOf(existingHash, existingHash.length), existingHash, FILE_EXISTING);

        // Apply oneOff1
        apply(oneOff1);

        return builder;
    }

    static File getHistory(final PatchingTestBuilder builder, final String patchID) {
        return builder.getFile(Constants.INSTALLATION, Constants.PATCHES, patchID);
    }

    static File getOverlays(final PatchingTestBuilder builder, final String layerName, final String patchID) {
        return builder.getFile(Constants.MODULES, Constants.SYSTEM, Constants.LAYERS, layerName, Constants.OVERLAYS, patchID);
    }

}
