/*******************************************************************************
 * Copyright (c) 2016, 2018 Red Hat.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Red Hat - Initial Contribution
 *******************************************************************************/

package org.eclipse.linuxtools.internal.docker.ui.launch;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.linuxtools.docker.core.DockerConnectionManager;
import org.eclipse.linuxtools.docker.core.DockerException;
import org.eclipse.linuxtools.internal.docker.core.DockerCompose;
import org.eclipse.linuxtools.internal.docker.core.DockerConnection;
import org.eclipse.linuxtools.internal.docker.core.ProcessLauncher;
import org.eclipse.linuxtools.internal.docker.ui.consoles.ConsoleMessages;
import org.eclipse.linuxtools.internal.docker.ui.testutils.CustomMatchers;
import org.eclipse.linuxtools.internal.docker.ui.testutils.MockDockerClientFactory;
import org.eclipse.linuxtools.internal.docker.ui.testutils.MockDockerConnectionFactory;
import org.eclipse.linuxtools.internal.docker.ui.testutils.ProjectInitializationRule;
import org.eclipse.linuxtools.internal.docker.ui.testutils.RunWithProject;
import org.eclipse.linuxtools.internal.docker.ui.testutils.swt.ButtonAssertions;
import org.eclipse.linuxtools.internal.docker.ui.testutils.swt.ClearConnectionManagerRule;
import org.eclipse.linuxtools.internal.docker.ui.testutils.swt.ClearLaunchConfigurationsRule;
import org.eclipse.linuxtools.internal.docker.ui.testutils.swt.CloseShellRule;
import org.eclipse.linuxtools.internal.docker.ui.testutils.swt.CloseWelcomePageRule;
import org.eclipse.linuxtools.internal.docker.ui.testutils.swt.ComboAssertions;
import org.eclipse.linuxtools.internal.docker.ui.testutils.swt.ConsoleViewRule;
import org.eclipse.linuxtools.internal.docker.ui.testutils.swt.DockerConnectionManagerUtils;
import org.eclipse.linuxtools.internal.docker.ui.testutils.swt.ProjectExplorerViewRule;
import org.eclipse.linuxtools.internal.docker.ui.testutils.swt.SWTUtils;
import org.eclipse.linuxtools.internal.docker.ui.testutils.swt.TextAssertions;
import org.eclipse.linuxtools.internal.docker.ui.testutils.swt.ToolbarButtonAssertions;
import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot;
import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotView;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotMenu;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotToolbarButton;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import com.spotify.docker.client.DockerClient;

/**
 * Testing the {@link DockerCompose} utility class using SWTBot.
 */
public class DockerComposeSWTBotTest {

	@ClassRule
	public static CloseWelcomePageRule closeWelcomePage = new CloseWelcomePageRule(
			CloseWelcomePageRule.DOCKER_PERSPECTIVE_ID);

	@Rule
	public ClearConnectionManagerRule clearConnectionManager = new ClearConnectionManagerRule();

	@Rule
	public ProjectInitializationRule projectInit = new ProjectInitializationRule();

	@Rule
	public ClearLaunchConfigurationsRule clearLaunchConfig = new ClearLaunchConfigurationsRule(
			IDockerComposeLaunchConfigurationConstants.CONFIG_TYPE_ID);

	@Rule
	public CloseShellRule closeShell = new CloseShellRule(IDialogConstants.CLOSE_LABEL);

	@Rule
	public ConsoleViewRule consoleViewRule = new ConsoleViewRule();

	@Rule
	public ProjectExplorerViewRule projectExplorerViewRule = new ProjectExplorerViewRule();

	private SWTWorkbenchBot bot = new SWTWorkbenchBot();

	private CountDownLatch latch;

	@Before
	public void setupMockedProcessLauncher() throws DockerException, InterruptedException {
		// configure the 'docker-compose up' mocks with a CountDownLatch to
		// simulate a long-running process
		final ProcessLauncher mockProcessLauncher = Mockito.mock(ProcessLauncher.class, Mockito.RETURNS_DEEP_STUBS);
		DockerCompose.getInstance().setProcessLauncher(mockProcessLauncher);
		setupDockerComposeUpMockProcess(mockProcessLauncher);
		// configure the 'docker-compose stop' mocks which release the
		// CountDownLatch to halt the long-running process
		setupDockerComposeStopMockProcess(mockProcessLauncher);

	}

	private void setupDockerComposeUpMockProcess(final ProcessLauncher mockProcessLauncher)
			throws DockerException, InterruptedException {
		final Process mockDockerComposeUpProcess = Mockito.mock(Process.class);
		Mockito.when(mockDockerComposeUpProcess.getInputStream())
				.thenReturn(new ByteArrayInputStream("up!\n".getBytes()));
		Mockito.when(mockDockerComposeUpProcess.getErrorStream()).thenReturn(new ByteArrayInputStream("".getBytes()));
		Mockito.when(mockDockerComposeUpProcess.getOutputStream()).thenReturn(new ByteArrayOutputStream());
		Mockito.when(
				mockProcessLauncher
						.processBuilder(ArgumentMatchers.anyString(),
								ArgumentMatchers.eq(DockerCompose.getDockerComposeCommandName()),
								CustomMatchers.arrayContains("up"))
						.workingDir(ArgumentMatchers.anyString()).start())
				.thenReturn(mockDockerComposeUpProcess);
		latch = new CountDownLatch(1);
		Mockito.when(mockDockerComposeUpProcess.waitFor()).then(new Answer<Object>() {

			@Override
			public Object answer(InvocationOnMock invocation) throws Throwable {
				latch.await(5, TimeUnit.SECONDS);
				return 0;
			}
		});
	}

