/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2017 by Hitachi Vantara : http://www.pentaho.com
 *
 *******************************************************************************
 *
 * 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.pentaho.di.core;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import com.google.common.util.concurrent.SettableFuture;
import org.junit.Test;
import org.pentaho.di.core.annotations.KettleLifecyclePlugin;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.lifecycle.KettleLifecycleListener;
import org.pentaho.di.core.lifecycle.LifecycleException;
import org.pentaho.di.core.plugins.KettleLifecyclePluginType;
import org.pentaho.di.core.plugins.PluginInterface;
import org.pentaho.di.core.plugins.PluginRegistry;
import org.pentaho.di.core.variables.Variables;
import org.pentaho.di.core.vfs.KettleVFS;

/**
 * Tests for the Kettle Environment
 * 
 */
public class KettleEnvironmentIT {

  private static final AtomicBoolean environmentInitCalled = new AtomicBoolean( false );
  private static final String pluginId = "MockLifecycleListener";

  @KettleLifecyclePlugin( id = pluginId )
  public static class MockLifecycleListener implements KettleLifecycleListener {
    @Override
    public void onEnvironmentInit() throws LifecycleException {
      environmentInitCalled.set( true );
    }

    @Override
    public void onEnvironmentShutdown() {
    }
  }

  @KettleLifecyclePlugin( id = pluginId )
  public static class FailingMockLifecycleListener extends MockLifecycleListener {
    @Override
    public void onEnvironmentInit() throws LifecycleException {
      throw new LifecycleException( false );
    }
  }

  @KettleLifecyclePlugin( id = pluginId )
  public static class SevereFailingMockLifecycleListener extends MockLifecycleListener {
    @Override
    public void onEnvironmentInit() throws LifecycleException {
      throw new LifecycleException( true );
    }
  }

  @KettleLifecyclePlugin( id = pluginId )
  public static class ThrowableFailingMockLifecycleListener extends MockLifecycleListener {
    @Override
    public void onEnvironmentInit() throws LifecycleException {
      // Simulate a LifecycleListener that wasn't updated to the latest API
      throw new AbstractMethodError();
    }
  }

  private void resetKettleEnvironmentInitializationFlag() throws SecurityException, NoSuchFieldException,
    IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException,
    InstantiationException {
    Field f = KettleEnvironment.class.getDeclaredField( "initialized" );
    f.setAccessible( true );
    f.set( null, new AtomicReference<SettableFuture<Boolean>>( null ) );
    Constructor<KettleVFS> constructor;
    constructor = KettleVFS.class.getDeclaredConstructor();
    constructor.setAccessible( true );
    KettleVFS KVFS = constructor.newInstance();
    f = KVFS.getClass().getDeclaredField( "kettleVFS" );
    f.setAccessible( true );
    Field modifiersField = Field.class.getDeclaredField( "modifiers" );
    modifiersField.setAccessible( true );
    modifiersField.setInt( f, f.getModifiers() & ~Modifier.FINAL | Modifier.VOLATILE );
    f.set( null, KVFS );
    f = KVFS.getClass().getDeclaredField( "defaultVariableSpace" );
    f.setAccessible( true );
    modifiersField.setInt( f, f.getModifiers() & ~Modifier.FINAL );
    Variables var = new Variables();
    var.initializeVariablesFrom( null );
    f.set( null, var );

  }

  /**
   * Validate that a LifecycleListener's environment init callback is called when the Kettle Environment is initialized.
   */
  @Test
  public void lifecycleListenerEnvironmentInitCallback() throws Exception {
    resetKettleEnvironmentInitializationFlag();
    assertFalse( "This test only works if the Kettle Environment is not yet initialized", KettleEnvironment
        .isInitialized() );
    System.setProperty( Const.KETTLE_PLUGIN_CLASSES, MockLifecycleListener.class.getName() );
    KettleEnvironment.init();

    PluginInterface pi = PluginRegistry.getInstance().findPluginWithId( KettleLifecyclePluginType.class, pluginId );
    MockLifecycleListener l =
        (MockLifecycleListener) PluginRegistry.getInstance().loadClass( pi, KettleLifecycleListener.class );
    assertNotNull( "Test plugin not registered properly", l );

    assertTrue( environmentInitCalled.get() );
  }

  /**
   * Validate that a LifecycleListener's environment init callback is called when the Kettle Environment is initialized.
   */
  @Test
  public void lifecycleListenerEnvironmentInitCallback_exception_thrown() throws Exception {
    resetKettleEnvironmentInitializationFlag();
    assertFalse( "This test only works if the Kettle Environment is not yet initialized", KettleEnvironment
        .isInitialized() );
    System.setProperty( Const.KETTLE_PLUGIN_CLASSES, FailingMockLifecycleListener.class.getName() );
    KettleEnvironment.init();

    PluginInterface pi = PluginRegistry.getInstance().findPluginWithId( KettleLifecyclePluginType.class, pluginId );
    MockLifecycleListener l =
        (MockLifecycleListener) PluginRegistry.getInstance().loadClass( pi, KettleLifecycleListener.class );
    assertNotNull( "Test plugin not registered properly", l );

    assertTrue( KettleEnvironment.isInitialized() );
  }

  /**
   * Validate that a LifecycleListener's environment init callback is called when the Kettle Environment is initialized.
   */
  @Test
  public void lifecycleListenerEnvironmentInitCallback_exception_thrown_severe() throws Exception {
    resetKettleEnvironmentInitializationFlag();
    assertFalse( "This test only works if the Kettle Environment is not yet initialized", KettleEnvironment
        .isInitialized() );
    System.setProperty( Const.KETTLE_PLUGIN_CLASSES, SevereFailingMockLifecycleListener.class.getName() );
    try {
      KettleEnvironment.init();
      fail( "Expected exception" );
    } catch ( KettleException ex ) {
      assertEquals( LifecycleException.class, ex.getCause().getClass() );
      ex.printStackTrace();
    }

    assertFalse( KettleEnvironment.isInitialized() );
  }

  @Test
  public void lifecycleListenerEnvironmentInitCallback_throwable_thrown() throws Exception {
    resetKettleEnvironmentInitializationFlag();
    assertFalse( "This test only works if the Kettle Environment is not yet initialized", KettleEnvironment
        .isInitialized() );
    System.setProperty( Const.KETTLE_PLUGIN_CLASSES, ThrowableFailingMockLifecycleListener.class.getName() );
    try {
      KettleEnvironment.init();
      fail( "Expected exception" );
    } catch ( KettleException ex ) {
      assertEquals( AbstractMethodError.class, ex.getCause().getClass() );
      ex.printStackTrace();
    }

    assertFalse( KettleEnvironment.isInitialized() );
  }

}
