/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.activemq.artemis.tests.smoke.upgradeTest;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Stream;

import org.apache.activemq.artemis.cli.commands.ActionContext;
import org.apache.activemq.artemis.cli.commands.Create;
import org.apache.activemq.artemis.cli.commands.Upgrade;
import org.apache.activemq.artemis.utils.FileUtil;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This test will compare expected values at the upgraded servers.
 */
public class CompareUpgradeTest {

   private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

   public static final String basedir = System.getProperty("basedir");


   @Test
   public void testCompareWindowsFreshInstance() throws Exception {
      String windows = basedir + "/target/classes/servers/windowsUpgrade";
      String windowsBin = windows + "/bin";
      String windowsETC = basedir + "/target/classes/servers/windowsUpgradeETC";

      String windowsExpected = basedir + "/target/classes/servers/windowsUpgradeExpected";
      String windowsExpectedBin = windowsExpected + "/bin";
      String windowsExpectedETC = basedir + "/target/classes/servers/windowsUpgradeETCExpected";

      compareDirectories(true, windowsExpectedBin, windowsBin);
      compareDirectories(true, windowsExpectedETC, windowsETC, "broker.xml", "artemis-users.properties", "management.xml");

      String referenceBin = basedir + "/target/reference-for-backup-check/servers/windowsUpgrade/bin";
      String referenceEtc = basedir + "/target/reference-for-backup-check/servers/windowsUpgradeETC";

      verifyBackupFiles(windows + "/old-config-bkp.0/bin", referenceBin, Create.ARTEMIS_CMD, Create.ARTEMIS_SERVICE_EXE, Create.ARTEMIS_SERVICE_EXE_CONFIG, Create.ARTEMIS_SERVICE_XML);
      verifyBackupFiles(windows + "/old-config-bkp.0/etc", referenceEtc, Create.ETC_ARTEMIS_PROFILE_CMD, Create.ETC_BOOTSTRAP_XML, Upgrade.OLD_LOGGING_PROPERTIES);
   }

   @Test
   public void testCompareLinuxFreshInstance() throws Exception {
      String linux = basedir + "/target/classes/servers/linuxUpgrade";
      String linuxBin = linux + "/bin";
      String linuxETC = basedir + "/target/classes/servers/linuxUpgradeETC";

      String linuxExpected = basedir + "/target/classes/servers/linuxUpgradeExpected";
      String linuxExpectedBin = linuxExpected + "/bin";
      String linuxExpectedETC = basedir + "/target/classes/servers/linuxUpgradeETCExpected";

      compareDirectories(true, linuxExpectedBin, linuxBin);
      compareDirectories(true, linuxExpectedETC, linuxETC, "broker.xml", "artemis-users.properties", "management.xml");

      String referenceBin = basedir + "/target/reference-for-backup-check/servers/linuxUpgrade/bin";
      String referenceEtc = basedir + "/target/reference-for-backup-check/servers/linuxUpgradeETC";

      verifyBackupFiles(linux + "/old-config-bkp.0/bin", referenceBin, Create.ARTEMIS, Create.ARTEMIS_SERVICE);
      verifyBackupFiles(linux + "/old-config-bkp.0/etc", referenceEtc, Create.ETC_ARTEMIS_PROFILE, Create.ETC_BOOTSTRAP_XML, Upgrade.OLD_LOGGING_PROPERTIES);
   }

   private void verifyBackupFiles(String backupFolder, String referenceFolder, String... files) throws Exception {
      assertTrue(files.length > 0, "Files to check must be specified");

      File bck = new File(backupFolder);
      if (!(bck.exists() && bck.isDirectory())) {
         fail("Backup folder does not exist at: " + bck.getAbsolutePath());
      }

      File[] backupFiles = bck.listFiles();
      assertNotNull(backupFiles, "Some backup files must exist");
      int backupFilesCount = backupFiles.length;
      assertTrue(backupFilesCount > 0, "Some backup files must exist");
      assertEquals(files.length, backupFilesCount, "Different number of backup files found than specified for inspection, update test if backup procedure changed");

      for (String f : files) {
         File bf = new File(backupFolder, f);
         if (!bf.exists()) {
            fail("Expected backup file does not exist at: " + bf.getAbsolutePath());
         }

         File reference = new File(referenceFolder, bf.getName());
         if (!reference.exists()) {
            fail("Reference file does not exist at: " + reference.getAbsolutePath());
         }

         assertArrayEquals(Files.readAllBytes(bf.toPath()), Files.readAllBytes(reference.toPath()), bf.getName() + " backup contents do not match reference file");
      }
   }

