/*
 * 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.server.platform.db.migration.step;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.slf4j.event.Level;
import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.core.platform.SpringComponentContainer;
import org.sonar.server.platform.db.migration.engine.MigrationContainer;
import org.sonar.server.platform.db.migration.engine.SimpleMigrationContainer;
import org.sonar.server.platform.db.migration.history.MigrationHistory;

import static com.google.common.base.Preconditions.checkState;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.mock;

public class MigrationStepsExecutorImplTest {
  @Rule
  public LogTester logTester = new LogTester();

  private MigrationContainer migrationContainer = new SimpleMigrationContainer();
  private MigrationHistory migrationHistor = mock(MigrationHistory.class);
  private MigrationStepsExecutorImpl underTest = new MigrationStepsExecutorImpl(migrationContainer, migrationHistor);

  @Test
  public void execute_does_not_fail_when_stream_is_empty_and_log_start_stop_INFO() {
    underTest.execute(Collections.emptyList());

    assertThat(logTester.logs()).hasSize(2);
    assertLogLevel(Level.INFO, "Executing DB migrations...", "Executed DB migrations: success | time=");
  }

  @Test
  public void execute_fails_with_ISE_if_no_instance_of_computation_step_exist_in_container() {
    List<RegisteredMigrationStep> steps = asList(registeredStepOf(1, MigrationStep1.class));

    ((SpringComponentContainer) migrationContainer).startComponents();
    try {
      underTest.execute(steps);
      fail("execute should have thrown a IllegalStateException");
    } catch (IllegalStateException e) {
      assertThat(e).hasMessage("Unable to load component " + MigrationStep1.class);
    } finally {
      assertThat(logTester.logs()).hasSize(2);
      assertLogLevel(Level.INFO, "Executing DB migrations...");
      assertLogLevel(Level.ERROR, "Executed DB migrations: failure | time=");
    }
  }

  private void assertLogLevel(Level level, String... expected) {
    List<String> logs = logTester.logs(level);
    assertThat(logs).hasSize(expected.length);
    Iterator<String> iterator = logs.iterator();
    Arrays.stream(expected).forEachOrdered(log -> {
      if (log.endsWith(" | time=")) {
        assertThat(iterator.next()).startsWith(log);
      } else {
        assertThat(iterator.next()).isEqualTo(log);
      }
    });
  }

  @Test
  public void execute_execute_the_instance_of_type_specified_in_step_in_stream_order() {
    migrationContainer.add(MigrationStep1.class, MigrationStep2.class, MigrationStep3.class);
    ((SpringComponentContainer) migrationContainer).startComponents();

    underTest.execute(asList(
      registeredStepOf(1, MigrationStep2.class),
      registeredStepOf(2, MigrationStep1.class),
      registeredStepOf(3, MigrationStep3.class)));

    assertThat(SingleCallCheckerMigrationStep.calledSteps)
      .containsExactly(MigrationStep2.class, MigrationStep1.class, MigrationStep3.class);
    assertThat(logTester.logs()).hasSize(8);
    assertLogLevel(Level.INFO,
      "Executing DB migrations...",
      "#1 '1-MigrationStep2'...",
      "#1 '1-MigrationStep2': success | time=",
      "#2 '2-MigrationStep1'...",
      "#2 '2-MigrationStep1': success | time=",
      "#3 '3-MigrationStep3'...",
      "#3 '3-MigrationStep3': success | time=",
      "Executed DB migrations: success | time=");

    assertThat(migrationContainer.getComponentByType(MigrationStep1.class).isCalled()).isTrue();
    assertThat(migrationContainer.getComponentByType(MigrationStep2.class).isCalled()).isTrue();
    assertThat(migrationContainer.getComponentByType(MigrationStep3.class).isCalled()).isTrue();
  }

  @Test
  public void execute_throws_MigrationStepExecutionException_on_first_failing_step_execution_throws_SQLException() {
    migrationContainer.add(MigrationStep2.class, SqlExceptionFailingMigrationStep.class, MigrationStep3.class);
    List<RegisteredMigrationStep> steps = asList(
      registeredStepOf(1, MigrationStep2.class),
      registeredStepOf(2, SqlExceptionFailingMigrationStep.class),
      registeredStepOf(3, MigrationStep3.class));

    ((SpringComponentContainer) migrationContainer).startComponents();
    try {
      underTest.execute(steps);
      fail("a MigrationStepExecutionException should have been thrown");
    } catch (MigrationStepExecutionException e) {
      assertThat(e).hasMessage("Execution of migration step #2 '2-SqlExceptionFailingMigrationStep' failed");
      assertThat(e).hasCause(SqlExceptionFailingMigrationStep.THROWN_EXCEPTION);
    } finally {
      assertThat(logTester.logs()).hasSize(6);
      assertLogLevel(Level.INFO,
        "Executing DB migrations...",
        "#1 '1-MigrationStep2'...",
        "#1 '1-MigrationStep2': success | time=",
        "#2 '2-SqlExceptionFailingMigrationStep'...");
      assertLogLevel(Level.ERROR,
        "#2 '2-SqlExceptionFailingMigrationStep': failure | time=",
        "Executed DB migrations: failure | time=");
    }
  }

  @Test
  public void execute_throws_MigrationStepExecutionException_on_first_failing_step_execution_throws_any_exception() {
    migrationContainer.add(MigrationStep2.class, RuntimeExceptionFailingMigrationStep.class, MigrationStep3.class);

    List<RegisteredMigrationStep> steps = asList(
      registeredStepOf(1, MigrationStep2.class),
      registeredStepOf(2, RuntimeExceptionFailingMigrationStep.class),
      registeredStepOf(3, MigrationStep3.class));

    ((SpringComponentContainer) migrationContainer).startComponents();
    try {
      underTest.execute(steps);
      fail("should throw MigrationStepExecutionException");
    } catch (MigrationStepExecutionException e) {
      assertThat(e).hasMessage("Execution of migration step #2 '2-RuntimeExceptionFailingMigrationStep' failed");
      assertThat(e.getCause()).isSameAs(RuntimeExceptionFailingMigrationStep.THROWN_EXCEPTION);
    }
  }

  private static RegisteredMigrationStep registeredStepOf(int migrationNumber, Class<? extends MigrationStep> migrationStep1Class) {
    return new RegisteredMigrationStep(migrationNumber, migrationNumber + "-" + migrationStep1Class.getSimpleName(), migrationStep1Class);
  }

  private static abstract class SingleCallCheckerMigrationStep implements MigrationStep {
    private static List<Class<? extends MigrationStep>> calledSteps = new ArrayList<>();
    private boolean called = false;

    @Override
    public void execute() {
      checkState(!called, "execute must not be called twice");
      this.called = true;
      calledSteps.add(getClass());
    }

    public boolean isCalled() {
      return called;
    }

    public static List<Class<? extends MigrationStep>> getCalledSteps() {
      return calledSteps;
    }
  }

  public static final class MigrationStep1 extends SingleCallCheckerMigrationStep {

  }

  public static final class MigrationStep2 extends SingleCallCheckerMigrationStep {

  }

  public static final class MigrationStep3 extends SingleCallCheckerMigrationStep {

  }

  public static class SqlExceptionFailingMigrationStep implements MigrationStep {
    private static final SQLException THROWN_EXCEPTION = new SQLException("Faking SQL exception in MigrationStep#execute()");

    @Override
    public void execute() throws SQLException {
      throw THROWN_EXCEPTION;
    }
  }

  public static class RuntimeExceptionFailingMigrationStep implements MigrationStep {
    private static final RuntimeException THROWN_EXCEPTION = new RuntimeException("Faking failing migration step");

    @Override
    public void execute() {
      throw THROWN_EXCEPTION;
    }
  }

}
