/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2021 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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.postinstall;

import org.jboss.installer.core.InstallationData;
import org.jboss.installer.core.InstallationFailedException;
import org.jboss.installer.core.LanguageUtils;
import org.jboss.installer.postinstall.server.DomainServer;
import org.jboss.installer.postinstall.server.StandaloneServer;
import org.jboss.installer.test.utils.MockLanguageUtils;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.InOrder;

import java.beans.PropertyChangeSupport;
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.List;
import java.util.Map;
import java.util.TreeSet;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

public class PostInstallTaskRunnerTest {

    @Rule
    public TemporaryFolder temp = new TemporaryFolder();

    private PropertyChangeSupport propertyChangeSupport = mock(PropertyChangeSupport.class);
    private LanguageUtils langUtils = new MockLanguageUtils();
    private Map<PostInstallTask, PostInstallTaskImpl> taskMap = new HashMap<>();

    @Test
    public void executeSimpleTask() throws Exception {
        final PostInstallTask desc = mockTaskDesc();
        final SimplePostInstallTaskImpl mock = mockSimpleTask(desc);
        final StandaloneServer mockServer = mock(StandaloneServer.class);
        final InstallationData installationData = registerTasks(desc);

        createRunner(mockServer).execute(installationData, propertyChangeSupport);

        verify(mock).applyToInstallation(eq(installationData), any());
    }

    @Test
    public void executeSimpleTaskLogsTaskName() throws Exception {
        final PostInstallTask desc = mockTaskDesc();
        final SimplePostInstallTaskImpl mock = mockSimpleTask(desc);
        when(mock.getName()).thenReturn("test_value");
        final StandaloneServer mockServer = mock(StandaloneServer.class);
        final InstallationData installationData = registerTasks(desc);

        createRunner(mockServer).execute(installationData, propertyChangeSupport);

        verify(propertyChangeSupport).firePropertyChange(PostInstallTaskRunner.POST_INSTALL_TASK_NAME_PROPERTY, "", "test_value");
        verify(mock).applyToInstallation(eq(installationData), any());
    }

    @Test
    public void executeCliTask() throws Exception {
        final PostInstallTask desc = mockTaskDesc();
        final CliPostInstallTaskImpl mockTask = mockCliTask(desc);
        final StandaloneServer mockServer = mockStandaloneServer("config1");
        final InstallationData installationData = registerTasks(desc);

        createRunner(mockServer).execute(installationData, propertyChangeSupport);

        verify(mockTask).applyToStandalone(eq(installationData), eq(mockServer), any());
    }

    @Test
    public void cleanUpAfterExecuteCliTask() throws Exception {
        final PostInstallTask desc = mockTaskDesc();
        final CliPostInstallTaskImpl mockTask = mockCliTask(desc);
        final StandaloneServer mockServer = mockStandaloneServer("config1");
        final InstallationData installationData = registerTasks(desc);

        final Path mockServerDir = temp.newFolder("mock-server").toPath();
        final Path testPath = mockServerDir.resolve("standalone").resolve("data");
        Files.createDirectories(testPath);
        Files.writeString(testPath.resolve("test"), "test");
        when(mockServer.getTemporaryPaths()).thenReturn(List.of(testPath));

        createRunner(mockServer).execute(installationData, propertyChangeSupport);

        assertThat(testPath).doesNotExist();
    }

