/*
 * This file is part of git-as-svn. It is subject to the license terms
 * in the LICENSE file found in the top-level directory of this distribution
 * and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn,
 * including this file, may be copied, modified, propagated, or distributed
 * except according to the terms contained in the LICENSE file.
 */
package svnserver.ext.gitea;

import io.gitea.ApiClient;
import io.gitea.api.RepositoryApi;
import io.gitea.api.UserApi;
import io.gitea.auth.ApiKeyAuth;
import io.gitea.model.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.Container.ExecResult;
import org.testcontainers.containers.FixedHostPortGenericContainer;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import org.tmatesoft.svn.core.SVNAuthenticationException;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.io.SVNRepository;
import svnserver.SvnTestHelper;
import svnserver.SvnTestServer;
import svnserver.TestHelper;
import svnserver.UserType;
import svnserver.config.RepositoryMappingConfig;
import svnserver.ext.gitea.auth.GiteaUserDBConfig;
import svnserver.ext.gitea.config.GiteaConfig;
import svnserver.ext.gitea.config.GiteaContext;
import svnserver.ext.gitea.config.GiteaToken;
import svnserver.ext.gitea.mapping.GiteaMappingConfig;
import svnserver.ext.gitlfs.storage.LfsStorage;
import svnserver.ext.gitlfs.storage.local.LfsLocalStorageTest;
import svnserver.repository.git.GitCreateMode;

import java.nio.file.Path;
import java.util.function.Function;

/**
 * @author Andrew Thornton <zeripa[email protected]>
 */
public final class GiteaIntegrationTest {
  @NotNull
  private static final Logger log = TestHelper.logger;
  @NotNull
  private static final String administrator = "administrator";
  @NotNull
  private static final String administratorPassword = "administrator";
  @NotNull
  private static final String user = "testuser";
  @NotNull
  private static final String userPassword = "userPassword";
  @NotNull
  private static final String collaborator = "collaborator";
  @NotNull
  private static final String collaboratorPassword = "collaboratorPassword";

  private GenericContainer<?> gitea;
  private String giteaUrl;
  private String giteaApiUrl;
  private GiteaToken administratorToken;
  private Repository testPublicRepository;
  private Repository testPrivateRepository;

  @BeforeClass
  void before() throws Exception {
    SvnTestHelper.skipTestIfDockerUnavailable();

    String giteaVersion = System.getenv("GITEA_VERSION");
    if (giteaVersion == null) {
      SvnTestHelper.skipTestIfRunningOnCI();

      giteaVersion = "latest";
    }

    final int hostPort = 9999;
    final int containerPort = 3000;
    final String hostname = DockerClientFactory.instance().dockerHostIpAddress();

    giteaUrl = String.format("http://%s:%s", hostname, hostPort);
    giteaApiUrl = giteaUrl + "/api/v1";

    gitea = new FixedHostPortGenericContainer<>("gitea/gitea:" + giteaVersion)
        .withFixedExposedPort(hostPort, containerPort)
        .withExposedPorts(containerPort)
        .withEnv("ROOT_URL", giteaUrl)
        .withEnv("INSTALL_LOCK", "true")
        .withEnv("SECRET_KEY", "CmjF5WBUNZytE2C80JuogljLs5enS0zSTlikbP2HyG8IUy15UjkLNvTNsyYW7wN")
        .withEnv("RUN_MODE", "prod")
        .withEnv("LFS_START_SERVER", "true")
        .waitingFor(Wait.forHttp("/user/login"))
        .withLogConsumer(new Slf4jLogConsumer(log));

    gitea.start();

    ExecResult createUserHelpResult = gitea.execInContainer("gitea", "admin", "create-user", "--help", "-c", "/data/gitea/conf/app.ini");
    boolean mustChangePassword = createUserHelpResult.getStdout().contains("--must-change-password");
    String mustChangePasswordString = mustChangePassword ? "--must-change-password=false" : "";
    {
      ExecResult result = gitea.execInContainer("gitea", "admin", "create-user", "--name", administrator,
          "--password", administratorPassword, "--email", "[email protected]", "--admin",
          mustChangePasswordString, "-c", "/data/gitea/conf/app.ini");
      System.out.println(result.getStdout());
      System.err.println(result.getStderr());
    }

    ApiClient apiClient = new ApiClient();
    apiClient.setBasePath(giteaApiUrl);
    apiClient.setUsername(administrator);
    apiClient.setPassword(administratorPassword);
    UserApi userApi = new UserApi(apiClient);
    AccessTokenName accessTokenName = new AccessTokenName();
    accessTokenName.setName("integration-test");
    AccessToken token = userApi.userCreateToken(administrator, accessTokenName);
    administratorToken = new GiteaToken(token.getSha1());

    // Switch to the GiteaContext approach
    // CreateTestUser
    final User testUser = createUser(user, userPassword);
    Assert.assertNotNull(testUser);

    final User collaboratorUser = createUser(collaborator, collaboratorPassword);
    Assert.assertNotNull(collaboratorUser);

    // Create a repository for the test user
    testPublicRepository = createRepository(user, "public-user-repo", "Public User Repository", false, true);
    Assert.assertNotNull(testPublicRepository);

    testPrivateRepository = createRepository(user, "private-user-repo", "Private User Repository", true, true);
    Assert.assertNotNull(testPrivateRepository);
  }