	private void setupDockerComposeStopMockProcess(final ProcessLauncher mockProcessLauncher)
			throws DockerException, InterruptedException {
		final Process mockDockerComposeStopProcess = Mockito.mock(Process.class);
		Mockito.when(mockDockerComposeStopProcess.getInputStream())
				.thenReturn(new ByteArrayInputStream("stop\n".getBytes()));
		Mockito.when(mockDockerComposeStopProcess.getErrorStream()).thenReturn(new ByteArrayInputStream("".getBytes()));
		Mockito.when(mockDockerComposeStopProcess.getOutputStream()).thenReturn(new ByteArrayOutputStream());

		Mockito.when(
				mockProcessLauncher
						.processBuilder(ArgumentMatchers.anyString(),
								ArgumentMatchers.eq(DockerCompose.getDockerComposeCommandName()),
								CustomMatchers.arrayContains("stop"))
						.workingDir(ArgumentMatchers.anyString()).start())
				.thenReturn(mockDockerComposeStopProcess);
		Mockito.when(mockDockerComposeStopProcess.waitFor()).then(invocation -> {
			latch.countDown();
			return 0;
		});
	}

	/**
	 * @return the {@link SWTBotMenu} for the "Run as > Docker Image Build"
	 *         shortcut
	 */
	private SWTBotMenu getRunAsDockerComposeContextMenu(final String projectName, final String dockerComposeFileName) {
		final SWTBotView projectExplorerBotView = this.projectExplorerViewRule.getProjectExplorerBotView();
		// make sure the project explorer view is visible, in case it was hidden
		// by another view.
		projectExplorerBotView.setFocus();
		final SWTBotTreeItem fooProjectTreeItem = SWTUtils.getTreeItem(projectExplorerBotView, projectName);
		assertThat(fooProjectTreeItem).isNotNull();
		SWTUtils.syncExec(() -> fooProjectTreeItem.expand());
		final SWTBotTreeItem dockerfileTreeItem = SWTUtils.getTreeItem(fooProjectTreeItem, dockerComposeFileName);
		assertThat(dockerfileTreeItem).isNotNull();
		SWTUtils.select(dockerfileTreeItem);
		final SWTBotMenu runAsDockerComposeMenu = SWTUtils.getContextMenu(
				projectExplorerBotView.bot().tree(),
				"Run As", "1 Docker Compose");
		return runAsDockerComposeMenu;
	}

	@Test
	@RunWithProject("foo")
	public void shouldDisableCommandOnFirstCallWhenMissingConnection() {
		// given no connection
		ClearConnectionManagerRule.removeAllConnections(DockerConnectionManager.getInstance());
		// when
		SWTUtils.asyncExec(() -> getRunAsDockerComposeContextMenu("foo", "docker-compose.yml").click());
		// then expect an error dialog because no Docker connection exists
		assertThat(bot.shell(LaunchMessages.getString("DockerComposeUpShortcut.no.connections.msg"))).isNotNull();
		// closing the wizard
		SWTUtils.syncExec(() -> {
			bot.button("No").click();
		});
	}

	@Test
	@RunWithProject("foo")
	public void shouldStartDockerComposeFromScratch() throws CoreException {
		// given
		final DockerClient client = MockDockerClientFactory.build();
		final DockerConnection dockerConnection = MockDockerConnectionFactory.from("Test", client)
				.withDefaultTCPConnectionSettings();
		DockerConnectionManagerUtils.configureConnectionManager(dockerConnection);
		// when
		SWTUtils.asyncExec(() -> getRunAsDockerComposeContextMenu("foo", "docker-compose.yml").click());
		// then confirm the connection
		bot.button("OK").click();
		// wait for the job to run
		SWTUtils.waitForJobsToComplete();
		// then expect the console to be displayed
		assertThat(SWTUtils.isConsoleViewVisible(this.bot)).isTrue();
		// expect the 'stop' button to be enabled
		final SWTBotToolbarButton consoleToolbarStopButton = SWTUtils.getConsoleToolbarButtonWithTooltipText(bot,
				ConsoleMessages.getString("DockerComposeStopAction.tooltip"));
		ToolbarButtonAssertions.assertThat(consoleToolbarStopButton).isEnabled();
		// verify that the launch configuration was saved
		final ILaunchConfiguration launchConfiguration = LaunchConfigurationUtils.getLaunchConfigurationByName(
				IDockerComposeLaunchConfigurationConstants.CONFIG_TYPE_ID, "Docker Compose [foo]");
		assertThat(launchConfiguration).isNotNull();
		// verify the latch
		assertThat(latch.getCount()).isEqualTo(1);
	}

