/*
 * Copyright 2015 Google Inc.
 *
 * 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 com.google.cloud.tools.eclipse.dataflow.core.launcher;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyMapOf;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.cloud.tools.eclipse.dataflow.core.launcher.options.PipelineOptionsHierarchy;
import com.google.cloud.tools.eclipse.dataflow.core.launcher.options.PipelineOptionsProperty;
import com.google.cloud.tools.eclipse.dataflow.core.launcher.options.PipelineOptionsType;
import com.google.cloud.tools.eclipse.dataflow.core.preferences.WritableDataflowPreferences;
import com.google.cloud.tools.eclipse.dataflow.core.project.DataflowDependencyManager;
import com.google.cloud.tools.eclipse.dataflow.core.project.MajorVersion;
import com.google.cloud.tools.eclipse.login.IGoogleLoginService;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.JavaLaunchDelegate;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

/**
 * Tests for {@link DataflowPipelineLaunchDelegate}.
 */
@RunWith(MockitoJUnitRunner.class)
public class DataflowPipelineLaunchDelegateTest {
  private DataflowPipelineLaunchDelegate dataflowDelegate;
  private final NullProgressMonitor monitor = new NullProgressMonitor();

  @Captor private ArgumentCaptor<Map<String, String>> variableMapCaptor;

  @Rule public TemporaryFolder tempFolder = new TemporaryFolder();

  @Mock private DataflowDependencyManager dependencyManager;
  @Mock private JavaLaunchDelegate javaDelegate;
  @Mock private IWorkspaceRoot workspaceRoot;
  @Mock private IProject project;
  @Mock private PipelineOptionsHierarchyFactory pipelineOptionsHierarchyFactory;
  @Mock private PipelineOptionsHierarchy pipelineOptionsHierarchy;
  @Mock private IGoogleLoginService loginService;
  @Mock private ILaunchConfigurationWorkingCopy configurationWorkingCopy;

  private final Map<String, String> pipelineArguments = new HashMap<>();
  private final Map<String, String> environmentMap = new HashMap<>();

  private Credential credential;

  @Before
  public void setup() throws Exception {
    when(pipelineOptionsHierarchyFactory.forProject(
            eq(project), eq(MajorVersion.ONE), any(IProgressMonitor.class)))
        .thenReturn(pipelineOptionsHierarchy);

    credential = new GoogleCredential.Builder()
        .setJsonFactory(mock(JsonFactory.class))
        .setTransport(mock(HttpTransport.class))
        .setClientSecrets("clientId", "clientSecret").build();
    credential.setRefreshToken("fake-refresh-token");
    when(loginService.getCredential("[email protected]")).thenReturn(credential);

    when(dependencyManager.getProjectMajorVersion(project)).thenReturn(MajorVersion.ONE);
    dataflowDelegate = new DataflowPipelineLaunchDelegate(javaDelegate,
        pipelineOptionsHierarchyFactory, dependencyManager, workspaceRoot, loginService);

    pipelineArguments.put("accountEmail", "");
    when(configurationWorkingCopy.getAttribute(
        eq(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES), anyMapOf(String.class, String.class)))
        .thenReturn(environmentMap);
  }

  @Test
  public void testGoogleApplicationCredentialsEnvironmentVariable() {
    assertEquals("GOOGLE_APPLICATION_CREDENTIALS",
        DataflowPipelineLaunchDelegate.GOOGLE_APPLICATION_CREDENTIALS_ENVIRONMENT_VARIABLE);
  }

  @Test
  public void testSetCredential_assumesAccountEmailIsGiven() throws CoreException {
    pipelineArguments.remove("accountEmail");

    try {
      dataflowDelegate.setCredential(configurationWorkingCopy, pipelineArguments);
      fail();
    } catch (NullPointerException ex) {
      assertEquals("account email missing in launch configuration or preferences", ex.getMessage());
    }
  }

  @Test
  public void testSetCredential_credentialEnvironmentVariableSet_serviceAccountKey()
      throws IOException {
    pipelineArguments.put("serviceAccountKey", tempFolder.newFile().getAbsolutePath());
    environmentMap.put("GOOGLE_APPLICATION_CREDENTIALS", "user-set-path");

    try {
      dataflowDelegate.setCredential(configurationWorkingCopy, pipelineArguments);
      fail();
    } catch (CoreException ex) {
      assertEquals("You cannot define the environment variable GOOGLE_APPLICATION_CREDENTIALS"
          + " when launching Dataflow pipelines from Cloud Tools for Eclipse.", ex.getMessage());
    }
  }