    @Test
    public void simpleTasksAreExecutedBeforeCliTask() throws Exception {
        final PostInstallTask desc1 = mockTaskDesc();
        final PostInstallTask desc2 = mockTaskDesc();
        final PostInstallTask desc3 = mockTaskDesc();
        final PostInstallTask desc4 = mockTaskDesc();
        final CliPostInstallTaskImpl mockCliTask1 = mockCliTask(desc1);
        final CliPostInstallTaskImpl mockCliTask2 = mockCliTask(desc2);
        final SimplePostInstallTaskImpl mockSimpleTask1 = mockSimpleTask(desc3);
        final SimplePostInstallTaskImpl mockSimpleTask2 = mockSimpleTask(desc4);
        final StandaloneServer mockServer = mockStandaloneServer("config1");
        final InstallationData installationData = registerTasks(desc1, desc2, desc3, desc4);

        createRunner(mockServer).execute(installationData, propertyChangeSupport);

        InOrder inOrder = inOrder(mockCliTask1, mockSimpleTask1, mockCliTask2, mockSimpleTask2);
        inOrder.verify(mockSimpleTask1).applyToInstallation(eq(installationData), any());
        inOrder.verify(mockSimpleTask2).applyToInstallation(eq(installationData), any());
        inOrder.verify(mockCliTask1).applyToStandalone(eq(installationData), eq(mockServer), any());
        inOrder.verify(mockCliTask2).applyToStandalone(eq(installationData), eq(mockServer), any());
    }

    @Test
    public void unknownTasksThrowException() throws Exception {
        final PostInstallTask desc = mockTaskDesc();
        final SimplePostInstallTaskImpl simpleTask = mockSimpleTask(desc);
        final PostInstallTask aTask = mockTaskDesc();
        // need to mock the InstallationData, otherwise we won't be able to pass wrong tasks in
        final InstallationData installationData = mock(InstallationData.class);
        when(installationData.getPostInstallTasks()).thenReturn(Arrays.asList(desc, aTask));

        try {
            createRunner((StandaloneServer) null).execute(installationData, propertyChangeSupport);
            fail("Should fail on unknown task");
        } catch (IllegalStateException e) {
            Assert.assertEquals("Unable to find the implementation of task null", e.getMessage());
        }

        verify(simpleTask).setLanguageUtils(any());
        verifyNoMoreInteractions(simpleTask);
    }

    @Test
    public void serverIsStartedBeforeExecutingCliTasks() throws Exception {
        final PostInstallTask desc = mockTaskDesc();
        final CliPostInstallTaskImpl mockTask = mockCliTask(desc);
        final StandaloneServer mockServer = mockStandaloneServer("config1");
        final InstallationData installationData = registerTasks(desc);

        createRunner(mockServer).execute(installationData, propertyChangeSupport);

        InOrder inOrder = inOrder(mockServer, mockTask);
        inOrder.verify(mockServer).start(any(String.class));
        inOrder.verify(mockTask).applyToStandalone(eq(installationData), eq(mockServer), any());
        inOrder.verify(mockServer).shutdown();
    }

    @Test
    public void cliTaskIsCalledForEachServerConfiguration() throws Exception {
        final PostInstallTask desc = mockTaskDesc();
        final CliPostInstallTaskImpl mockTask = mockCliTask(desc);
        final StandaloneServer mockServer = mockStandaloneServer("config1", "config2");
        final InstallationData installationData = registerTasks(desc);

        createRunner(mockServer).execute(installationData, propertyChangeSupport);

        InOrder inOrder = inOrder(mockServer, mockTask);
        inOrder.verify(mockServer).start("config1");
        inOrder.verify(mockTask).applyToStandalone(eq(installationData), eq(mockServer), any());
        inOrder.verify(mockServer).start("config2");
        inOrder.verify(mockTask).applyToStandalone(eq(installationData), eq(mockServer), any());
    }

    @Test
    public void cliTaskCalledForDomainServer() throws Exception {
        final PostInstallTask desc = mockTaskDesc();
        final CliPostInstallTaskImpl mockTask = mockCliTask(desc);
        final DomainServer mockServer = mockDomainServer("config1");
        final InstallationData installationData = registerTasks(desc);

        createRunner(mockServer).execute(installationData, propertyChangeSupport);

        verify(mockTask).applyToDomain(eq(installationData), eq(mockServer), any());
    }

