/*
 * SonarQube
 * Copyright (C) 2009-2024 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.component;

import com.google.common.base.Strings;
import java.util.List;
import org.assertj.core.groups.Tuple;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.sonar.api.utils.System2;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.audit.AuditPersister;
import org.sonar.db.audit.model.ComponentKeyNewValue;
import org.sonar.db.project.ProjectDto;

import static org.apache.commons.lang3.RandomStringUtils.secure;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.sonar.db.component.BranchType.PULL_REQUEST;
import static org.sonar.db.component.ComponentKeyUpdaterDao.computeNewKey;
import static org.sonar.db.component.ComponentTesting.newDirectory;
import static org.sonar.db.component.ComponentTesting.newFileDto;

class ComponentKeyUpdaterDaoIT {

  @RegisterExtension
  private final DbTester db = DbTester.create(System2.INSTANCE);
  private final AuditPersister auditPersister = mock(AuditPersister.class);
  private final DbClient dbClient = db.getDbClient();
  private final DbSession dbSession = db.getSession();
  private final ComponentKeyUpdaterDao underTest = db.getDbClient().componentKeyUpdaterDao();
  private final ComponentKeyUpdaterDao underTestWithAuditPersister = new ComponentKeyUpdaterDao(auditPersister);

  @Test
  void updateKey_changes_the_key_of_tree_of_components() {
    ProjectData projectData = populateSomeData();

    underTest.updateKey(dbSession, projectData.getProjectDto().getUuid(), "org.struts:struts", "struts:core");
    dbSession.commit();

    assertThat(db.select("select uuid as \"UUID\", kee as \"KEE\" from components"))
      .extracting(t -> t.get("UUID"), t -> t.get("KEE"))
      .containsOnly(
        Tuple.tuple("A", "struts:core"),
        Tuple.tuple("B", "struts:core:/src/org/struts"),
        Tuple.tuple("C", "struts:core:/src/org/struts/RequestContext.java"),
        Tuple.tuple("D", "foo:struts-core"));
  }

  @Test
  void updateKey_updates_disabled_components() {
    ProjectData projectData = db.components().insertPrivateProject(p -> p.setKey("my_project"));
    ComponentDto mainBranch = projectData.getMainBranchComponent();
    ComponentDto directory = db.components().insertComponent(
      newDirectory(mainBranch, "B")
        .setKey("my_project:directory"));
    db.components().insertComponent(newFileDto(mainBranch, directory).setKey("my_project:directory/file"));
    ComponentDto inactiveDirectory = db.components().insertComponent(newDirectory(mainBranch, "/inactive_directory").setKey("my_project" +
      ":inactive_directory").setEnabled(false));
    db.components().insertComponent(newFileDto(mainBranch, inactiveDirectory).setKey("my_project:inactive_directory/file").setEnabled(false));

    underTest.updateKey(dbSession, projectData.projectUuid(), "my_project", "your_project");
    dbSession.commit();

    List<ComponentDto> result = dbClient.componentDao().selectByBranchUuid(mainBranch.uuid(), dbSession);
    assertThat(result)
      .hasSize(5)
      .extracting(ComponentDto::getKey)
      .containsOnlyOnce("your_project", "your_project:directory", "your_project:directory/file", "your_project:inactive_directory",
        "your_project:inactive_directory/file");
  }

  @Test
  void updateKey_updates_branches_too() {
    ProjectData projectData = db.components().insertPublicProject();
    ComponentDto mainBranch = projectData.getMainBranchComponent();
    String branchName = secure().nextAlphanumeric(248);
    ComponentDto branch = db.components().insertProjectBranch(mainBranch, b -> b.setKey(branchName));
    db.components().insertComponent(newFileDto(branch, mainBranch.uuid()));
    db.components().insertComponent(newFileDto(branch, mainBranch.uuid()));
    int prComponentCount = 3;

    String oldProjectKey = mainBranch.getKey();
    assertThat(dbClient.componentDao().selectByBranchUuid(mainBranch.uuid(), dbSession)).hasSize(1);
    assertThat(dbClient.componentDao().selectByBranchUuid(branch.uuid(), dbSession)).hasSize(prComponentCount);

    String newProjectKey = "newKey";
    underTest.updateKey(dbSession, projectData.projectUuid(), projectData.projectKey(), newProjectKey);

    assertThat(dbClient.componentDao().selectByKey(dbSession, oldProjectKey)).isEmpty();
    assertThat(dbClient.componentDao().selectByKey(dbSession, newProjectKey)).isPresent();
    assertThat(dbClient.componentDao().selectByKeyAndBranch(dbSession, newProjectKey, branchName)).isPresent();
    assertThat(dbClient.componentDao().selectByBranchUuid(mainBranch.uuid(), dbSession)).hasSize(1);
    assertThat(dbClient.componentDao().selectByBranchUuid(branch.uuid(), dbSession)).hasSize(prComponentCount);

    db.select(dbSession, "select kee from components")
      .forEach(map -> map.values().forEach(k -> assertThat(k.toString()).startsWith(newProjectKey)));
  }

  @Test
  void updateKey_updates_pull_requests_too() {
    ProjectData projectData = db.components().insertPublicProject();
    ComponentDto mainBranch = projectData.getMainBranchComponent();
    String pullRequestKey1 = secure().nextAlphanumeric(100);
    ComponentDto pullRequest = db.components().insertProjectBranch(mainBranch, b -> b.setBranchType(PULL_REQUEST).setKey(pullRequestKey1));
    db.components().insertComponent(newFileDto(pullRequest));
    db.components().insertComponent(newFileDto(pullRequest));
    int prComponentCount = 3;

    String oldProjectKey = mainBranch.getKey();
    assertThat(dbClient.componentDao().selectByBranchUuid(mainBranch.uuid(), dbSession)).hasSize(1);
    assertThat(dbClient.componentDao().selectByBranchUuid(pullRequest.uuid(), dbSession)).hasSize(prComponentCount);

    String newProjectKey = "newKey";
    underTest.updateKey(dbSession, projectData.projectUuid(), projectData.projectKey(), newProjectKey);

    assertThat(dbClient.componentDao().selectByKey(dbSession, oldProjectKey)).isEmpty();
    assertThat(dbClient.componentDao().selectByKey(dbSession, newProjectKey)).isPresent();
    assertThat(dbClient.componentDao().selectByKeyAndPullRequest(dbSession, newProjectKey, pullRequestKey1)).isPresent();

    assertThat(dbClient.componentDao().selectByBranchUuid(mainBranch.uuid(), dbSession)).hasSize(1);
    assertThat(dbClient.componentDao().selectByBranchUuid(pullRequest.uuid(), dbSession)).hasSize(prComponentCount);

    db.select(dbSession, "select kee from components")
      .forEach(map -> map.values().forEach(k -> assertThat(k.toString()).startsWith(newProjectKey)));
  }

  @Test
  void updateKey_throws_IAE_if_component_with_specified_key_does_not_exist() {
    populateSomeData();

    assertThatThrownBy(() -> underTest.updateKey(dbSession, "A", "org.struts:struts", "foo:struts-core"))
      .isInstanceOf(IllegalArgumentException.class)
      .hasMessage("Impossible to update key: a component with key \"foo:struts-core\" already exists.");
  }

  @Test
  void updateKey_throws_IAE_when_sub_component_key_is_too_long() {
    ProjectData projectData = db.components().insertPrivateProject("project-uuid", p -> p.setKey("old-project-key"));
    ProjectDto project = projectData.getProjectDto();
    db.components().insertComponent(newFileDto(projectData.getMainBranchComponent()).setKey("old-project-key:file"));
    String newLongProjectKey = Strings.repeat("a", 400);
    String projectUuid = project.getUuid();
    String projectKey = project.getKey();
    assertThatThrownBy(() -> underTest.updateKey(dbSession, projectUuid, projectKey, newLongProjectKey))
      .isInstanceOf(IllegalArgumentException.class)
      .hasMessage("Component key length (405) is longer than the maximum authorized (400). '" + newLongProjectKey + ":file' was provided.");
  }

  @Test
  void compute_new_key() {
    assertThat(computeNewKey("my_project", "my_", "your_")).isEqualTo("your_project");
    assertThat(computeNewKey("my_project", "my_", "$()_")).isEqualTo("$()_project");
  }

  @Test
  void updateKey_callsAuditPersister() {
    db.components().insertPrivateProject("A", p -> p.setKey("my_project"));

    underTestWithAuditPersister.updateKey(dbSession, "A", "my_project", "your_project");

    verify(auditPersister, times(1)).componentKeyUpdate(any(DbSession.class), any(ComponentKeyNewValue.class), anyString());
  }

  private ProjectData populateSomeData() {
    ProjectData projectData = db.components().insertPrivateProject(t -> t.setKey("org.struts:struts").setUuid("A").setBranchUuid("A"));
    ComponentDto mainBranch1 = projectData.getMainBranchComponent();
    ComponentDto directory1 = db.components().insertComponent(newDirectory(mainBranch1, "/src/org/struts").setUuid("B"));
    db.components().insertComponent(ComponentTesting.newFileDto(mainBranch1, directory1).setKey("org.struts:struts:/src/org/struts" +
      "/RequestContext.java").setUuid("C"));
    ComponentDto project2 = db.components().insertPublicProject(t -> t.setKey("foo:struts-core").setUuid("D")).getMainBranchComponent();
    return projectData;
  }
}