  @Test
  public void testSetCredential_credentialEnvironmentVariableSet_loginAccount() {
    pipelineArguments.put("accountEmail", "[email protected]");
    environmentMap.put("GOOGLE_APPLICATION_CREDENTIALS", "user-set-path");

    try {
      dataflowDelegate.setCredential(configurationWorkingCopy, pipelineArguments);
      fail();
    } catch (CoreException ex) {
      assertEquals("You cannot define the environment variable GOOGLE_APPLICATION_CREDENTIALS"
          + " when launching Dataflow pipelines from Cloud Tools for Eclipse.", ex.getMessage());
    }
  }

  @Test
  public void testSetCredential_noCredentialGiven() {
    try {
      dataflowDelegate.setCredential(configurationWorkingCopy, pipelineArguments);
      fail();
    } catch (CoreException ex) {
      assertEquals("No Google account selected for this launch.", ex.getMessage());
    }
  }

  @Test
  public void testSetCredential_savedAccountNotLoggedIn() {
    pipelineArguments.put("accountEmail", "[email protected]");
    when(loginService.getCredential("[email protected]")).thenReturn(null);  // not logged in

    try {
      dataflowDelegate.setCredential(configurationWorkingCopy, pipelineArguments);
      fail();
    } catch (CoreException ex) {
      assertEquals("The Google account saved for this lanuch is not logged in.", ex.getMessage());
    }
  }

  @Test
  public void testSetCredential_loginAccount() throws CoreException, IOException {
    pipelineArguments.put("accountEmail", "[email protected]");

    dataflowDelegate.setCredential(configurationWorkingCopy, pipelineArguments);
    verifyLoginAccountSet();
  }

  @Test
  public void testSetCredential_serviceAccountTakesPrecedence() throws CoreException, IOException {
    String keyFile = tempFolder.newFile().getAbsolutePath();
    pipelineArguments.put("accountEmail", "[email protected]");
    pipelineArguments.put("serviceAccountKey", keyFile);

    dataflowDelegate.setCredential(configurationWorkingCopy, pipelineArguments);
    verifyServiceAccountKeySet(keyFile);
  }

  private void verifyLoginAccountSet() throws IOException {
    verify(configurationWorkingCopy).setAttribute(
        eq(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES), variableMapCaptor.capture());
    String jsonCredentialPath = variableMapCaptor.getValue().get("GOOGLE_APPLICATION_CREDENTIALS");
    assertNotNull(jsonCredentialPath);
    assertThat(jsonCredentialPath, containsString("google-ct4e-"));
    assertThat(jsonCredentialPath, endsWith(".json"));

    Path credentialFile = Paths.get(jsonCredentialPath);
    assertTrue(Files.exists(credentialFile));

    String contents = new String(Files.readAllBytes(credentialFile), StandardCharsets.UTF_8);
    assertThat(contents, containsString("fake-refresh-token"));
  }

  private void verifyServiceAccountKeySet(String keyFileGiven) {
    verify(configurationWorkingCopy).setAttribute(
        eq(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES), variableMapCaptor.capture());
    String keyFile = variableMapCaptor.getValue().get("GOOGLE_APPLICATION_CREDENTIALS");
    assertEquals(keyFileGiven, keyFile);
  }

  @Test
  public void testSetCredential_originalEnvironmentMapUntouched_loginAccount()
      throws CoreException, IOException {
    pipelineArguments.put("accountEmail", "[email protected]");

    dataflowDelegate.setCredential(configurationWorkingCopy, pipelineArguments);
    verifyLoginAccountSet();
    assertTrue(environmentMap.isEmpty());
  }

  @Test
  public void testSetCredential_originalEnvironmentMapUntouched_serviceAccount()
      throws CoreException, IOException {
    String keyFile = tempFolder.newFile().getAbsolutePath();
    pipelineArguments.put("serviceAccountKey", keyFile);

    dataflowDelegate.setCredential(configurationWorkingCopy, pipelineArguments);
    verifyServiceAccountKeySet(keyFile);
    assertTrue(environmentMap.isEmpty());
  }

  @Test
  public void testSetCredential_nonExistingServiceAccountKey() {
    pipelineArguments.put("serviceAccountKey", "/non/existing/file.ext");

    try {
      dataflowDelegate.setCredential(configurationWorkingCopy, pipelineArguments);
      fail();
    } catch (CoreException ex) {
      assertThat(ex.getMessage(), startsWith("Cannot open service account key file: "));
      assertThat(ex.getMessage(), containsString("existing"));
      assertThat(ex.getMessage(), endsWith("file.ext"));
    }
  }