  @NotNull
  private User createUser(@NotNull String username, @NotNull String password) throws Exception {
    return createUser(username, username + "@example.com", password);
  }

  @NotNull
  private Repository createRepository(@NotNull String username, @NotNull String name, @NotNull String description, @Nullable Boolean _private, @Nullable Boolean autoInit) throws Exception {
    final ApiClient apiClient = sudo(GiteaContext.connect(giteaApiUrl, administratorToken), username);
    final RepositoryApi repositoryApi = new RepositoryApi(apiClient);
    final CreateRepoOption repoOption = new CreateRepoOption();
    repoOption.setName(name);
    repoOption.setDescription(description);
    repoOption.setPrivate(_private);
    repoOption.setAutoInit(autoInit);
    repoOption.setReadme("Default");
    return repositoryApi.createCurrentUserRepo(repoOption);
  }

  @NotNull
  private User createUser(@NotNull String username, @NotNull String email, @NotNull String password) throws Exception {
    // Need to create user using command line because users now default to requiring to change password
    ExecResult createUserHelpResult = gitea.execInContainer("gitea", "admin", "create-user", "--help", "-c", "/data/gitea/conf/app.ini");
    boolean mustChangePassword = createUserHelpResult.getStdout().contains("--must-change-password");
    String mustChangePasswordString = mustChangePassword ? "--must-change-password=false" : "";
    gitea.execInContainer("gitea", "admin", "create-user", "--name", username,
        "--password", password, "--email", email,
        mustChangePasswordString, "-c", "/data/gitea/conf/app.ini");
    ApiClient apiClient = GiteaContext.connect(giteaApiUrl, administratorToken);
    UserApi userApi = new UserApi(sudo(apiClient, username));

    return userApi.userGetCurrent();
  }

  // Gitea API methods
  @NotNull
  private ApiClient sudo(ApiClient apiClient, String username) {
    ApiKeyAuth sudoParam = (ApiKeyAuth) apiClient.getAuthentication("SudoParam");
    sudoParam.setApiKey(username);
    return apiClient;
  }

  @Test
  void testLfs() throws Exception {
    final LfsStorage storage = GiteaConfig.createLfsStorage(giteaUrl, testPublicRepository.getFullName(), administratorToken);
    final svnserver.auth.User user = svnserver.auth.User.create(administrator, administrator, administrator, administrator, UserType.Gitea, new svnserver.auth.User.LfsCredentials(administrator, administratorPassword));

    LfsLocalStorageTest.checkLfs(storage, user);
    LfsLocalStorageTest.checkLfs(storage, user);

    LfsLocalStorageTest.checkLocks(storage, user);
  }

