/*
 * Copyright (C) 2022 Nail Samatov <sanail@yandex.ru> and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */
package org.eclipse.jgit.lfs.internal;

import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;

import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.RemoteAddCommand;
import org.eclipse.jgit.attributes.FilterCommandRegistry;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lfs.CleanFilter;
import org.eclipse.jgit.lfs.Protocol;
import org.eclipse.jgit.lfs.SmudgeFilter;
import org.eclipse.jgit.lfs.errors.LfsConfigInvalidException;
import org.eclipse.jgit.lfs.lib.Constants;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.transport.http.HttpConnection;
import org.eclipse.jgit.util.HttpSupport;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class LfsConnectionFactoryTest extends RepositoryTestCase {

	private static final String SMUDGE_NAME = org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX
			+ Constants.ATTR_FILTER_DRIVER_PREFIX
			+ org.eclipse.jgit.lib.Constants.ATTR_FILTER_TYPE_SMUDGE;

	private static final String CLEAN_NAME = org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX
			+ Constants.ATTR_FILTER_DRIVER_PREFIX
			+ org.eclipse.jgit.lib.Constants.ATTR_FILTER_TYPE_CLEAN;

	private final static String LFS_SERVER_URL1 = "https://lfs.server1/test/uri";

	private final static String LFS_SERVER_URL2 = "https://lfs.server2/test/uri";

	private final static String ORIGIN_URL = "https://git.server/test/uri";

	private Git git;

	@BeforeClass
	public static void installLfs() {
		FilterCommandRegistry.register(SMUDGE_NAME, SmudgeFilter.FACTORY);
		FilterCommandRegistry.register(CLEAN_NAME, CleanFilter.FACTORY);
	}

	@AfterClass
	public static void removeLfs() {
		FilterCommandRegistry.unregister(SMUDGE_NAME);
		FilterCommandRegistry.unregister(CLEAN_NAME);
	}

	@Override
	@Before
	public void setUp() throws Exception {
		super.setUp();
		git = new Git(db);

		// Just to have a non empty repo
		writeTrashFile("Test.txt", "Hello world from the LFS Factory Test");
		git.add().addFilepattern("Test.txt").call();
		git.commit().setMessage("Initial commit").call();
	}

	@Test
	public void lfsUrlFromRemoteUrlWithDotGit() throws Exception {
		addRemoteUrl("https://localhost/repo.git");
		checkLfsUrl("https://localhost/repo.git/info/lfs");
	}

	@Test
	public void lfsUrlFromRemoteUrlWithoutDotGit() throws Exception {
		addRemoteUrl("https://localhost/repo");
		checkLfsUrl("https://localhost/repo.git/info/lfs");
	}

	@Test
	public void lfsUrlFromLocalConfig() throws Exception {
		addRemoteUrl("https://localhost/repo");

		StoredConfig cfg = ((Repository) db).getConfig();
		cfg.setString(ConfigConstants.CONFIG_SECTION_LFS,
				null,
				ConfigConstants.CONFIG_KEY_URL,
				"https://localhost/repo/lfs");
		cfg.save();

		checkLfsUrl("https://localhost/repo/lfs");
	}

	@Test
	public void lfsUrlFromOriginConfig() throws Exception {
		addRemoteUrl("https://localhost/repo");

		StoredConfig cfg = ((Repository) db).getConfig();
		cfg.setString(ConfigConstants.CONFIG_SECTION_LFS,
				org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME,
				ConfigConstants.CONFIG_KEY_URL,
				"https://localhost/repo/lfs");
		cfg.save();

		checkLfsUrl("https://localhost/repo/lfs");
	}

	@Test
	public void lfsUrlNotConfigured() throws Exception {
		assertThrows(LfsConfigInvalidException.class,
				() -> LfsConnectionFactory.getLfsConnection(db,
				HttpSupport.METHOD_POST, Protocol.OPERATION_DOWNLOAD));
	}

	@Test
	public void checkGetLfsConnection_lfsurl_lfsconfigFromWorkingDir()
			throws Exception {
		writeLfsConfig();
		checkLfsUrl(LFS_SERVER_URL1);
	}

	@Test
	public void checkGetLfsConnection_lfsurl_lfsconfigFromIndex()
			throws Exception {
		writeLfsConfig();
		git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call();
		deleteTrashFile(Constants.DOT_LFS_CONFIG);
		checkLfsUrl(LFS_SERVER_URL1);
	}

	@Test
	public void checkGetLfsConnection_lfsurl_lfsconfigFromHEAD()
			throws Exception {
		writeLfsConfig();
		git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call();
		git.commit().setMessage("Commit LFS Config").call();

		/*
		 * reading .lfsconfig from HEAD seems only testable using a bare repo,
		 * since otherwise working tree or index are used
		 */
		File directory = createTempDirectory("testBareRepo");
		try (Repository bareRepoDb = Git.cloneRepository()
				.setDirectory(directory)
				.setURI(db.getDirectory().toURI().toString()).setBare(true)
				.call().getRepository()) {

			checkLfsUrl(LFS_SERVER_URL1);
		}
	}

	@Test
	public void checkGetLfsConnection_remote_lfsconfigFromWorkingDir()
			throws Exception {
		addRemoteUrl(ORIGIN_URL);
		writeLfsConfig(LFS_SERVER_URL1, "lfs", DEFAULT_REMOTE_NAME, "url");
		checkLfsUrl(LFS_SERVER_URL1);
	}

	/**
	 * Test the config file precedence.
	 *
	 * Checking only with the local repository config is sufficient since from
	 * that point the "normal" precedence is used.
	 *
	 * @throws Exception
	 */
	@Test
	public void checkGetLfsConnection_ConfigFilePrecedence_lfsconfigFromWorkingDir()
			throws Exception {
		writeLfsConfig();
		checkLfsUrl(LFS_SERVER_URL1);

		StoredConfig config = git.getRepository().getConfig();
		config.setString(ConfigConstants.CONFIG_SECTION_LFS, null,
				ConfigConstants.CONFIG_KEY_URL, LFS_SERVER_URL2);
		config.save();

		checkLfsUrl(LFS_SERVER_URL2);
	}

	@Test
	public void checkGetLfsConnection_InvalidLfsConfig_WorkingDir()
			throws Exception {
		writeInvalidLfsConfig();
		LfsConfigInvalidException actualException = assertThrows(
				LfsConfigInvalidException.class, () -> {
			LfsConnectionFactory.getLfsConnection(db, HttpSupport.METHOD_POST,
					Protocol.OPERATION_DOWNLOAD);
		});
		assertTrue(getStackTrace(actualException)
				.contains("Invalid line in config file"));
	}

	@Test
	public void checkGetLfsConnection_InvalidLfsConfig_Index()
			throws Exception {
		writeInvalidLfsConfig();
		git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call();
		deleteTrashFile(Constants.DOT_LFS_CONFIG);
		LfsConfigInvalidException actualException = assertThrows(
				LfsConfigInvalidException.class, () -> {
			LfsConnectionFactory.getLfsConnection(db, HttpSupport.METHOD_POST,
					Protocol.OPERATION_DOWNLOAD);
		});
		assertTrue(getStackTrace(actualException)
				.contains("Invalid line in config file"));
	}

	@Test
	public void checkGetLfsConnection_InvalidLfsConfig_HEAD() throws Exception {
		writeInvalidLfsConfig();
		git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call();
		git.commit().setMessage("Commit LFS Config").call();

		/*
		 * reading .lfsconfig from HEAD seems only testable using a bare repo,
		 * since otherwise working tree or index are used
		 */
		File directory = createTempDirectory("testBareRepo");
		try (Repository bareRepoDb = Git.cloneRepository()
				.setDirectory(directory)
				.setURI(db.getDirectory().toURI().toString()).setBare(true)
				.call().getRepository()) {
			LfsConfigInvalidException actualException = assertThrows(
					LfsConfigInvalidException.class,
					() -> {
						LfsConnectionFactory.getLfsConnection(db,
								HttpSupport.METHOD_POST,
								Protocol.OPERATION_DOWNLOAD);
					});
			assertTrue(getStackTrace(actualException)
					.contains("Invalid line in config file"));
		}
	}

	private void addRemoteUrl(String remotUrl) throws Exception {
		RemoteAddCommand add = git.remoteAdd();
		add.setUri(new URIish(remotUrl));
		add.setName(org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME);
		add.call();
	}

	/**
	 * Returns the stack trace of the provided exception as string
	 *
	 * @param actualException
	 * @return The exception stack trace as string
	 */
	private String getStackTrace(Exception actualException) {
		StringWriter sw = new StringWriter();
		PrintWriter pw = new PrintWriter(sw);
		actualException.printStackTrace(pw);
		return sw.toString();
	}

	private void writeLfsConfig() throws IOException {
		writeLfsConfig(LFS_SERVER_URL1, "lfs", "url");
	}

	private void writeLfsConfig(String lfsUrl, String section, String name)
			throws IOException {
		writeLfsConfig(lfsUrl, section, null, name);
	}

	/*
	 * Write simple lfs config with single entry. Do not use FileBasedConfig to
	 * avoid introducing new dependency (for now).
	 */
	private void writeLfsConfig(String lfsUrl, String section,
			String subsection, String name) throws IOException {
		StringBuilder config = new StringBuilder();
		config.append("[");
		config.append(section);
		if (subsection != null) {
			config.append(" \"");
			config.append(subsection);
			config.append("\"");
		}
		config.append("]\n");
		config.append("    ");
		config.append(name);
		config.append(" = ");
		config.append(lfsUrl);
		writeTrashFile(Constants.DOT_LFS_CONFIG, config.toString());
	}

	private void writeInvalidLfsConfig() throws IOException {
		writeTrashFile(Constants.DOT_LFS_CONFIG,
				"{lfs]\n    url = " + LFS_SERVER_URL1);
	}

	private void checkLfsUrl(String lfsUrl) throws IOException {
		HttpConnection lfsServerConn;
		lfsServerConn = LfsConnectionFactory.getLfsConnection(db,
				HttpSupport.METHOD_POST, Protocol.OPERATION_DOWNLOAD);

		assertEquals(lfsUrl + Protocol.OBJECTS_LFS_ENDPOINT,
				lfsServerConn.getURL().toString());
	}
}