    @Test
    public void failedCliTaskPreventsOtherTasks() throws Exception {
        final PostInstallTask desc1 = mockTaskDesc();
        final PostInstallTask desc2 = mockTaskDesc();
        final CliPostInstallTaskImpl mockCliTask1 = mock(CliPostInstallTaskImpl.class);
        taskMap.put(desc1, mockCliTask1);
        when(mockCliTask1.applyToStandalone(any(), any(), any())).thenReturn(false);
        final CliPostInstallTaskImpl mockCliTask2 = mockCliTask(desc2);

        final StandaloneServer mockServer = mockStandaloneServer("config1");
        final InstallationData installationData = registerTasks(desc1, desc2);

        try {
            createRunner(mockServer).execute(installationData, propertyChangeSupport);
            fail("Exception should have been thrown");
        } catch (InstallationFailedException e) {
            // OK, expected
        }

        verify(mockCliTask2).setLanguageUtils(any());
        verifyNoMoreInteractions(mockCliTask2);
    }

    @Test
    public void failedSimpleTaskPreventsOtherTasks() throws Exception {
        final PostInstallTask desc1 = mockTaskDesc();
        final PostInstallTask desc2 = mockTaskDesc();
        final SimplePostInstallTaskImpl mockSimpleTask1 = mockSimpleTask(desc1);
        final CliPostInstallTaskImpl mockCliTask2 = mockCliTask(desc2);
        when(mockSimpleTask1.applyToInstallation(any(), any())).thenReturn(false);

        final StandaloneServer mockServer = mockStandaloneServer("config1");
        final InstallationData installationData = registerTasks(desc1, desc2);

        try {
            createRunner(mockServer).execute(installationData, propertyChangeSupport);
            fail("An exception should have been raised");
        } catch (InstallationFailedException e) {
            // OK we're expecting this
        }

        verify(mockCliTask2).setLanguageUtils(any());
        verifyNoMoreInteractions(mockCliTask2);
    }

    private PostInstallTask mockTaskDesc() {
        return mock(PostInstallTask.class);
    }

    private PostInstallTaskRunnerImpl createRunner(StandaloneServer mockServer) {
        return new PostInstallTaskRunnerImpl(mockServer, null, langUtils, null, taskMap::get);
    }

    private PostInstallTaskRunnerImpl createRunner(DomainServer mockServer) {
        return new PostInstallTaskRunnerImpl(null, mockServer, langUtils, null, taskMap::get);
    }

    private CliPostInstallTaskImpl mockCliTask(PostInstallTask desc) {
        final CliPostInstallTaskImpl mock = mock(CliPostInstallTaskImpl.class);
        when(mock.applyToStandalone(any(), any(), any())).thenReturn(true);
        when(mock.applyToDomain(any(), any(), any())).thenReturn(true);
        taskMap.put(desc, mock);
        return mock;
    }

    private SimplePostInstallTaskImpl mockSimpleTask(PostInstallTask desc) {
        final SimplePostInstallTaskImpl mock = mock(SimplePostInstallTaskImpl.class);
        when(mock.applyToInstallation(any(), any())).thenReturn(true);
        taskMap.put(desc, mock);
        return mock;
    }

    private StandaloneServer mockStandaloneServer(String... configs) {
        final StandaloneServer mockServer = mock(StandaloneServer.class);
        when(mockServer.availableConfigurations()).thenReturn(new TreeSet<>(Arrays.asList(configs)));
        when(mockServer.getTemporaryPaths()).thenReturn(Collections.emptyList());
        return mockServer;
    }

    private DomainServer mockDomainServer(String... configs) {
        final DomainServer mockServer = mock(DomainServer.class);
        when(mockServer.availableConfigurations()).thenReturn(new TreeSet<>(Arrays.asList(configs)));
        when(mockServer.getTemporaryPaths()).thenReturn(Collections.emptyList());
        return mockServer;
    }

    private InstallationData registerTasks(PostInstallTask... tasks) {
        final InstallationData installationData = new InstallationData();
        for (PostInstallTask task : tasks) {
            // cast it to correct type so that we don't need to register methods
            installationData.addPostInstallTask(task);
        }
        return installationData;
    }
}
