/*
 * 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.db.purge;

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.sonar.api.impl.utils.AlwaysIncreasingSystem2;
import org.sonar.api.utils.System2;
import org.sonar.core.util.UuidFactoryFast;
import org.sonar.db.DbTester;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentTesting;
import org.sonar.db.component.ProjectData;
import org.sonar.db.component.SnapshotDto;
import org.sonar.db.dialect.Dialect;
import org.sonar.db.duplication.DuplicationUnitDto;
import org.sonar.db.entity.EntityDto;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.metric.MetricDto;
import org.sonar.db.newcodeperiod.NewCodePeriodType;
import org.sonar.db.permission.GlobalPermission;
import org.sonar.db.portfolio.PortfolioDto;
import org.sonar.db.portfolio.PortfolioProjectDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.report.ReportScheduleDto;
import org.sonar.db.report.ReportSubscriptionDto;
import org.sonar.db.rule.RuleDto;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;

import static com.google.common.collect.Lists.newArrayList;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.sonar.db.component.ComponentTesting.newBranchDto;
import static org.sonar.db.component.ComponentTesting.newFileDto;
import static org.sonar.db.component.ComponentTesting.newProjectCopy;
import static org.sonar.db.component.SnapshotDto.STATUS_PROCESSED;
import static org.sonar.db.component.SnapshotDto.STATUS_UNPROCESSED;
import static org.sonar.db.issue.IssueTesting.newCodeReferenceIssue;

@RunWith(DataProviderRunner.class)
public class PurgeCommandsIT {

  @Rule
  public DbTester dbTester = DbTester.create(System2.INSTANCE);

  private final AlwaysIncreasingSystem2 system2 = new AlwaysIncreasingSystem2();
  private final PurgeProfiler profiler = new PurgeProfiler();
  private final Random random = new Random();
  private final PurgeCommands underTest = new PurgeCommands(dbTester.getSession(), profiler, system2);

  /**
   * Required because there is no autogenerated keys for analysis_properties
   */
  @After
  public void resetAnalysisProperties() {
    dbTester.executeUpdateSql("DELETE FROM analysis_properties");
  }

  /**
   * Test that SQL queries execution do not fail with a huge number of parameter
   */
  @Test
  public void should_not_fail_when_deleting_huge_number_of_analyses() {
    new PurgeCommands(dbTester.getSession(), profiler, system2).deleteAnalyses(getHugeNumberOfUuids());
    // The goal of this test is only to check that the query do no fail, not to check result
  }

  @Test
  public void purgeAnalyses_deletes_duplications() {
    ComponentDto project = dbTester.components().insertPrivateProject().getMainBranchComponent();
    SnapshotDto analysis1 = dbTester.components().insertSnapshot(project);
    SnapshotDto analysis2 = dbTester.components().insertSnapshot(project);
    SnapshotDto analysis3 = dbTester.components().insertSnapshot(project);
    SnapshotDto analysis4 = dbTester.components().insertSnapshot(project);
    int count = 8;
    for (SnapshotDto analysis : asList(analysis1, analysis2, analysis3, analysis4)) {
      IntStream.range(0, count).forEach(i -> insertDuplication(project, analysis));
    }

    underTest.purgeAnalyses(singletonList(analysis1.getUuid()));
    assertThat(countDuplications(analysis1)).isZero();
    assertThat(countDuplications(analysis2)).isEqualTo(count);
    assertThat(countDuplications(analysis3)).isEqualTo(count);
    assertThat(countDuplications(analysis4)).isEqualTo(count);

    underTest.purgeAnalyses(asList(analysis1.getUuid(), analysis3.getUuid(), analysis4.getUuid()));
    assertThat(countDuplications(analysis1)).isZero();
    assertThat(countDuplications(analysis2)).isEqualTo(count);
    assertThat(countDuplications(analysis3)).isZero();
    assertThat(countDuplications(analysis4)).isZero();
  }

  /**
   * Test that SQL queries execution do not fail with a huge number of parameter
   */
  @Test
  public void purgeAnalyses_should_not_fail_when_purging_huge_number_of_analyses() {
    new PurgeCommands(dbTester.getSession(), profiler, system2).purgeAnalyses(getHugeNumberOfUuids());
    // The goal of this test is only to check that the query do no fail, not to check result
  }

  @Test
  @UseDataProvider("projects")
  public void deleteComponents_delete_tree_of_components_of_a_project(ComponentDto project) {
    dbTester.components().insertComponent(project);
    ComponentDto otherProject = dbTester.components().insertPrivateProject().getMainBranchComponent();
    Stream.of(project, otherProject).forEach(prj -> {
      ComponentDto directory1 = dbTester.components().insertComponent(ComponentTesting.newDirectory(prj, "a"));
      ComponentDto directory2 = dbTester.components().insertComponent(ComponentTesting.newDirectory(prj, "b"));
      dbTester.components().insertComponent(newFileDto(prj));
      dbTester.components().insertComponent(newFileDto(directory1));
      dbTester.components().insertComponent(newFileDto(directory2));
    });

    underTest.deleteComponents(project.uuid());

    assertThat(countComponentOfRoot(project)).isZero();
    assertThat(countComponentOfRoot(otherProject)).isEqualTo(6);
  }

  @Test
  public void deleteNonMainBranchComponentsByProjectUuid_shouldDeletesAllBranchesOfAProjectExceptMainBranch() {
    ProjectData projectData = dbTester.components().insertPublicProject();
    ComponentDto mainBranch = projectData.getMainBranchComponent();
    ComponentDto branch = dbTester.components().insertProjectBranch(mainBranch);

    ComponentDto directory1 = dbTester.components().insertComponent(ComponentTesting.newDirectory(mainBranch, "a"));
    ComponentDto directory2 = dbTester.components().insertComponent(ComponentTesting.newDirectory(mainBranch, "b"));
    dbTester.components().insertComponent(newFileDto(mainBranch));
    dbTester.components().insertComponent(newFileDto(directory1));
    dbTester.components().insertComponent(newFileDto(directory2));

    directory1 = dbTester.components().insertComponent(ComponentTesting.newDirectory(branch, "a"));
    directory2 = dbTester.components().insertComponent(ComponentTesting.newDirectory(branch, "b"));
    dbTester.components().insertComponent(newFileDto(branch, mainBranch.uuid()));
    dbTester.components().insertComponent(newFileDto(directory1));
    dbTester.components().insertComponent(newFileDto(directory2));

    underTest.deleteNonMainBranchComponentsByProjectUuid(projectData.projectUuid());

    assertThat(countComponentOfRoot(mainBranch)).isEqualTo(6);
    assertThat(countComponentOfRoot(branch)).isZero();
  }

  @Test
  @UseDataProvider("views")
  public void deleteComponents_delete_tree_of_components_of_a_view(ComponentDto view) {
    dbTester.components().insertComponent(view);
    ComponentDto otherView = dbTester.components().insertPrivatePortfolio();
    Stream.of(view, otherView).forEach(vw -> {
      dbTester.components().insertSubView(vw);
      dbTester.components().insertComponent(newProjectCopy(dbTester.components().insertPrivateProject().getMainBranchComponent(), vw));
      dbTester.components().insertComponent(newProjectCopy(dbTester.components().insertPrivateProject().getMainBranchComponent(), vw));
    });

    underTest.deleteComponents(view.uuid());
    assertThat(countComponentOfRoot(view)).isZero();
    assertThat(countComponentOfRoot(otherView)).isEqualTo(4);
  }

  @Test
  public void deleteComponents_does_not_delete_child_tables() {
    ComponentDto component = dbTester.components().insertPrivateProject().getMainBranchComponent();
    ComponentDto file = dbTester.components().insertComponent(newFileDto(component));
    SnapshotDto analysis = dbTester.components().insertSnapshot(component);
    dbTester.events().insertEvent(analysis);
    IssueDto issue = dbTester.issues().insert(dbTester.rules().insert(), component, file);
    dbTester.issues().insertChange(issue);

    underTest.deleteComponents(component.uuid());

    assertThat(dbTester.countRowsOfTable("components")).isZero();
    assertThat(dbTester.countRowsOfTable("snapshots")).isOne();
    assertThat(dbTester.countRowsOfTable("events")).isOne();
    assertThat(dbTester.countRowsOfTable("issues")).isOne();
    assertThat(dbTester.countRowsOfTable("issue_changes")).isOne();
  }

  @Test
  public void deleteProjects() {
    ProjectData projectData = dbTester.components().insertPrivateProject();
    ComponentDto mainBranch = projectData.getMainBranchComponent();
    ProjectDto projectDto = dbTester.getDbClient().projectDao().selectProjectByKey(dbTester.getSession(), mainBranch.getKey()).get();
    ComponentDto file = dbTester.components().insertComponent(newFileDto(mainBranch));
    SnapshotDto analysis = dbTester.components().insertSnapshot(mainBranch);
    dbTester.events().insertEvent(analysis);
    IssueDto issue = dbTester.issues().insert(dbTester.rules().insert(), mainBranch, file);
    dbTester.issues().insertChange(issue);

    assertThat(dbTester.countRowsOfTable("projects")).isOne();

    underTest.deleteComponents(mainBranch.uuid());
    underTest.deleteProject(projectData.projectUuid());

    assertThat(dbTester.countRowsOfTable("projects")).isZero();
    assertThat(dbTester.countRowsOfTable("components")).isZero();
    assertThat(dbTester.countRowsOfTable("snapshots")).isOne();
    assertThat(dbTester.countRowsOfTable("events")).isOne();
    assertThat(dbTester.countRowsOfTable("issues")).isOne();
    assertThat(dbTester.countRowsOfTable("issue_changes")).isOne();
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteAnalyses_by_rootUuid_all_analyses_of_specified_root_uuid(ComponentDto projectOrView) {
    dbTester.components().insertComponent(projectOrView);
    ComponentDto otherProject = dbTester.components().insertPrivateProject().getMainBranchComponent();
    Stream.of(projectOrView, otherProject).forEach(p -> {
      dbTester.components().insertSnapshot(p, t -> t.setLast(false));
      dbTester.components().insertSnapshot(p, t -> t.setLast(true));
      dbTester.components().insertSnapshot(p, t -> t.setLast(false).setStatus(STATUS_UNPROCESSED));
      dbTester.components().insertSnapshot(p, t -> t.setLast(false).setStatus(STATUS_PROCESSED));
    });

    underTest.deleteAnalyses(projectOrView.uuid());

    assertThat(countAnalysesOfRoot(projectOrView)).isZero();
    assertThat(countAnalysesOfRoot(otherProject)).isEqualTo(4);
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteAnalyses_by_rootUuid_deletes_event_component_changes(ComponentDto projectOrView) {
    dbTester.components().insertComponent(projectOrView);
    ComponentDto otherProject = dbTester.components().insertPrivateProject().getMainBranchComponent();
    int count = 5;
    IntStream.range(0, count).forEach(i -> {
      insertRandomEventComponentChange(projectOrView);
      insertRandomEventComponentChange(otherProject);
    });

    underTest.deleteAnalyses(projectOrView.uuid());

    assertThat(countEventComponentChangesOf(projectOrView)).isZero();
    assertThat(countEventComponentChangesOf(otherProject)).isEqualTo(count);
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteAnalyses_by_rootUuid_deletes_events(ComponentDto projectOrView) {
    dbTester.components().insertComponent(projectOrView);
    SnapshotDto analysis1 = dbTester.components().insertSnapshot(projectOrView);
    SnapshotDto analysis2 = dbTester.components().insertSnapshot(projectOrView);
    ComponentDto otherProject = dbTester.components().insertPrivateProject().getMainBranchComponent();
    SnapshotDto otherAnalysis1 = dbTester.components().insertSnapshot(otherProject);
    SnapshotDto otherAnalysis2 = dbTester.components().insertSnapshot(otherProject);
    int count = 7;
    IntStream.range(0, count).forEach(i -> {
      dbTester.events().insertEvent(analysis1);
      dbTester.events().insertEvent(analysis2);
      dbTester.events().insertEvent(otherAnalysis1);
      dbTester.events().insertEvent(otherAnalysis2);
    });

    underTest.deleteAnalyses(projectOrView.uuid());

    assertThat(countEventsOf(analysis1)).isZero();
    assertThat(countEventsOf(analysis2)).isZero();
    assertThat(countEventsOf(otherAnalysis1)).isEqualTo(count);
    assertThat(countEventsOf(otherAnalysis2)).isEqualTo(count);
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteAnalyses_by_rootUuid_deletes_measures(ComponentDto projectOrView) {
    MetricDto metric1 = dbTester.measures().insertMetric();
    MetricDto metric2 = dbTester.measures().insertMetric();
    dbTester.components().insertComponent(projectOrView);
    SnapshotDto analysis1 = dbTester.components().insertSnapshot(projectOrView);
    SnapshotDto analysis2 = dbTester.components().insertSnapshot(projectOrView);
    ComponentDto otherProject = dbTester.components().insertPrivateProject().getMainBranchComponent();
    SnapshotDto otherAnalysis1 = dbTester.components().insertSnapshot(otherProject);
    SnapshotDto otherAnalysis2 = dbTester.components().insertSnapshot(otherProject);
    int count = 5;
    Stream.of(metric1, metric2)
      .forEach(metric -> {
        IntStream.range(0, count).forEach(i -> {
          dbTester.measures().insertMeasure(projectOrView, analysis1, metric);
          dbTester.measures().insertMeasure(projectOrView, analysis2, metric);
          dbTester.measures().insertMeasure(otherProject, otherAnalysis1, metric);
          dbTester.measures().insertMeasure(otherProject, otherAnalysis2, metric);
        });
      });

    underTest.deleteAnalyses(projectOrView.uuid());

    assertThat(countMeasuresOf(analysis1)).isZero();
    assertThat(countMeasuresOf(analysis2)).isZero();
    assertThat(countMeasuresOf(otherAnalysis1)).isEqualTo(count * 2);
    assertThat(countMeasuresOf(otherAnalysis2)).isEqualTo(count * 2);
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteAnalyses_by_rootUuid_deletes_analysis_properties(ComponentDto projectOrView) {
    dbTester.components().insertComponent(projectOrView);
    SnapshotDto analysis1 = dbTester.components().insertSnapshot(projectOrView);
    SnapshotDto analysis2 = dbTester.components().insertSnapshot(projectOrView);
    ComponentDto otherProject = dbTester.components().insertPrivateProject().getMainBranchComponent();
    SnapshotDto otherAnalysis1 = dbTester.components().insertSnapshot(otherProject);
    SnapshotDto otherAnalysis2 = dbTester.components().insertSnapshot(otherProject);
    int count = 5;
    IntStream.range(0, count).forEach(i -> {
      insertRandomAnalysisProperty(analysis1);
      insertRandomAnalysisProperty(analysis2);
      insertRandomAnalysisProperty(otherAnalysis1);
      insertRandomAnalysisProperty(otherAnalysis2);
    });

    underTest.deleteAnalyses(projectOrView.uuid());

    assertThat(countAnalysisPropertiesOf(analysis1)).isZero();
    assertThat(countAnalysisPropertiesOf(analysis2)).isZero();
    assertThat(countAnalysisPropertiesOf(otherAnalysis1)).isEqualTo(count);
    assertThat(countAnalysisPropertiesOf(otherAnalysis2)).isEqualTo(count);
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteAbortedAnalyses_deletes_only_analyse_with_unprocessed_status(ComponentDto projectOrView) {
    dbTester.components().insertComponent(projectOrView);
    ComponentDto otherProject = dbTester.components().insertPrivateProject().getMainBranchComponent();
    Stream.of(projectOrView, otherProject)
      .forEach(p -> {
        dbTester.components().insertSnapshot(p, t -> t.setStatus(STATUS_PROCESSED).setLast(false));
        dbTester.components().insertSnapshot(p, t -> t.setStatus(STATUS_PROCESSED).setLast(true));
        dbTester.components().insertSnapshot(p, t -> t.setStatus(STATUS_UNPROCESSED).setLast(false));
        // unrealistic case but the last analysis is never deleted even if unprocessed
        dbTester.components().insertSnapshot(p, t -> t.setStatus(STATUS_UNPROCESSED).setLast(true));
      });

    underTest.deleteAbortedAnalyses(projectOrView.uuid());

    assertThat(countAnalysesOfRoot(projectOrView, STATUS_UNPROCESSED, true)).isOne();
    assertThat(countAnalysesOfRoot(projectOrView, STATUS_UNPROCESSED, false)).isZero();
    assertThat(countAnalysesOfRoot(projectOrView, STATUS_PROCESSED, true)).isOne();
    assertThat(countAnalysesOfRoot(projectOrView, STATUS_PROCESSED, false)).isOne();
    assertThat(countAnalysesOfRoot(otherProject, STATUS_UNPROCESSED, true)).isOne();
    assertThat(countAnalysesOfRoot(otherProject, STATUS_UNPROCESSED, false)).isOne();
    assertThat(countAnalysesOfRoot(otherProject, STATUS_PROCESSED, true)).isOne();
    assertThat(countAnalysesOfRoot(otherProject, STATUS_PROCESSED, false)).isOne();
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteAnalyses_by_analyses_deletes_specified_analysis(ComponentDto projectOrView) {
    dbTester.components().insertComponent(projectOrView);
    SnapshotDto analysis1 = dbTester.components().insertSnapshot(projectOrView, s -> s.setStatus(STATUS_PROCESSED));
    SnapshotDto analysis2 = dbTester.components().insertSnapshot(projectOrView, s -> s.setStatus(STATUS_PROCESSED));
    SnapshotDto analysis3 = dbTester.components().insertSnapshot(projectOrView, s -> s.setStatus(STATUS_UNPROCESSED));
    SnapshotDto analysis4 = dbTester.components().insertSnapshot(projectOrView, s -> s.setStatus(STATUS_UNPROCESSED));

    underTest.deleteAnalyses(singletonList(analysis1.getUuid()));
    assertThat(uuidsOfAnalysesOfRoot(projectOrView))
      .containsOnly(analysis2.getUuid(), analysis3.getUuid(), analysis4.getUuid());

    underTest.deleteAnalyses(asList(analysis2.getUuid(), analysis3.getUuid()));
    assertThat(uuidsOfAnalysesOfRoot(projectOrView))
      .containsOnly(analysis4.getUuid());

    underTest.deleteAnalyses(asList(analysis1.getUuid(), analysis2.getUuid(), analysis3.getUuid(), analysis4.getUuid()));
    assertThat(uuidsOfAnalysesOfRoot(projectOrView)).isEmpty();
  }

  private Stream<String> uuidsOfAnalysesOfRoot(ComponentDto rootComponent) {
    return dbTester.select("select uuid as \"UUID\" from snapshots where root_component_uuid='" + rootComponent.uuid() + "'")
      .stream()
      .map(t -> (String) t.get("UUID"));
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteAnalyses_by_analyses_deletes_event_component_changes(ComponentDto projectOrView) {
    dbTester.components().insertComponent(projectOrView);
    SnapshotDto analysis = dbTester.components().insertSnapshot(projectOrView, randomLastAndStatus());
    SnapshotDto otherAnalysis = dbTester.components().insertSnapshot(projectOrView);
    int count = 5;
    IntStream.range(0, count).forEach(i -> {
      insertRandomEventComponentChange(analysis);
      insertRandomEventComponentChange(otherAnalysis);
    });

    underTest.deleteAnalyses(singletonList(analysis.getUuid()));

    assertThat(countEventComponentChangesOf(analysis)).isZero();
    assertThat(countEventComponentChangesOf(otherAnalysis)).isEqualTo(count);
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteAnalyses_by_analyses_deletes_events(ComponentDto projectOrView) {
    dbTester.components().insertComponent(projectOrView);
    SnapshotDto analysis = dbTester.components().insertSnapshot(projectOrView, randomLastAndStatus());
    SnapshotDto otherAnalysis = dbTester.components().insertSnapshot(projectOrView);
    int count = 5;
    IntStream.range(0, count).forEach(i -> {
      dbTester.events().insertEvent(analysis);
      dbTester.events().insertEvent(otherAnalysis);
    });

    underTest.deleteAnalyses(singletonList(analysis.getUuid()));

    assertThat(countEventsOf(analysis)).isZero();
    assertThat(countEventsOf(otherAnalysis)).isEqualTo(count);
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteAnalyses_by_analyses_deletes_measures(ComponentDto projectOrView) {
    MetricDto metric1 = dbTester.measures().insertMetric();
    MetricDto metric2 = dbTester.measures().insertMetric();
    dbTester.components().insertComponent(projectOrView);
    SnapshotDto analysis = dbTester.components().insertSnapshot(projectOrView, randomLastAndStatus());
    SnapshotDto otherAnalysis = dbTester.components().insertSnapshot(projectOrView);
    int count = 5;
    Stream.of(metric1, metric2)
      .forEach(metric -> {
        IntStream.range(0, count).forEach(i -> {
          dbTester.measures().insertMeasure(projectOrView, analysis, metric);
          dbTester.measures().insertMeasure(projectOrView, otherAnalysis, metric);
        });
      });

    underTest.deleteAnalyses(singletonList(analysis.getUuid()));

    assertThat(countMeasuresOf(analysis)).isZero();
    assertThat(countMeasuresOf(otherAnalysis)).isEqualTo(count * 2);
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteAnalyses_by_analyses_deletes_analysis_properties(ComponentDto projectOrView) {
    dbTester.components().insertComponent(projectOrView);
    SnapshotDto analysis = dbTester.components().insertSnapshot(projectOrView, randomLastAndStatus());
    SnapshotDto otherAnalysis = dbTester.components().insertSnapshot(projectOrView);
    int count = 4;
    IntStream.range(0, count).forEach(i -> {
      insertRandomAnalysisProperty(analysis);
      insertRandomAnalysisProperty(otherAnalysis);
    });

    underTest.deleteAnalyses(singletonList(analysis.getUuid()));

    assertThat(countAnalysisPropertiesOf(analysis)).isZero();
    assertThat(countAnalysisPropertiesOf(otherAnalysis)).isEqualTo(count);
  }

  @Test
  @UseDataProvider("projects")
  public void deleteOutdatedProperties_deletes_properties_by_component_uuid(ComponentDto project) {
    ComponentDto component = dbTester.components().insertComponent(project);
    ComponentDto anotherComponent = dbTester.components().insertPublicProject().getMainBranchComponent();
    int count = 4;
    IntStream.range(0, count).forEach(i -> {
      insertRandomProperty(component);
      insertRandomProperty(anotherComponent);
    });

    underTest.deleteOutdatedProperties(component.uuid());

    assertThat(countPropertiesOf(component)).isZero();
    assertThat(countPropertiesOf(anotherComponent)).isEqualTo(count);
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteIssues_deletes_all_issues_of_specified_root_component(ComponentDto projectOrView) {
    RuleDto rule1 = dbTester.rules().insert();
    RuleDto rule2 = dbTester.rules().insert();
    dbTester.components().insertComponent(projectOrView);
    ComponentDto file = dbTester.components().insertComponent(newFileDto(projectOrView));
    ComponentDto otherProject = dbTester.components().insertPrivateProject().getMainBranchComponent();
    ComponentDto otherFile = dbTester.components().insertComponent(newFileDto(otherProject));
    int count = 5;
    IntStream.range(0, count).forEach(i -> {
      Stream.of(rule1, rule2).forEach(rule -> {
        dbTester.issues().insertIssue(t -> t.setRule(rule).setProject(projectOrView).setComponent(projectOrView));
        dbTester.issues().insertIssue(t -> t.setRule(rule).setProject(projectOrView).setComponent(file));
        dbTester.issues().insertIssue(t -> t.setRule(rule).setProject(otherProject).setComponent(otherProject));
        dbTester.issues().insertIssue(t -> t.setRule(rule).setProject(otherProject).setComponent(otherFile));
      });
    });

    underTest.deleteIssues(projectOrView.uuid());

    assertThat(countIssuesOfRoot(projectOrView)).isZero();
    assertThat(countIssuesOfRoot(otherProject)).isEqualTo(count * 4);
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteIssues_deletes_issue_changes(ComponentDto projectOrView) {
    RuleDto rule = dbTester.rules().insert();
    dbTester.components().insertComponent(projectOrView);
    ComponentDto file = dbTester.components().insertComponent(newFileDto(projectOrView));
    int count = 5;
    IntStream.range(0, count).forEach(i -> {
      IssueDto issue = dbTester.issues().insertIssue(t -> t.setRule(rule).setProject(projectOrView).setComponent(projectOrView));
      dbTester.issues().insertChange(issue);
      issue = dbTester.issues().insertIssue(t -> t.setRule(rule).setProject(projectOrView).setComponent(file));
      dbTester.issues().insertChange(issue);
    });

    underTest.deleteIssues(projectOrView.uuid());

    assertThat(dbTester.countRowsOfTable("ISSUE_CHANGES")).isZero();
  }

  @Test
  @UseDataProvider("projectsAndViews")
  public void deleteIssues_deletes_new_code_reference_issues(ComponentDto projectOrView) {
    RuleDto rule = dbTester.rules().insert();
    dbTester.components().insertComponent(projectOrView);
    ComponentDto file = dbTester.components().insertComponent(newFileDto(projectOrView));
    List<String> issueKeys = new ArrayList<>();
    int count = 5;
    IntStream.range(0, count).forEach(i -> {
      IssueDto issue = dbTester.issues().insertIssue(t -> t.setRule(rule).setProject(projectOrView).setComponent(projectOrView));
      dbTester.issues().insertNewCodeReferenceIssue(newCodeReferenceIssue(issue));
      issue = dbTester.issues().insertIssue(t -> t.setRule(rule).setProject(projectOrView).setComponent(file));
      dbTester.issues().insertNewCodeReferenceIssue(newCodeReferenceIssue(issue));
      issueKeys.add("'" + issue.getKey() + "'");
    });

    underTest.deleteIssues(projectOrView.uuid());

    assertThat(dbTester.countSql("select count(uuid) from new_code_reference_issues where issue_key in (" +
                                 String.join(", ", issueKeys) + ")")).isZero();
  }

  @Test
  public void deletePermissions_deletes_permissions_of_public_project() {
    ProjectDto project = dbTester.components().insertPublicProject().getProjectDto();
    addPermissions(project);

    PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);
    purgeCommands.deletePermissions(project.getUuid());

    assertThat(dbTester.countRowsOfTable("group_roles")).isEqualTo(2);
    assertThat(dbTester.countRowsOfTable("user_roles")).isOne();
  }

  @Test
  public void deletePermissions_deletes_permissions_of_private_project() {
    ProjectDto project = dbTester.components().insertPrivateProject().getProjectDto();
    addPermissions(project);

    PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);
    purgeCommands.deletePermissions(project.getUuid());

    assertThat(dbTester.countRowsOfTable("group_roles")).isOne();
    assertThat(dbTester.countRowsOfTable("user_roles")).isOne();
  }

  @Test
  public void deletePermissions_deletes_permissions_of_view() {
    ComponentDto portfolio = dbTester.components().insertPublicPortfolio();
    PortfolioDto portfolioDto = dbTester.components().getPortfolioDto(portfolio);
    addPermissions(portfolioDto);

    PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);
    purgeCommands.deletePermissions(portfolio.uuid());

    assertThat(dbTester.countRowsOfTable("group_roles")).isEqualTo(2);
    assertThat(dbTester.countRowsOfTable("user_roles")).isOne();
  }

  @Test
  public void deleteNewCodePeriodsByRootUuid_deletes_branch_new_code_periods() {
    ComponentDto project = dbTester.components().insertPrivateProject().getMainBranchComponent();
    BranchDto branch = newBranchDto(project);
    dbTester.components().insertProjectBranch(project, branch);

    // global settings
    dbTester.newCodePeriods().insert(NewCodePeriodType.PREVIOUS_VERSION, null);

    // project settings
    dbTester.newCodePeriods().insert(project.uuid(), NewCodePeriodType.NUMBER_OF_DAYS, "20");

    // branch settings
    dbTester.newCodePeriods().insert(project.uuid(), branch.getUuid(), NewCodePeriodType.NUMBER_OF_DAYS, "1");

    PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);
    purgeCommands.deleteNewCodePeriods(branch.getUuid());

    // should delete branch settings only
    assertThat(dbTester.countRowsOfTable("new_code_periods")).isEqualTo(2);
  }

  @Test
  public void deleteNewCodePeriodsByRootUuid_deletes_project_new_code_periods() {
    ComponentDto project = dbTester.components().insertPrivateProject().getMainBranchComponent();
    BranchDto branch = newBranchDto(project);
    dbTester.components().insertProjectBranch(project, branch);

    // global settings
    dbTester.newCodePeriods().insert(NewCodePeriodType.PREVIOUS_VERSION, null);

    // project settings
    dbTester.newCodePeriods().insert(project.uuid(), NewCodePeriodType.NUMBER_OF_DAYS, "20");

    // branch settings
    dbTester.newCodePeriods().insert(project.uuid(), branch.getUuid(), NewCodePeriodType.NUMBER_OF_DAYS, "1");

    PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);
    purgeCommands.deleteNewCodePeriods(project.uuid());

    // should delete branch and project settings only
    assertThat(dbTester.countRowsOfTable("new_code_periods")).isOne();
  }

  @Test
  public void deleteNewCodePeriodsByRootUuid_should_not_delete_any_if_root_uuid_is_null() {
    ComponentDto project = dbTester.components().insertPrivateProject().getMainBranchComponent();
    BranchDto branch = newBranchDto(project);
    dbTester.components().insertProjectBranch(project, branch);

    // global settings
    dbTester.newCodePeriods().insert(NewCodePeriodType.PREVIOUS_VERSION, null);

    // project settings
    dbTester.newCodePeriods().insert(project.uuid(), NewCodePeriodType.NUMBER_OF_DAYS, "20");

    // branch settings
    dbTester.newCodePeriods().insert(project.uuid(), branch.getUuid(), NewCodePeriodType.NUMBER_OF_DAYS, "1");

    PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);
    purgeCommands.deleteNewCodePeriods(null);

    // should delete branch and project settings only
    assertThat(dbTester.countRowsOfTable("new_code_periods")).isEqualTo(3);
  }

  @Test
  public void deleteUserDismissedMessages_deletes_dismissed_warnings_on_project_for_all_users() {
    UserDto user1 = dbTester.users().insertUser();
    UserDto user2 = dbTester.users().insertUser();
    ProjectDto project = dbTester.components().insertPrivateProject().getProjectDto();
    ProjectDto anotherProject = dbTester.components().insertPrivateProject().getProjectDto();
    dbTester.users().insertUserDismissedMessage(user1, project, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
    dbTester.users().insertUserDismissedMessage(user2, project, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
    dbTester.users().insertUserDismissedMessage(user1, anotherProject, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
    PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);

    purgeCommands.deleteUserDismissedMessages(project.getUuid());

    assertThat(dbTester.countRowsOfTable("user_dismissed_messages")).isOne();
  }

  @Test
  public void deleteProjectInPortfolios_deletes_project_and_branch_from_portfolios_if_root_is_branch() {
    var portfolio1 = dbTester.components().insertPrivatePortfolioDto();
    var portfolio2 = dbTester.components().insertPrivatePortfolioDto();
    dbTester.components().insertPrivatePortfolio();
    ProjectDto project = dbTester.components().insertPrivateProject().getProjectDto();
    ProjectDto anotherProject = dbTester.components().insertPrivateProject().getProjectDto();

    dbTester.components().addPortfolioProject(portfolio1, project, anotherProject);
    dbTester.components().addPortfolioProjectBranch(portfolio1, project, "projectBranch");
    dbTester.components().addPortfolioProjectBranch(portfolio1, anotherProject, "anotherProjectBranch");

    dbTester.components().addPortfolioProject(portfolio2, project);

    PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);

    purgeCommands.deleteProjectInPortfolios(project.getUuid());

    assertThat(dbTester.getDbClient().portfolioDao().selectAllPortfolioProjects(dbTester.getSession()))
      .extracting(PortfolioProjectDto::getPortfolioUuid, PortfolioProjectDto::getProjectUuid, PortfolioProjectDto::getBranchUuids)
      .containsExactlyInAnyOrder(tuple(portfolio1.getUuid(), anotherProject.getUuid(), singleton("anotherProjectBranch")));
  }

  @Test
  public void deleteProjectInPortfolios_deletes_project_and_branch_from_portfolios_if_root_is_project_only_selecting_a_single_branch() {
    var portfolio1 = dbTester.components().insertPrivatePortfolioDto();
    dbTester.components().insertPrivatePortfolio();
    ProjectDto project = dbTester.components().insertPrivateProject().getProjectDto();

    dbTester.components().addPortfolioProject(portfolio1, project);
    dbTester.components().addPortfolioProjectBranch(portfolio1, project, "projectBranch");

    PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);

    purgeCommands.deleteProjectInPortfolios(project.getUuid());

    assertThat(dbTester.getDbClient().portfolioDao().selectAllPortfolioProjects(dbTester.getSession())).isEmpty();
    assertThat(dbTester.countRowsOfTable("portfolio_proj_branches")).isZero();
    assertThat(dbTester.countRowsOfTable("portfolio_projects")).isZero();
  }

  @Test
  public void deleteProjectInPortfolios_deletes_branch_from_portfolios_if_root_is_branch() {
    var portfolio1 = dbTester.components().insertPrivatePortfolioDto();
    ProjectDto project = dbTester.components().insertPrivateProject().getProjectDto();

    dbTester.components().addPortfolioProject(portfolio1, project);
    dbTester.components().addPortfolioProjectBranch(portfolio1, project, "projectBranch");
    dbTester.components().addPortfolioProjectBranch(portfolio1, project, "anotherBranch");

    PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);

    purgeCommands.deleteProjectInPortfolios("projectBranch");

    assertThat(dbTester.getDbClient().portfolioDao().selectAllPortfolioProjects(dbTester.getSession()))
      .extracting(PortfolioProjectDto::getPortfolioUuid, PortfolioProjectDto::getProjectUuid, PortfolioProjectDto::getBranchUuids)
      .containsExactlyInAnyOrder(tuple(portfolio1.getUuid(), project.getUuid(), Set.of("anotherBranch")));
  }

  @Test
  public void deleteReportSchedules_shouldDeleteReportShedules_if_root_is_branch() {
    BranchDto mainBranch = dbTester.components().insertPrivateProject().getMainBranchDto();
    BranchDto mainBranch2 = dbTester.components().insertPrivateProject().getMainBranchDto();
    dbTester.getDbClient().reportScheduleDao().upsert(dbTester.getSession(), new ReportScheduleDto().setUuid("uuid")
      .setBranchUuid(mainBranch.getUuid())
      .setLastSendTimeInMs(1));
    dbTester.getDbClient().reportScheduleDao().upsert(dbTester.getSession(), new ReportScheduleDto().setUuid("uuid2")
      .setBranchUuid(mainBranch2.getUuid())
      .setLastSendTimeInMs(2));
    PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);

    purgeCommands.deleteReportSchedules(mainBranch.getUuid());
    assertThat(dbTester.getDbClient().reportScheduleDao().selectAll(dbTester.getSession())).hasSize(1)
      .extracting(r -> r.getUuid()).containsExactly("uuid2");
  }

  @Test
  public void deleteReportSubscriptions_shouldDeleteReportSubscriptions_if_root_is_branch() {
    BranchDto mainBranch = dbTester.components().insertPrivateProject().getMainBranchDto();
    BranchDto mainBranch2 = dbTester.components().insertPrivateProject().getMainBranchDto();
    dbTester.getDbClient().reportSubscriptionDao().insert(dbTester.getSession(), new ReportSubscriptionDto().setUuid("uuid")
      .setUserUuid("userUuid")
      .setBranchUuid(mainBranch.getUuid()));

    dbTester.getDbClient().reportSubscriptionDao().insert(dbTester.getSession(), new ReportSubscriptionDto().setUuid("uuid2")
      .setUserUuid("userUuid")
      .setBranchUuid(mainBranch2.getUuid()));

    PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);

    purgeCommands.deleteReportSubscriptions(mainBranch.getUuid());
    assertThat(dbTester.getDbClient().reportSubscriptionDao().selectAll(dbTester.getSession())).hasSize(1)
      .extracting(r -> r.getUuid()).containsExactly("uuid2");
  }

  private void addPermissions(EntityDto projectDto) {
    if (!projectDto.isPrivate()) {
      dbTester.users().insertEntityPermissionOnAnyone("foo1", projectDto);
      dbTester.users().insertPermissionOnAnyone("not project level");
    }

    GroupDto group = dbTester.users().insertGroup();
    dbTester.users().insertEntityPermissionOnGroup(group, "bar", projectDto);
    dbTester.users().insertPermissionOnGroup(group, "not project level");

    UserDto user = dbTester.users().insertUser();
    dbTester.users().insertProjectPermissionOnUser(user, "doh", projectDto);
    dbTester.users().insertGlobalPermissionOnUser(user, GlobalPermission.SCAN);

    assertThat(dbTester.countRowsOfTable("group_roles")).isEqualTo(projectDto.isPrivate() ? 2 : 4);
    assertThat(dbTester.countRowsOfTable("user_roles")).isEqualTo(2);
  }

  private int countComponentOfRoot(ComponentDto projectOrView) {
    return dbTester.countSql("select count(1) from components where branch_uuid='" + projectOrView.uuid() + "'");
  }

  private void insertDuplication(ComponentDto project, SnapshotDto analysis) {
    dbTester.getDbClient().duplicationDao().insert(dbTester.getSession(), new DuplicationUnitDto()
      .setAnalysisUuid(analysis.getUuid())
      .setComponentUuid(project.uuid())
      .setHash(randomAlphabetic(12))
      .setIndexInFile(random.nextInt(10))
      .setStartLine(random.nextInt(10))
      .setEndLine(random.nextInt(10)));
    dbTester.commit();
  }

  private int countDuplications(SnapshotDto analysis) {
    return dbTester.countSql("select count(1) from duplications_index where analysis_uuid='" + analysis.getUuid() + "'");
  }

  private int countMeasuresOf(SnapshotDto analysis) {
    return dbTester.countSql("select count(1) from project_measures where analysis_uuid='" + analysis.getUuid() + "'");
  }

  private void insertRandomEventComponentChange(ComponentDto componentDto) {
    insertRandomEventComponentChange(newUuid(), componentDto.uuid());
  }

  private void insertRandomEventComponentChange(SnapshotDto analysis) {
    insertRandomEventComponentChange(analysis.getUuid(), newUuid());
  }

  private void insertRandomEventComponentChange(String analysisUuid, String componentUuid) {
    dbTester.executeInsert(
      "EVENT_COMPONENT_CHANGES",
      "UUID", newUuid(),
      "EVENT_UUID", newUuid(),
      "EVENT_COMPONENT_UUID", componentUuid,
      "EVENT_ANALYSIS_UUID", analysisUuid,
      "CHANGE_CATEGORY", randomAlphabetic(12),
      "COMPONENT_UUID", newUuid(),
      "COMPONENT_KEY", randomAlphabetic(9),
      "COMPONENT_NAME", randomAlphabetic(10),
      "CREATED_AT", 1L);
  }

  private static String newUuid() {
    return UuidFactoryFast.getInstance().create();
  }

  private int countAnalysisPropertiesOf(SnapshotDto analysis) {
    return dbTester.countSql("select count(1) from analysis_properties where analysis_uuid='" + analysis.getUuid() + "'");
  }

  private int countPropertiesOf(ComponentDto componentDto) {
    return dbTester.countSql("select count(1) from properties where entity_uuid='" + componentDto.uuid() + "'");
  }

  private int countEventsOf(SnapshotDto analysis) {
    return dbTester.countSql("select count(1) from events where analysis_uuid='" + analysis.getUuid() + "'");
  }

  private int countEventComponentChangesOf(ComponentDto rootComponent) {
    return dbTester.countSql("select count(1) from event_component_changes where event_component_uuid='" + rootComponent.uuid() + "'");
  }

  private int countEventComponentChangesOf(SnapshotDto analysis) {
    return dbTester.countSql("select count(1) from event_component_changes where event_analysis_uuid='" + analysis.getUuid() + "'");
  }

  private void insertRandomAnalysisProperty(SnapshotDto analysis1) {
    boolean isEmpty = new Random().nextBoolean();
    dbTester.executeInsert(
      "ANALYSIS_PROPERTIES",
      "UUID", newUuid(),
      "ANALYSIS_UUID", analysis1.getUuid(),
      "KEE", randomAlphabetic(10),
      "TEXT_VALUE", isEmpty ? null : randomAlphabetic(50),
      "IS_EMPTY", isEmpty,
      "CREATED_AT", 1L);
  }

  private void insertRandomProperty(ComponentDto component) {
    boolean isEmpty = new Random().nextBoolean();
    dbTester.executeInsert(
      "PROPERTIES",
      "UUID", newUuid(),
      "PROP_KEY", randomAlphabetic(10),
      "ENTITY_UUID", component.uuid(),
      "TEXT_VALUE", randomAlphabetic(10),
      "IS_EMPTY", isEmpty,
      "CREATED_AT", 1L);
  }

  private int countAnalysesOfRoot(ComponentDto projectOrView) {
    return dbTester.countSql("select count(1) from snapshots where root_component_uuid='" + projectOrView.uuid() + "'");
  }

  private int countAnalysesOfRoot(ComponentDto projectOrView, String status, boolean isLast) {
    Dialect dialect = dbTester.getDbClient().getDatabase().getDialect();
    String bool = isLast ? dialect.getTrueSqlValue() : dialect.getFalseSqlValue();
    return dbTester.countSql("select count(1) from snapshots where root_component_uuid='" + projectOrView.uuid() + "' and status='" + status + "' and islast=" + bool);
  }

  private List<String> getHugeNumberOfUuids() {
    List<String> hugeNbOfSnapshotUuids = newArrayList();
    for (long i = 0; i < 4500; i++) {
      hugeNbOfSnapshotUuids.add("uuid_" + i);
    }
    return hugeNbOfSnapshotUuids;
  }

  @DataProvider
  public static Object[] projects() {
    return new Object[]{
      ComponentTesting.newPrivateProjectDto(), ComponentTesting.newPublicProjectDto(),
    };
  }

  @DataProvider
  public static Object[] views() {
    return new Object[]{
      ComponentTesting.newPortfolio(), ComponentTesting.newApplication()
    };
  }

  @DataProvider
  public static Object[] projectsAndViews() {
    return Stream.concat(Arrays.stream(views()), Arrays.stream(projects())).toArray(Object[]::new);
  }

  private Consumer<SnapshotDto> randomLastAndStatus() {
    return t -> t.setLast(random.nextBoolean()).setStatus(random.nextBoolean() ? STATUS_PROCESSED : STATUS_UNPROCESSED);
  }

  private int countIssuesOfRoot(ComponentDto root) {
    return dbTester.countSql("select count(*) from issues where project_uuid='" + root.uuid() + "'");
  }

}
