/*
 * SonarQube
 * Copyright (C) 2009-2023 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonar.ce.app;

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.DisableOnDebug;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.sonar.ce.ComputeEngine;
import org.sonar.process.MessageException;
import org.sonar.process.MinimumViableSystem;
import org.sonar.process.Monitored;

import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

@RunWith(DataProviderRunner.class)
public class CeServerTest {
  @Rule
  public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));

  private CeServer underTest = null;
  private Thread waitingThread = null;
  private MinimumViableSystem minimumViableSystem = mock(MinimumViableSystem.class, Mockito.RETURNS_MOCKS);
  private CeSecurityManager ceSecurityManager = mock(CeSecurityManager.class);

  @After
  public void tearDown() throws Exception {
    if (underTest != null) {
      underTest.hardStop();
    }
    Thread waitingThread = this.waitingThread;
    this.waitingThread = null;
    if (waitingThread != null) {
      waitingThread.join();
    }
  }

  @Test
  public void constructor_does_not_start_a_new_Thread() {
    int activeCount = Thread.activeCount();
    newCeServer();
    assertThat(Thread.activeCount()).isSameAs(activeCount);
  }

  @Test
  public void constructor_calls_ceSecurityManager() {
    newCeServer();
    verify(ceSecurityManager).apply();
  }

  @Test
  public void awaitStop_throws_ISE_if_called_before_start() {
    CeServer ceServer = newCeServer();

    assertThatThrownBy(ceServer::awaitStop)
      .isInstanceOf(IllegalStateException.class)
      .hasMessage("awaitStop() must not be called before start()");
  }

  @Test
  public void start_starts_a_new_Thread() {
    int activeCount = Thread.activeCount();

    newCeServer().start();

    assertThat(Thread.activeCount()).isSameAs(activeCount + 1);
  }

  @Test
  public void stop_stops_Thread() {
    CeServer ceServer = newCeServer();
    assertThat(ceThreadExists()).isFalse();
    ceServer.start();
    assertThat(ceThreadExists()).isTrue();
    ceServer.stop();
    await().atMost(5, TimeUnit.SECONDS).until(() -> !ceThreadExists());
  }

  @Test
  public void stop_dontDoAnythingIfThreadDoesntExist() {
    CeServer ceServer = newCeServer();
    assertThat(ceThreadExists()).isFalse();

    ceServer.stop();

    //expect no exception and thread still doesn't exist
    assertThat(ceThreadExists()).isFalse();
  }

  private static boolean ceThreadExists() {
    return Thread.getAllStackTraces().keySet().stream().anyMatch(t -> t.getName().equals("ce-main"));
  }

  @Test
  public void start_throws_ISE_when_called_twice() {
    CeServer ceServer = newCeServer();

    ceServer.start();

    assertThatThrownBy(ceServer::start)
      .isInstanceOf(IllegalStateException.class)
      .hasMessage("start() can not be called twice");
  }

  @Test
  public void getStatus_throws_ISE_when_called_before_start() {
    CeServer ceServer = newCeServer();

    assertThatThrownBy(ceServer::getStatus)
      .isInstanceOf(IllegalStateException.class)
      .hasMessage("getStatus() can not be called before start()");
  }

  @Test
  public void getStatus_does_not_return_OPERATIONAL_until_ComputeEngine_startup_returns() {
    BlockingStartupComputeEngine computeEngine = new BlockingStartupComputeEngine(null);
    CeServer ceServer = newCeServer(computeEngine);

    ceServer.start();

    assertThat(ceServer.getStatus()).isEqualTo(Monitored.Status.DOWN);

    // release ComputeEngine startup method
    computeEngine.releaseStartup();

    await().atMost(5, TimeUnit.SECONDS).until(() -> ceServer.getStatus() == Monitored.Status.OPERATIONAL);
  }

  @Test
  @UseDataProvider("exceptions")
  public void getStatus_returns_FAILED_when_ComputeEngine_startup_throws_any_Exception_or_Error(RuntimeException exception) {
    BlockingStartupComputeEngine computeEngine = new BlockingStartupComputeEngine(exception);
    CeServer ceServer = newCeServer(computeEngine);

    ceServer.start();

    assertThat(ceServer.getStatus()).isEqualTo(Monitored.Status.DOWN);

    // release ComputeEngine startup method which will throw startupException
    computeEngine.releaseStartup();

    await().atMost(5, TimeUnit.SECONDS).until(() -> ceServer.getStatus() == Monitored.Status.FAILED);
  }

  @DataProvider
  public static Object[] exceptions() {
    return new Object[] {new MessageException("exception"), new IllegalStateException("Faking failing ComputeEngine#startup()")};
  }

  @Test
  public void awaitStop_keeps_blocking_calling_thread_even_if_calling_thread_is_interrupted_but_until_stop_is_called() throws Exception {
    final CeServer ceServer = newCeServer();
    Thread waitingThread = newWaitingThread(ceServer::awaitStop);

    ceServer.start();
    waitingThread.start();

    // interrupts waitingThread 5 times in a row (we really insist)
    for (int i = 0; i < 5; i++) {
      waitingThread.interrupt();
      Thread.sleep(5);
      assertThat(waitingThread.isAlive()).isTrue();
    }

    ceServer.hardStop();
    // wait for waiting thread to stop because we stopped ceServer
    // if it does not, the test will fail with timeout
    waitingThread.join();
  }

  @Test
  public void awaitStop_unblocks_when_waiting_for_ComputeEngine_startup_fails() {
    CeServer ceServer = newCeServer(new ComputeEngine() {
      @Override
      public void startup() {
        throw new Error("Faking ComputeEngine.startup() failing");
      }

      @Override
      public void stopProcessing() {
        throw new UnsupportedOperationException("stopProcessing() should never be called in this test");
      }

      @Override
      public void shutdown() {
        throw new UnsupportedOperationException("shutdown() should never be called in this test");
      }
    });

    ceServer.start();
    // if awaitStop does not unblock, the test will fail with timeout
    ceServer.awaitStop();
  }

  @Test
  public void staticMain_withoutAnyArguments_expectException() {
    String[] emptyArray = {};

    assertThatThrownBy(() -> CeServer.main(emptyArray))
      .hasMessage("Only a single command-line argument is accepted (absolute path to configuration file)");
  }

  @Test
  public void stop_releases_thread_in_awaitStop_even_when_ComputeEngine_shutdown_fails() throws InterruptedException {
    final CeServer ceServer = newCeServer(new ComputeEngine() {
      @Override
      public void startup() {
        // nothing to do at startup
      }

      @Override
      public void stopProcessing() {
        throw new UnsupportedOperationException("stopProcessing should not be called in this test");
      }

      @Override
      public void shutdown() {
        throw new Error("Faking ComputeEngine.shutdown() failing");
      }
    });
    Thread waitingThread = newWaitingThread(ceServer::awaitStop);

    ceServer.start();
    waitingThread.start();
    ceServer.hardStop();
    // wait for waiting thread to stop because we stopped ceServer
    // if it does not, the test will fail with timeout
    waitingThread.join();
  }

  private CeServer newCeServer() {
    return newCeServer(mock(ComputeEngine.class));
  }

  private CeServer newCeServer(ComputeEngine computeEngine) {
    checkState(this.underTest == null, "Only one CeServer can be created per test method");
    this.underTest = new CeServer(computeEngine, minimumViableSystem, ceSecurityManager);
    return underTest;
  }

  private Thread newWaitingThread(Runnable runnable) {
    Thread t = new Thread(runnable);
    checkState(this.waitingThread == null, "Only one waiting thread can be created per test method");
    this.waitingThread = t;
    return t;
  }

  private static class BlockingStartupComputeEngine implements ComputeEngine {
    private final CountDownLatch latch = new CountDownLatch(1);
    @CheckForNull
    private final RuntimeException throwable;

    public BlockingStartupComputeEngine(@Nullable RuntimeException throwable) {
      this.throwable = throwable;
    }

    @Override
    public void startup() {
      try {
        latch.await(1000, MILLISECONDS);
      } catch (InterruptedException e) {
        throw new RuntimeException("await failed", e);
      }
      if (throwable != null) {
        throw throwable;
      }
    }

    @Override
    public void stopProcessing() {
      // do nothing
    }

    @Override
    public void shutdown() {
      // do nothing
    }

    private void releaseStartup() {
      this.latch.countDown();
    }
  }
}