  @Test
  public void testSetCredential_directoryAsServiceAccountKey() {
    pipelineArguments.put("serviceAccountKey", "/");

    try {
      dataflowDelegate.setCredential(configurationWorkingCopy, pipelineArguments);
      fail();
    } catch (CoreException ex) {
      assertThat(ex.getMessage(), startsWith("Not a file but directory: "));
    }
  }

  @Test
  public void testSetCredential_serviceAccountKey() throws CoreException, IOException {
    String keyFile = tempFolder.newFile().getAbsolutePath();
    pipelineArguments.put("serviceAccountKey", keyFile);

    dataflowDelegate.setCredential(configurationWorkingCopy, pipelineArguments);
    verifyServiceAccountKeySet(keyFile);
  }

  @Test
  public void testLaunchWithLaunchConfigurationWithIncompleteArgsThrowsIllegalArgumentException()
      throws CoreException {
    ILaunchConfiguration configuration = mockILaunchConfiguration();
    Map<String, String> incompleteRequiredArguments = ImmutableMap.of();
    when(
        configuration.getAttribute(
            PipelineConfigurationAttr.ALL_ARGUMENT_VALUES.toString(),
            ImmutableMap.<String, String>of())).thenReturn(incompleteRequiredArguments);

    Set<PipelineOptionsProperty> properties =
        ImmutableSet.of(requiredProperty("foo"), requiredProperty("bar-baz"));
    when(
        pipelineOptionsHierarchy.getRequiredOptionsByType(
            "com.google.cloud.dataflow.sdk.options.BlockingDataflowPipelineOptions"))
        .thenReturn(
            ImmutableMap.of(
                new PipelineOptionsType(
                    "MyOptions", Collections.<PipelineOptionsType>emptySet(), properties),
                properties));

    String mode = "run";
    ILaunch launch = mock(ILaunch.class);

    try {
      dataflowDelegate.launch(configuration, mode, launch, monitor);
      fail();
    } catch (IllegalArgumentException ex) {
      assertTrue(ex.getMessage().contains("Dataflow Pipeline Configuration is not valid"));
    }
  }

  @Test
  public void testLaunchWithProjectThatDoesNotExistThrowsCoreException() throws CoreException {
    ILaunchConfiguration configuration = mockILaunchConfiguration();
    when(project.exists()).thenReturn(false);

    String mode = "run";
    ILaunch launch = mock(ILaunch.class);

    try {
      dataflowDelegate.launch(configuration, mode, launch, monitor);
      fail();
    } catch (CoreException ex) {
      assertTrue(
          ex.getMessage().contains("Project \"Test-project,Name\" does not exist"));
    }
  }

  @Test
  public void testLaunchWithValidLaunchConfigurationCreatesJsonCredential()
      throws CoreException, IOException {
    ILaunchConfiguration configuration = mockILaunchConfiguration();
    when(configuration.getAttribute(
        "com.google.cloud.dataflow.eclipse.ALL_ARGUMENT_VALUES", new HashMap<String, String>()))
        .thenReturn(ImmutableMap.of("accountEmail", "[email protected]"));

    when(configuration.copy("dataflow_tmp_config_working_copy-testConfiguration"))
        .thenReturn(configurationWorkingCopy);

    WritableDataflowPreferences globalPreferences = WritableDataflowPreferences.global();
    globalPreferences.setDefaultAccountEmail("[email protected]");
    globalPreferences.save();

    dataflowDelegate.launch(configuration, "run" /* mode */, mock(ILaunch.class), monitor);
    verifyLoginAccountSet();
  }

  @Test
  public void testLaunchWithValidLaunchConfigurationSendsWorkingCopyToLaunchDelegate()
      throws CoreException {
    ILaunchConfiguration configuration = mockILaunchConfiguration();

    when(
        pipelineOptionsHierarchy.getPropertyNames(
            "com.google.cloud.dataflow.sdk.options.BlockingDataflowPipelineOptions"))
        .thenReturn(ImmutableSet.<String>of("foo", "bar", "baz", "Extra"));

    Map<String, String> argumentValues =
        ImmutableMap.<String, String>builder()
            .put("foo", "Spam")
            .put("bar", "Eggs")
            .put("baz", "Ham")
            .put("Extra", "PipelineArgs")
            .put("NotUsedInThisRunner", "Ignored")
            .put("accountEmail", "[email protected]")
            .build();

    when(
        configuration.getAttribute(
            PipelineConfigurationAttr.ALL_ARGUMENT_VALUES.toString(),
            Collections.<String, String>emptyMap())).thenReturn(argumentValues);

    String javaArgs = "ExtraJavaArgs";
    when(configuration.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, ""))
        .thenReturn(javaArgs);
    when(configuration.copy("dataflow_tmp_config_working_copy-testConfiguration"))
        .thenReturn(configurationWorkingCopy);