   private void compareDirectories(boolean allowExpectedWord, String expectedFolder, String upgradeFolder, String... ignoredFiles) throws Exception {
      File expectedFolderFile = new File(expectedFolder);
      File[] foundFiles = expectedFolderFile.listFiles(pathname -> {
         for (String i :ignoredFiles) {
            if (pathname.getName().contains(i)) {
               return false;
            }
         }
         return true;
      });


      File upgradeFolderFile = new File(upgradeFolder);

      for (File f : foundFiles) {
         File upgradeFile = new File(upgradeFolderFile, f.getName());
         if (!upgradeFile.exists()) {
            fail(upgradeFile.getAbsolutePath() + " not does exist");
         }


         if (f.getName().endsWith(".exe")) {
            assertArrayEquals(Files.readAllBytes(f.toPath()), Files.readAllBytes(upgradeFile.toPath()), f.getName() + " is different after upgrade");
         } else {
            compareFiles(allowExpectedWord, f, upgradeFile);
         }
      }
   }

   private static void compareFiles(boolean allowExpectedWord, File expectedFile, File upgradeFile) throws IOException {
      logger.debug("comparing: {} to {}", upgradeFile, upgradeFile);
      try (Stream<String> expectedStream = Files.lines(expectedFile.toPath());
           Stream<String> upgradeStream = Files.lines(upgradeFile.toPath())) {

         Iterator<String> expectedIterator = expectedStream.iterator();
         Iterator<String> upgradeIterator = upgradeStream.iterator();

         int line = 1;

         while (expectedIterator.hasNext()) {
            assertTrue(upgradeIterator.hasNext());

            String expectedString = expectedIterator.next().trim();
            String upgradeString = upgradeIterator.next().trim();

            // there's a test in this class that will use a different name ID. on that case we replace Expected by ""
            // on the comparison
            if (allowExpectedWord) {
               expectedString = expectedString.replace("Expected", "");
            }

            assertEquals(expectedString, upgradeString, "error on line " + line + " at " + upgradeFile);
            line++;
         }

         assertFalse(upgradeIterator.hasNext());
      }
   }

