/**
 * 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.jboss.installer.core;

import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;

import com.google.auto.service.AutoService;
import org.apache.commons.io.FileUtils;
import org.assertj.core.api.Assertions;
import org.jboss.installer.auto.AutomaticInstallationParsingException;
import org.jboss.installer.auto.InstallationDataSerializer;
import org.jboss.installer.auto.ListXMLEventReader;
import org.jboss.installer.postinstall.PostInstallTask;
import org.jboss.installer.postinstall.task.AddAdminUserTask;
import org.jboss.installer.postinstall.task.LoggingLevelsTask;
import org.jboss.installer.test.utils.MockInputConsole;
import org.jboss.installer.test.utils.MockLanguageUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.xml.sax.SAXParseException;

import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;

import static org.assertj.core.api.Assertions.assertThat;
import static org.jboss.installer.auto.InstallationDataSerializer.NS;
import static org.jboss.installer.auto.InstallationDataSerializer.PASSWORD_ATTRIBUTE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

public class InstallationDataSerializerTest {

   private Path testDir;
   private final InstallationDataSerializer serializer = new InstallationDataSerializer(new MockLanguageUtils(), new MockInputConsole());

   @Before
   public void setUp() throws Exception {
      testDir = Files.createTempDirectory("test");
   }

   @After
   public void tearDown() throws Exception {
      FileUtils.deleteDirectory(testDir.toFile());
   }

   @Test
   public void testWriteEmptyInstallationData() throws Exception {
      final Path outputPath = testDir.resolve("auto.xml");
      final InstallationData installationData = new InstallationData();

      persist(installationData, outputPath.toAbsolutePath());

      assertTrue(outputPath.toFile().exists());
      final String content = FileUtils.readFileToString(outputPath.toFile(), "UTF-8");
      assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + "<installation-data xmlns=\""+NS+"\"/>\n", content);
   }

   @Test
   public void testWriteMinimalInstallationData() throws Exception {
      final Path outputPath = testDir.resolve("auto.xml");
      final InstallationData installationData = new InstallationData();
      installationData.setAdminUsername("testName");
      installationData.setPassword("testPassword");
      installationData.setTargetFolder(testDir.resolve("output"));

      persist(installationData, outputPath.toAbsolutePath());
      FileUtils.writeStringToFile(outputPath.getParent().resolve("auto.xml.variables").toFile(), "adminPassword=testPassword", Charset.defaultCharset());

      assertTrue(outputPath.toFile().exists());

      final InstallationData loadedData = load(outputPath);
      assertEquals("testName", loadedData.getAdminUsername());
      assertEquals("testPassword", loadedData.getPassword());
      assertEquals(testDir.resolve("output"), loadedData.getTargetFolder());
   }

   @Test
   public void testWriteInstallationDataWithMavenRepositories() throws Exception {
      final Path outputPath = testDir.resolve("auto.xml");
      final Map<String, URL> mavenRepositoriesMap = new HashMap<>();
      mavenRepositoriesMap.put("repo1", new URL("https://repository.jboss.org/nexus/content/repositories/public/"));
      mavenRepositoriesMap.put("repo2", new URL("https://maven.repository.redhat.com/ga/"));
      final InstallationData installationData = new InstallationData();
      installationData.setMavenRepositories(mavenRepositoriesMap);

      persist(installationData, outputPath.toAbsolutePath());

      assertTrue(outputPath.toFile().exists());
      final InstallationData loadedData = load(outputPath);
      assertEquals(2, loadedData.getMavenRepositories().size());
      assertTrue("repo1 should exist", loadedData.getMavenRepositories().keySet().contains("repo1"));
      assertTrue("repo2 should exist", loadedData.getMavenRepositories().keySet().contains("repo2"));
   }
   @Test
   public void testWriteInstallationDataWithPostInstallTask() throws Exception {
      final Path outputPath = testDir.resolve("auto.xml");
      final InstallationData installationData = new InstallationData();
      installationData.addPostInstallTask(new AddAdminUserTask());

      persist(installationData, outputPath.toAbsolutePath());

      assertTrue(outputPath.toFile().exists());

      final InstallationData loadedData = load(outputPath);
      assertEquals(1, loadedData.getPostInstallTasks().size());
      assertEquals(new AddAdminUserTask().getName(), loadedData.getPostInstallTasks().get(0).getName());
   }

   @Test
   public void testWriteInstallationDataWithConfig() throws Exception {
      final Path outputPath = testDir.resolve("auto.xml");
      final InstallationData installationData = new InstallationData();
      installationData.putConfig(new LoggingLevelsTask.Config("DEBUG", "TRACE"));

      persist(installationData, outputPath.toAbsolutePath());

      assertTrue(outputPath.toFile().exists());

      final InstallationData loadedData = load(outputPath);
      final LoggingLevelsTask.Config config = loadedData.getConfig(LoggingLevelsTask.Config.class);
      assertNotNull(config);
      assertEquals("DEBUG", config.getRootLevel());
      assertEquals("TRACE", config.getConsoleLevel());
   }

   @Test(expected = AutomaticInstallationParsingException.class)
   public void testReadInstallationDataWithUnknownPostInstallTaskThrowsException() throws Exception {
      final Path outputPath = testDir.resolve("auto.xml");
      final InstallationData installationData = new InstallationData();
      installationData.addPostInstallTask(new AddAdminUserTask());

      persist(installationData, outputPath.toAbsolutePath());

      // modify the file to try loading class that doesn't exist
      String xml = FileUtils.readFileToString(outputPath.toAbsolutePath().toFile(), "UTF-8");
      xml = xml.replace(new AddAdminUserTask().getSerializationName(), "i.dont.Exit.class");
      FileUtils.writeStringToFile(outputPath.toAbsolutePath().toFile(), xml, "UTF-8");

      assertTrue(outputPath.toFile().exists());

      load(outputPath);
   }

   @Test(expected = SAXParseException.class)
   public void testReadInstallationDataWithUnknownElement() throws Exception {
      final Path outputPath = testDir.resolve("auto.xml");
      final InstallationData installationData = new InstallationData();
      installationData.addPostInstallTask(new AddAdminUserTask());

      persist(installationData, outputPath.toAbsolutePath());

      // modify the file to try loading class that doesn't exist
      String xml = FileUtils.readFileToString(outputPath.toAbsolutePath().toFile(), "UTF-8");
      xml = xml.replace("post-install-task", "unsupported-element");
      FileUtils.writeStringToFile(outputPath.toAbsolutePath().toFile(), xml, "UTF-8");

      assertTrue(outputPath.toFile().exists());

      load(outputPath);
   }

   @Test
   public void testConfigRegistersItsVariables() throws Exception {
      final Path outputPath = testDir.resolve("auto.xml");
      final InstallationData installationData = new InstallationData();
      installationData.putConfig(new TestPostInstallConfig());

      persist(installationData, outputPath.toAbsolutePath());
      System.out.println(Files.readString(outputPath));

      final Path variablesFile = outputPath.getParent().resolve("auto.xml.variables");
      assertTrue(variablesFile.toFile().exists());
      assertEquals("testVariable=", FileUtils.readFileToString(variablesFile.toFile(), "UTF-8").trim());
      FileUtils.writeStringToFile(outputPath.getParent().resolve("auto.xml.variables").toFile(), "testVariable=some value", Charset.defaultCharset());

      final InstallationData loadedData = load(outputPath);
      assertEquals("some value", ((TestPostInstallConfig)loadedData.getConfig(TestPostInstallConfig.class)).testVariable);
   }

   @Test
   public void testExcludesAndIncludedPackages() throws Exception {
      final Path outputPath = testDir.resolve("auto.xml");
      final InstallationData installationData = new InstallationData();
      installationData.setExcludedPackages(Arrays.asList("package1", "package2"));
      installationData.setSelectedPackages(Arrays.asList("package3", "package4"));

      persist(installationData, outputPath.toAbsolutePath());

      final InstallationData loadedData = load(outputPath);
      assertThat(loadedData.getExcludedPackages()).contains(
              "package1",
              "package2"
      );
      assertThat(loadedData.getSelectedPackages()).contains(
              "package3",
              "package4"
      );
   }

   @Test
   public void testPasswordAttributeInVariableFile() throws Exception{
      final Path outputPath = testDir.resolve("auto.xml");
      final InstallationData installationData = new InstallationData();
      installationData.setAdminUsername("testName");
      installationData.setPassword("testPassword");
      persist(installationData, outputPath.toAbsolutePath());

      assertTrue(outputPath.toFile().exists());
      final String content = FileUtils.readFileToString(outputPath.getParent().resolve("auto.xml.variables").toFile());
      assertTrue(content.contains("adminPassword"));
   }

   @Test
   public void readInvalidXml() throws Exception {
      FileUtils.writeStringToFile(testDir.resolve("test.xml").toFile(),
              "<installation-data xmlns=\""+NS+"\">\n" +
                      "<i-dont-exist>foobar</i-dont-exist>\n" +
                      "</installation-data>");

      assertThrows(SAXParseException.class, () ->
         load(testDir.resolve("test.xml"))
      );
   }

   @Test
   public void testReadVariablesFileNotSpecified() throws Exception {
      final Path outputPath = testDir.resolve("auto.xml");
      final InstallationData installationData = new InstallationData();
      installationData.setAdminUsername("testName");
      installationData.setPassword("testPassword");

      persist(installationData, outputPath.toAbsolutePath());

      final InstallationData loadedData = load(outputPath);

      assertEquals("test", loadedData.getPassword());
   }

   @Test
   public void testReadCustomVariablesFileDoesntExist() throws Exception {
      final Path outputPath = testDir.resolve("auto.xml");
      final InstallationData installationData = new InstallationData();
      installationData.setAdminUsername("testName");
      installationData.setPassword("testPassword");

      persist(installationData, outputPath.toAbsolutePath());

      final Path variablesFile = testDir.resolve("idontexist.xml.variables");
      Assertions.assertThatThrownBy(()-> load(outputPath, variablesFile))
              .isInstanceOf(AutomaticInstallationParsingException.class)
              .hasMessage("console.error.automatic_installer_file_not_found [" + variablesFile.toUri().toURL()  + "]");
   }

   @Test
   public void testReadCustomVariablesFileExists() throws Exception {
      final Path variablesPath = testDir.resolve("custom.xml.variables");
      Files.writeString(variablesPath, (PASSWORD_ATTRIBUTE+"="+"random_password"));

      final Path outputPath = testDir.resolve("auto.xml");
      final InstallationData installationData = new InstallationData();
      installationData.setAdminUsername("testName");
      installationData.setPassword("testPassword");

      persist(installationData, outputPath.toAbsolutePath());

      final InstallationData loadedData = load(outputPath, testDir.resolve("custom.xml.variables"));

      assertEquals("random_password", loadedData.getPassword());
   }

   @Test
   public void readNonExistingXml() throws Exception {
      final Path inputFilePath = testDir.resolve("idontexist.xml");
      Assertions.assertThatThrownBy(() -> load(inputFilePath))
              .isInstanceOf(AutomaticInstallationParsingException.class)
              .hasMessage("console.error.automatic_installer_file_not_found [" + inputFilePath.toUri().toURL() + "]");
   }

   private InstallationData load(Path inputFilePath, Path variables) throws Exception {
      return serializer.deserialize(inputFilePath.toUri().toURL(), Optional.of(variables.toUri().toURL()));
   }

   private InstallationData load(Path inputFilePath) throws Exception {
      return serializer.deserialize(inputFilePath.toUri().toURL(), Optional.empty());
   }

   private void persist(InstallationData installationData, Path outputFile) throws AutomaticInstallationParsingException {
      serializer.serialize(installationData, outputFile);
   }

   @AutoService(PostInstallTask.class)
   public static class TestPostInstallTask implements PostInstallTask {

      @Override
      public String getSerializationName() {
         return "test-serialization-name";
      }

      @Override
      public String getName() {
         return "test.serialization";
      }

      @Override
      public Class<? extends InstallationData.PostInstallConfig> getConfigClass() {
         return TestPostInstallConfig.class;
      }
   }

   public static class TestPostInstallConfig implements InstallationData.PostInstallConfig {

      String testVariable;

      @Override
      public XMLEventReader serialize(XMLEventFactory eventFactory, Set<String> variables) {
         variables.add("testVariable");
         return new ListXMLEventReader(Collections.emptyList());
      }

      @Override
      public void deserialize(XMLEventReader reader, BiFunction<String, String, String> variableResolver) throws AutomaticInstallationParsingException {
         testVariable = variableResolver.apply("testVariable", "Test variable");
      }
   }

}