  // Tests
  @Test
  void testApiConnectPassword() throws Exception {
    ApiClient apiClient = new ApiClient();
    apiClient.setBasePath(giteaApiUrl);
    apiClient.setUsername(administrator);
    apiClient.setPassword(administratorPassword);
    UserApi userApi = new UserApi(apiClient);
    User user = userApi.userGetCurrent();
    Assert.assertNotNull(user);
    Assert.assertEquals(user.getLogin(), administrator);
  }

  @Test
  void testGiteaContextConnect() throws Exception {
    ApiClient apiClient = GiteaContext.connect(giteaApiUrl, administratorToken);
    UserApi userApi = new UserApi(apiClient);
    User user = userApi.userGetCurrent();
    Assert.assertNotNull(user);
    Assert.assertEquals(user.getLogin(), administrator);
  }

  @Test
  void testCheckAdminLogin() throws Exception {
    checkUser(administrator, administratorPassword);
  }

  private void checkUser(@NotNull String login, @NotNull String password) throws Exception {
    try (SvnTestServer server = createServer(administratorToken, null)) {
      server.openSvnRepository(login, password).getLatestRevision();
    }
  }

  // SvnTest Methods
  @NotNull
  private SvnTestServer createServer(@NotNull GiteaToken token, @Nullable Function<Path, RepositoryMappingConfig> mappingConfigCreator) throws Exception {
    final GiteaConfig giteaConfig = new GiteaConfig(giteaApiUrl, token);
    return SvnTestServer.createEmpty(new GiteaUserDBConfig(), mappingConfigCreator, false, SvnTestServer.LfsMode.None, giteaConfig);
  }

  @Test
  void testCheckUserLogin() throws Exception {
    checkUser(user, userPassword);
  }

  @Test
  void testInvalidPassword() {
    Assert.expectThrows(SVNAuthenticationException.class, () -> checkUser(administrator, "wrongpassword"));
  }

  @Test
  void testInvalidUser() {
    Assert.expectThrows(SVNAuthenticationException.class, () -> checkUser("wronguser", administratorPassword));
  }

  @Test
  void testGiteaMapping() throws Exception {
    try (SvnTestServer server = createServer(administratorToken, dir -> new GiteaMappingConfig(dir, GitCreateMode.EMPTY))) {
      // Test user can get own private repo
      openSvnRepository(server, testPrivateRepository, user, userPassword).getLatestRevision();
      // Collaborator cannot get test's private repo
      Assert.assertThrows(SVNAuthenticationException.class, () -> openSvnRepository(server, testPrivateRepository, collaborator, collaboratorPassword).getLatestRevision());
      // Anyone can get public repo
      openSvnRepository(server, testPublicRepository, "anonymous", "nopassword").getLatestRevision();
      // Collaborator can get public repo
      openSvnRepository(server, testPublicRepository, collaborator, collaboratorPassword).getLatestRevision();
      // Add collaborator to private repo
      repoAddCollaborator(testPrivateRepository.getOwner().getLogin(), testPrivateRepository.getName(), collaborator);
      // Collaborator can get private repo
      openSvnRepository(server, testPrivateRepository, collaborator, collaboratorPassword).getLatestRevision();
    }
  }

  @NotNull
  private SVNRepository openSvnRepository(@NotNull SvnTestServer server, @NotNull Repository repository, @NotNull String username, @NotNull String password) throws SVNException {
    return SvnTestServer.openSvnRepository(server.getUrl(false).appendPath(repository.getFullName() + "/master", false), username, password);
  }

  private void repoAddCollaborator(@NotNull String owner, @NotNull String repo, @NotNull String collaborator) throws Exception {
    ApiClient apiClient = GiteaContext.connect(giteaApiUrl, administratorToken);
    RepositoryApi repositoryApi = new RepositoryApi(apiClient);
    AddCollaboratorOption aco = new AddCollaboratorOption();
    aco.setPermission("write");
    repositoryApi.repoAddCollaborator(owner, repo, collaborator, aco);
  }

  @AfterClass
  void after() {
    if (gitea != null) {
      gitea.stop();
      gitea = null;
    }
  }
}