   @Test
   public void testWindows() throws Exception {
      final String windows = basedir + "/target/classes/servers/windowsUpgrade";
      final String windowsBin = windows + "/bin";
      final String windowsETC = basedir + "/target/classes/servers/windowsUpgradeETC";
      final String windowsData = windows + "/data-custom";
      final String hawtioRoles = "amq";

      checkExpectedValues(windowsBin + "/artemis.cmd", "set ARTEMIS_INSTANCE_ETC=", "\"" + windowsETC + "\"");
      Map<String, String> result = checkExpectedValues(windowsBin + "/artemis-service.xml",
                                                       "<env name=\"ARTEMIS_HOME\" value=", null, // no expected value for this, we will check on the output
                                                       "<env name=\"ARTEMIS_INSTANCE\" value=", "\"" + windows  + "\"/>",
                                                       "<env name=\"ARTEMIS_INSTANCE_ETC\" value=", "\"" + windowsETC + "\"/>",
                                                       "<env name=\"ARTEMIS_DATA_DIR\" value=", "\"" + windowsData + "\"/>",
                                                       "<startargument>-Dhawtio.roles=", hawtioRoles + "</startargument>"
      );

      String home = result.get("<env name=\"ARTEMIS_HOME\" value=");
      assertNotNull(home);
      assertFalse(home.contains("must-change"), "home value must be changed during upgrade");

      Map<String, String> brokerProfileResult = checkExpectedValues(windowsETC + "/artemis.profile.cmd",
                                   "set ARTEMIS_HOME=", null, // no expected value for this, we will check on the output
                                   "set ARTEMIS_INSTANCE=", "\"" + windows + "\"",
                                   "set ARTEMIS_DATA_DIR=", "\"" + windowsData + "\"",
                                   "set ARTEMIS_OOME_DUMP=", "\"" + windows + "/log/oom_dump.hprof\"");

      home = brokerProfileResult.get("set ARTEMIS_HOME=");
      assertNotNull(home);
      assertFalse(home.contains("must-change"), "home value must be changed during upgrade");

      checkExpectedValues(windowsETC + "/bootstrap.xml",
                          "<server configuration=", "\"file:" + windowsETC + "//broker.xml\"/>");

      File oldLogging = new File(windowsETC + "/logging.properties");
      File newLogging = new File(windowsETC + "/log4j2.properties");

      assertFalse(oldLogging.exists(), "Old logging must be removed by upgrade");
      assertTrue(newLogging.exists(), "New Logging must be installed by upgrade");

      final String utilityProfilePath = windowsETC + "/artemis-utility.profile.cmd";
      final File utilityProfile = new File(utilityProfilePath);
      assertTrue(utilityProfile.exists(), "New utility profile must exist after upgrade");

      Map<String, String> utilityProfileResult = checkExpectedValues(utilityProfilePath,
            "set ARTEMIS_HOME=", null, // no expected value for this, we will check on the output
            "set ARTEMIS_INSTANCE=", "\"" + windows + "\"",
            "set ARTEMIS_DATA_DIR=", "\"" + windowsData + "\"");

      assertEquals(3, utilityProfileResult.size(), "Unexpected number of results");

      utilityProfileResult.forEach((key, value) -> {
         assertEquals(value, brokerProfileResult.get(key), "Unexpected difference between profile values for key: " + key);
      });
   }


   @Test
   public void testLinux() throws Exception {
      final String instanceDir = basedir + "/target/classes/servers/linuxUpgrade";
      final String bin = instanceDir + "/bin";
      final String etc = basedir + "/target/classes/servers/linuxUpgradeETC";
      final String data = instanceDir + "/data-custom";

      checkExpectedValues(bin + "/artemis", "ARTEMIS_INSTANCE_ETC=", "'" + etc + "'");

      Map<String, String> brokerProfileResult = checkExpectedValues(etc + "/artemis.profile",
                                                       "ARTEMIS_HOME=", null, // no expected value, will check on result
                                                       "ARTEMIS_INSTANCE=", "'" + instanceDir + "'",
                                                       "ARTEMIS_DATA_DIR=", "'" + data + "'",
                                                       "ARTEMIS_OOME_DUMP=", "'" + instanceDir + "/log/oom_dump.hprof'");

      String home = brokerProfileResult.get("ARTEMIS_HOME=");
      assertNotNull(home);
      assertNotEquals("'must-change'", home);

      File oldLogging = new File(etc + "/logging.properties");
      File newLogging = new File(etc + "/log4j2.properties");

      assertFalse(oldLogging.exists(), "Old logging must be removed by upgrade");
      assertTrue(newLogging.exists(), "New Logging must be installed by upgrade");

      final String utilityProfilePath = etc + "/artemis-utility.profile";
      final File utilityProfile = new File(utilityProfilePath);
      assertTrue(utilityProfile.exists(), "New utility profile must exist after upgrade");

      Map<String, String> utilityProfileResult = checkExpectedValues(utilityProfilePath,
            "ARTEMIS_HOME=", null, // no expected value, will check on result
            "ARTEMIS_INSTANCE=", "'" + instanceDir + "'",
            "ARTEMIS_DATA_DIR=", "'" + data + "'");

      assertEquals(3, utilityProfileResult.size(), "Unexpected number of results");

      utilityProfileResult.forEach((key, value) -> {
         assertEquals(value, brokerProfileResult.get(key), "Unexpected difference between profile values for key: " + key);
      });
   }