    ILaunch launch = mock(ILaunch.class);

    dataflowDelegate.launch(configuration, "run", launch, monitor);

    Set<String> expectedArgumentComponents = ImmutableSet.of(
        "--runner=BlockingDataflowPipelineRunner", "--foo=Spam", "--bar=Eggs", "--baz=Ham",
        "--Extra=PipelineArgs", "ExtraJavaArgs");

    ArgumentCaptor<String> programArgumentsCaptor = ArgumentCaptor.forClass(String.class);

    verify(javaDelegate)
        .launch(eq(configurationWorkingCopy), eq("run"), eq(launch), any(IProgressMonitor.class));
    verify(configurationWorkingCopy)
        .setAttribute(
            eq(IJavaLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS),
            programArgumentsCaptor.capture());

    String providedArguments = programArgumentsCaptor.getValue();
    String[] argumentComponents = providedArguments.split(" ");
    assertEquals(expectedArgumentComponents.size(), argumentComponents.length);
    assertTrue(expectedArgumentComponents.containsAll(Arrays.asList(argumentComponents)));
  }

  @Test
  public void testLaunchWithEmptyArgumentsDoesNotPassEmptyArguments() throws CoreException {
    ILaunchConfiguration configuration = mockILaunchConfiguration();

    when(
        pipelineOptionsHierarchy.getPropertyNames(
            "com.google.cloud.dataflow.sdk.options.BlockingDataflowPipelineOptions"))
        .thenReturn(ImmutableSet.of("foo", "bar", "baz", "Extra", "Empty"));

    // Need an order-preserving null-accepting map
    Map<String, String> argumentValues = new LinkedHashMap<>();
    argumentValues.put("foo", "Spam");
    argumentValues.put("bar", "Eggs");
    argumentValues.put("baz", "Ham");
    argumentValues.put("Extra", "PipelineArgs");
    argumentValues.put("NotUsedInThisRunner", "Ignored");
    argumentValues.put("Empty", "");
    argumentValues.put("accountEmail", "[email protected]");
    argumentValues.put("Null", null);

    when(
        configuration.getAttribute(
            PipelineConfigurationAttr.ALL_ARGUMENT_VALUES.toString(),
            Collections.<String, String>emptyMap())).thenReturn(argumentValues);

    String javaArgs = "ExtraJavaArgs";
    when(configuration.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, ""))
        .thenReturn(javaArgs);
    when(configuration.copy("dataflow_tmp_config_working_copy-testConfiguration"))
        .thenReturn(configurationWorkingCopy);

    ILaunch launch = mock(ILaunch.class);

    dataflowDelegate.launch(configuration, "run", launch, monitor);

    Set<String> expectedArgumentComponents = ImmutableSet.of(
        "--runner=BlockingDataflowPipelineRunner", "--foo=Spam", "--bar=Eggs", "--baz=Ham",
        "--Extra=PipelineArgs", "ExtraJavaArgs");

    ArgumentCaptor<String> programArgumentsCaptor = ArgumentCaptor.forClass(String.class);

    verify(javaDelegate)
        .launch(eq(configurationWorkingCopy), eq("run"), eq(launch), any(IProgressMonitor.class));
    verify(configurationWorkingCopy)
        .setAttribute(
            eq(IJavaLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS),
            programArgumentsCaptor.capture());

    String[] argumentComponents = programArgumentsCaptor.getValue().split(" ");
    assertEquals(expectedArgumentComponents.size(), argumentComponents.length);
    assertTrue(expectedArgumentComponents.containsAll(Arrays.asList(argumentComponents)));
  }

  private static PipelineOptionsProperty requiredProperty(String name) {
    return new PipelineOptionsProperty(name, false, true, Collections.<String>emptySet(), null);
  }

  private ILaunchConfiguration mockILaunchConfiguration() throws CoreException {
    ILaunchConfiguration configuration = mock(ILaunchConfiguration.class);
    String configurationName = "testConfiguration";
    when(configuration.getName()).thenReturn(configurationName);

    PipelineRunner runner = PipelineRunner.BLOCKING_DATAFLOW_PIPELINE_RUNNER;
    when(configuration.getAttribute(eq(PipelineConfigurationAttr.RUNNER_ARGUMENT.toString()),
        anyString())).thenReturn(runner.getRunnerName());

    String projectName = "Test-project,Name";
    when(configuration.getAttribute(eq(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME),
        anyString())).thenReturn(projectName);
    when(workspaceRoot.getProject(projectName)).thenReturn(project);
    when(project.exists()).thenReturn(true);

    return configuration;
  }
}