	@Test
	@RunWithProject("foo")
	public void shouldStartDockerComposeWithExistingLaunchConfiguration() throws CoreException {
		// given
		final DockerClient client = MockDockerClientFactory.build();
		final DockerConnection dockerConnection = MockDockerConnectionFactory.from("Test", client)
				.withDefaultTCPConnectionSettings();
		DockerConnectionManagerUtils.configureConnectionManager(dockerConnection);
		final IFile dockerComposeScript = projectInit.getProject().getFile("docker-compose.yml");
		LaunchConfigurationUtils.createDockerComposeUpLaunchConfiguration(dockerConnection, dockerComposeScript);
		// when
		SWTUtils.asyncExec(() -> getRunAsDockerComposeContextMenu("foo", "docker-compose.yml").click());
		// then confirm the connection
		final SWTBotToolbarButton consoleToolbarStopButton = SWTUtils.getConsoleToolbarButtonWithTooltipText(bot,
				ConsoleMessages.getString("DockerComposeStopAction.tooltip"));
		ToolbarButtonAssertions.assertThat(consoleToolbarStopButton).isEnabled();
	}

	@Test
	@RunWithProject("foo")
	public void shouldStopDockerCompose() throws CoreException {
		// given
		shouldStartDockerComposeFromScratch();
		// when
		final SWTBotToolbarButton consoleToolbarStopButton = SWTUtils.getConsoleToolbarButtonWithTooltipText(bot,
				ConsoleMessages.getString("DockerComposeStopAction.tooltip"));
		ToolbarButtonAssertions.assertThat(consoleToolbarStopButton).isEnabled();
		consoleToolbarStopButton.click();
		// then
		// verify the latch is down
		assertThat(latch.getCount()).isEqualTo(0);
		// verify the stop button is disabled
		ToolbarButtonAssertions.assertThat(consoleToolbarStopButton).isNotEnabled();
	}

	@Test
	@RunWithProject("foo")
	public void shouldRestartDockerCompose() throws InterruptedException, DockerException {
		// given
		final DockerClient client = MockDockerClientFactory.build();
		final DockerConnection dockerConnection = MockDockerConnectionFactory.from("Test", client)
				.withDefaultTCPConnectionSettings();
		DockerConnectionManagerUtils.configureConnectionManager(dockerConnection);
		// when starting without launch config
		SWTUtils.asyncExec(() -> getRunAsDockerComposeContextMenu("foo", "docker-compose.yml").click());
		bot.button("OK").click();
		// wait for the job to run
		SWTUtils.waitForJobsToComplete();
		// when stopping
		final SWTBotToolbarButton consoleToolbarStopButton = SWTUtils.getConsoleToolbarButtonWithTooltipText(bot,
				ConsoleMessages.getString("DockerComposeStopAction.tooltip"));
		ToolbarButtonAssertions.assertThat(consoleToolbarStopButton).isEnabled();
		consoleToolbarStopButton.click();
		// redo the setup to get a new mock process
		setupMockedProcessLauncher();
		// when restarting
		SWTUtils.asyncExec(() -> getRunAsDockerComposeContextMenu("foo", "docker-compose.yml").click());
		// wait for the job to run
		SWTUtils.waitForJobsToComplete();
		// then
		ToolbarButtonAssertions.assertThat(consoleToolbarStopButton).isEnabled();
	}

	@Test
	@RunWithProject("foo")
	@Ignore // ignored for now because the "Run" menu from the toolbar remains
			// visible (on macOS) and this
	// has side-effects on the other tests that fail because the widgets are not
	// found.
	public void shouldValidateLaunchConfiguration() throws CoreException {
		// given
		final DockerClient client = MockDockerClientFactory.build();
		final DockerConnection dockerConnection = MockDockerConnectionFactory.from("Test", client)
				.withDefaultTCPConnectionSettings();
		DockerConnectionManagerUtils.configureConnectionManager(dockerConnection);
		final IFile dockerComposeScript = projectInit.getProject().getFile("docker-compose.yml");
		LaunchConfigurationUtils.createDockerComposeUpLaunchConfiguration(dockerConnection, dockerComposeScript);
		// when
		bot.toolbarDropDownButtonWithTooltip("Run").menuItem("Run Configurations...").click();
		final SWTBotTreeItem dockerComposeTreeItem = SWTUtils.expand(bot.tree(), "Docker Compose");
		SWTUtils.select(dockerComposeTreeItem, "Docker Compose [foo]");
		// verify that the config is set and the form can be closed with the
		// "OK" button
		ComboAssertions.assertThat(bot.comboBox(0)).isEnabled().itemSelected("Test");
		TextAssertions.assertThat(bot.text(2)).isEnabled().textEquals("/foo");
		ButtonAssertions.assertThat(bot.button("Run")).isEnabled();

	}
}