   @Test
   public void testCompareUpgradeCurrentVersion() throws Throwable {
      File upgradeConfig = new File(basedir + "/target/upgrade/currentVersion");
      File originalConfig = new File(basedir + "/target/upgrade/currentVersionCopy");
      FileUtil.deleteDirectory(originalConfig); // removing eventual previous runs

      // for previous runs
      removeBackups(upgradeConfig);

      // I'm keeping the current configuration as originalConfig, to make a comparison after upgrade is called
      FileUtil.copyDirectory(upgradeConfig, originalConfig);

      // looking up for the ARTEMIS_HOME from the profile file
      Properties properties = new Properties();
      properties.load(new FileInputStream(new File(originalConfig, "/etc/artemis.profile")));
      File home = new File(parseProperty(properties, "ARTEMIS_HOME"));

      Upgrade upgrade = new Upgrade();
      upgrade.setHomeValues(home, null, null);
      upgrade.setDirectory(upgradeConfig);
      upgrade.run(new ActionContext());

      // for current run
      removeBackups(upgradeConfig);

      // Calling upgrade on itself should not cause any changes to *any* file
      // output should be exactly the same
      assertTrue(compareDirectories(false, originalConfig.toPath(), upgradeConfig.toPath()));
   }

   private void removeBackups(File upgradeConfig) {
      File[] bkpFiles = upgradeConfig.listFiles(f -> f.isDirectory() && f.getName().contains("config-bkp"));
      for (File f : bkpFiles) {
         logger.debug("Removing {}", f);
         FileUtil.deleteDirectory(f);
      }
   }

   private static String parseProperty(Properties properties, String name) {
      String property = properties.getProperty(name);
      if (property == null) {
         return null;
      }
      // the property value might have quotes needed for bash. We need to remove those here
      if (property.startsWith("'") && property.endsWith("'")) {
         property = property.substring(1, property.length() - 1);
      }
      return property;
   }


   public static boolean compareDirectories(boolean allowExpectedWord, Path expected, Path upgrade) throws IOException {
      Files.walkFileTree(expected, new SimpleFileVisitor<>() {
         @Override
         public FileVisitResult visitFile(Path expectedFile, BasicFileAttributes attrs) throws IOException {
            FileVisitResult result = super.visitFile(expectedFile, attrs);

            Path relativize = expected.relativize(expectedFile);
            Path upgradeFile = upgrade.resolve(relativize);

            compareFiles(allowExpectedWord, expectedFile.toFile(), upgradeFile.toFile());

            return result;
         }
      });
      return true;
   }

   private Map<String, String> checkExpectedValues(String fileName, String... expectedPairs) throws Exception {
      assertTrue(expectedPairs.length > 0 && expectedPairs.length % 2 == 0, "You must pass a pair of expected values");
      Map<String, String> expectedValues = new HashMap<>();
      Map<String, String> matchingValues = new HashMap<>();

      for (int i = 0; i < expectedPairs.length; i += 2) {
         assertFalse(expectedValues.containsKey(expectedPairs[i]), "Value duplicated on pairs ::" + expectedPairs[i]);
         expectedValues.put(expectedPairs[i], expectedPairs[i + 1]);
      }

      File file = new File(fileName);
      try (Stream<String> lines = Files.lines(file.toPath())) {
         lines.forEach(line -> {
            String trimmedLine = line.trim();
            expectedValues.forEach((key, value) -> {
               if (trimmedLine.startsWith(key)) {
                  String actualValue = trimmedLine.substring(key.length());
                  logger.debug("match = {}", line);
                  matchingValues.put(key, actualValue);

                  if (value == null) {
                     logger.debug("no expected value was defined for {}, we will just fill out the matchingValues for further evaluation", key);
                  } else {
                     if (logger.isDebugEnabled()) {
                        logger.debug("prefix={}, expecting={}, actualValue={}", key, value, actualValue);
                     }
                     assertEquals(value, actualValue, key + " did not match");
                  }
               }
            });
         });
      }

      Object[] expectedKeys = expectedValues.keySet().stream().sorted().toArray();
      Object[] matchedKeys = matchingValues.keySet().stream().sorted().toArray();

      assertArrayEquals(expectedKeys, matchedKeys, "Some elements were not found in the output of " + fileName);
      assertEquals(matchingValues.size(), expectedValues.size(), "Some elements were not found in the output of " + fileName);

      return matchingValues;
   }
}
