/*
 * 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.jclouds.compute.callables;
import static org.easymock.EasyMock.createMockBuilder;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.jclouds.compute.callables.BlockUntilInitScriptStatusIsZeroThenReturnOutput.loopUntilTrueOrThrowCancellationException;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.fail;

import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;

import org.jclouds.compute.callables.BlockUntilInitScriptStatusIsZeroThenReturnOutput.ExitStatusOfCommandGreaterThanZero;
import org.jclouds.compute.domain.ExecResponse;
import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.compute.domain.NodeMetadataBuilder;
import org.jclouds.scriptbuilder.InitScript;
import org.testng.annotations.Test;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.eventbus.EventBus;
import com.google.common.util.concurrent.AbstractFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;

@Test(groups = "unit", singleThreaded = true, testName = "BlockUntilInitScriptStatusIsZeroThenReturnOutputTest")
public class BlockUntilInitScriptStatusIsZeroThenReturnOutputTest {

   public void testloopUntilTrueOrThrowCancellationExceptionReturnsWhenPredicateIsTrue() {
      AbstractFuture<ExecResponse> future = new AbstractFuture<ExecResponse>() {

         @Override
         public boolean isCancelled() {
            return false;
         }

      };

      Predicate<String> pred = loopUntilTrueOrThrowCancellationException(Predicates
               .<String> alwaysTrue(), 1, 1, future);
      assertEquals(pred.apply("foo"), true);

   }

   public void testloopUntilTrueOrThrowCancellationExceptionReturnsWhenPredicateIsTrueSecondTimeWhileNotCancelled() {
      AbstractFuture<ExecResponse> future = new AbstractFuture<ExecResponse>() {

         @Override
         public boolean isCancelled() {
            return false;
         }

      };

      Predicate<String> predicate = new Predicate<String>() {
         AtomicBoolean bool = new AtomicBoolean();

         @Override
         public boolean apply(String input) {
            return bool.getAndSet(true);
         }

      };

      Predicate<String> pred = loopUntilTrueOrThrowCancellationException(predicate, 1, 1, future);
      assertEquals(pred.apply("foo"), true);

   }

   // need to break the loop when cancelled.
   public void testloopUntilTrueOrThrowCancellationExceptionSkipsAndReturnsFalseOnCancelled() {
      AbstractFuture<ExecResponse> future = new AbstractFuture<ExecResponse>() {

         @Override
         public boolean isCancelled() {
            return true;
         }

      };
      Predicate<String> pred = loopUntilTrueOrThrowCancellationException(Predicates.<String> alwaysFalse(), 1, 1,
            future);
      assertEquals(pred.apply("foo"), false);

   }

   public void testExitStatusOfCommandGreaterThanZeroTrueWhen1() {

      SudoAwareInitManager commandRunner = createMockBuilder(SudoAwareInitManager.class).addMockedMethod("runAction")
               .createStrictMock();
      expect(commandRunner.runAction("status")).andReturn(new ExecResponse("", "", 1));
      replay(commandRunner);

      Predicate<String> pred = new ExitStatusOfCommandGreaterThanZero(commandRunner);
      assertEquals(pred.apply("status"), true);

      verify(commandRunner);

   }

   public void testExitStatusOfCommandGreaterThanZeroFalseWhen0() {

      SudoAwareInitManager commandRunner = createMockBuilder(SudoAwareInitManager.class).addMockedMethod("runAction")
               .createStrictMock();
      expect(commandRunner.runAction("status")).andReturn(new ExecResponse("", "", 0));
      replay(commandRunner);

      Predicate<String> pred = new ExitStatusOfCommandGreaterThanZero(commandRunner);
      assertEquals(pred.apply("status"), false);

      verify(commandRunner);

   }

   EventBus eventBus = new EventBus();

   public void testExitStatusZeroReturnsExecResponse() throws InterruptedException, ExecutionException {
      ListeningExecutorService userExecutor = MoreExecutors.newDirectExecutorService();
      Predicate<String> notRunningAnymore = Predicates.alwaysTrue();
      SudoAwareInitManager commandRunner = createMockBuilder(SudoAwareInitManager.class).addMockedMethod("runAction")
               .addMockedMethod("getStatement").addMockedMethod("getNode").addMockedMethod("toString")
               .createStrictMock();
      InitScript initScript = createMockBuilder(InitScript.class).addMockedMethod("getInstanceName").createStrictMock();

      expect(commandRunner.runAction("stdout")).andReturn(new ExecResponse("stdout", "", 0));
      expect(commandRunner.runAction("stderr")).andReturn(new ExecResponse("stderr", "", 0));
      expect(commandRunner.runAction("exitstatus")).andReturn(new ExecResponse("444\n", "", 0));

      toStringAndEventBusExpectations(commandRunner, initScript);

      replay(commandRunner, initScript);

      BlockUntilInitScriptStatusIsZeroThenReturnOutput future = new BlockUntilInitScriptStatusIsZeroThenReturnOutput(
               userExecutor, eventBus, notRunningAnymore, commandRunner);

      future.run();

      assertEquals(future.get(), new ExecResponse("stdout", "stderr", 444));

      verify(commandRunner, initScript);

   }

   public void testFirstExitStatusOneButSecondExitStatusZeroReturnsExecResponse() throws InterruptedException,
            ExecutionException {
      ListeningExecutorService userExecutor = MoreExecutors.newDirectExecutorService();
      Predicate<String> notRunningAnymore = Predicates.alwaysTrue();

      SudoAwareInitManager commandRunner = createMockBuilder(SudoAwareInitManager.class).addMockedMethod("runAction")
               .addMockedMethod("getStatement").addMockedMethod("getNode").addMockedMethod("toString")
               .createStrictMock();
      InitScript initScript = createMockBuilder(InitScript.class).addMockedMethod("getInstanceName").createStrictMock();

      // exit status is 1 means we are still running!
      expect(commandRunner.runAction("stdout")).andReturn(new ExecResponse("", "", 0));
      expect(commandRunner.runAction("stderr")).andReturn(new ExecResponse("", "", 0));
      expect(commandRunner.runAction("exitstatus")).andReturn(new ExecResponse("", "", 1));

      // second time around, it did stop
      expect(commandRunner.runAction("stdout")).andReturn(new ExecResponse("stdout", "", 0));
      expect(commandRunner.runAction("stderr")).andReturn(new ExecResponse("stderr", "", 0));
      expect(commandRunner.runAction("exitstatus")).andReturn(new ExecResponse("444\n", "", 0));

      toStringAndEventBusExpectations(commandRunner, initScript);

      replay(commandRunner, initScript);

      BlockUntilInitScriptStatusIsZeroThenReturnOutput future = new BlockUntilInitScriptStatusIsZeroThenReturnOutput(
               userExecutor, eventBus, notRunningAnymore, commandRunner);

      future.run();

      assertEquals(future.get(), new ExecResponse("stdout", "stderr", 444));

      verify(commandRunner, initScript);

   }

   public void testCancelInterruptStopsCommand() throws InterruptedException, ExecutionException {
      ListeningExecutorService userExecutor = MoreExecutors.newDirectExecutorService();
      Predicate<String> notRunningAnymore = Predicates.alwaysTrue();
      SudoAwareInitManager commandRunner = createMockBuilder(SudoAwareInitManager.class).addMockedMethod(
               "refreshAndRunAction").addMockedMethod("runAction").addMockedMethod("getStatement").addMockedMethod(
               "getNode").addMockedMethod("toString").createStrictMock();
      InitScript initScript = createMockBuilder(InitScript.class).addMockedMethod("getInstanceName").createStrictMock();

      // log what we are stopping
      expect(commandRunner.getStatement()).andReturn(initScript);
      expect(initScript.getInstanceName()).andReturn("init-script");

      // stop
      expect(commandRunner.refreshAndRunAction("stop")).andReturn(new ExecResponse("stdout", "", 0));

      // create cancellation exception
      expect(commandRunner.getStatement()).andReturn(initScript);
      expect(initScript.getInstanceName()).andReturn("init-script");
      expect(commandRunner.getNode()).andReturn(
               new NodeMetadataBuilder().ids("id").status(NodeMetadata.Status.RUNNING).build()).atLeastOnce();

      // StatementOnNodeFailure event
      expect(commandRunner.getStatement()).andReturn(initScript);
      expect(commandRunner.getNode()).andReturn(
               new NodeMetadataBuilder().ids("id").status(NodeMetadata.Status.RUNNING).build()).atLeastOnce();

      replay(commandRunner, initScript);

      BlockUntilInitScriptStatusIsZeroThenReturnOutput future = new BlockUntilInitScriptStatusIsZeroThenReturnOutput(
               userExecutor, eventBus, notRunningAnymore, commandRunner);

      future.cancel(true);

      try {
         future.get();
         fail();
      } catch (CancellationException e) {

      }

      verify(commandRunner, initScript);

   }

   public void testCancelDontInterruptLeavesCommandRunningAndReturnsLastStatus() throws InterruptedException,
            ExecutionException {
      ListeningExecutorService userExecutor = MoreExecutors.newDirectExecutorService();
      Predicate<String> notRunningAnymore = Predicates.alwaysTrue();
      SudoAwareInitManager commandRunner = createMockBuilder(SudoAwareInitManager.class).addMockedMethod("runAction")
               .addMockedMethod("getStatement").addMockedMethod("getNode").addMockedMethod("toString")
               .createStrictMock();
      InitScript initScript = createMockBuilder(InitScript.class).addMockedMethod("getInstanceName").createStrictMock();

      expect(commandRunner.runAction("stdout")).andReturn(new ExecResponse("stillrunning", "", 0));
      expect(commandRunner.runAction("stderr")).andReturn(new ExecResponse("", "", 0));
      expect(commandRunner.runAction("exitstatus")).andReturn(new ExecResponse("", "", 1));

      toStringAndEventBusExpectations(commandRunner, initScript);

      replay(commandRunner, initScript);

      BlockUntilInitScriptStatusIsZeroThenReturnOutput future = new BlockUntilInitScriptStatusIsZeroThenReturnOutput(
               userExecutor, eventBus, notRunningAnymore, commandRunner);

      future.cancel(false);

      // note if this didn't cancel properly, the loop would never end!
      future.run();

      try {
         future.get();
         fail();
      } catch (CancellationException e) {

      }
      verify(commandRunner, initScript);

   }

   private void toStringAndEventBusExpectations(SudoAwareInitManager commandRunner, InitScript initScript) {
      toStringExpectations(commandRunner, initScript);
      expect(commandRunner.getStatement()).andReturn(initScript);
      expect(commandRunner.getNode()).andReturn(
               new NodeMetadataBuilder().ids("id").status(NodeMetadata.Status.RUNNING).build());
   }

   private void toStringExpectations(SudoAwareInitManager commandRunner, InitScript initScript) {
      expect(commandRunner.getStatement()).andReturn(initScript);
      expect(initScript.getInstanceName()).andReturn("init-script");
   }
}
