package archive

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/gitaly/v16/internal/git/gittest"
	"gitlab.com/gitlab-org/gitaly/v16/internal/helper/perm"
	"gitlab.com/gitlab-org/gitaly/v16/internal/testhelper"
	"gitlab.com/gitlab-org/gitaly/v16/internal/testhelper/testcfg"
)

func TestWriteTarball(t *testing.T) {
	t.Parallel()

	ctx := testhelper.Context(t)
	tempDir := testhelper.TempDir(t)
	srcDir := filepath.Join(tempDir, "src")

	// regular file
	writeFile(t, filepath.Join(srcDir, "a.txt"), []byte("a"))
	// empty dir
	require.NoError(t, os.Mkdir(filepath.Join(srcDir, "empty_dir"), perm.PublicDir))
	// file with long name
	writeFile(t, filepath.Join(srcDir, strings.Repeat("b", 150)+".txt"), []byte("b"))
	// regular file that is not expected to be part of the archive (not in the members list)
	writeFile(t, filepath.Join(srcDir, "excluded.txt"), []byte("excluded"))
	// folder with multiple files all expected to be archived
	nestedPath := filepath.Join(srcDir, "nested1")
	for i := 0; i < readDirEntriesPageSize+1; i++ {
		writeFile(t, filepath.Join(nestedPath, fmt.Sprintf("%d.txt", i)), []byte{byte(i)})
	}
	// nested file that is not expected to be part of the archive
	writeFile(t, filepath.Join(srcDir, "nested2/nested/nested/c.txt"), []byte("c"))
	// deeply nested file
	writeFile(t, filepath.Join(srcDir, "nested2/nested/nested/nested/nested/d.txt"), []byte("d"))
	// file that is used to create a symbolic link, is not expected to be part of the archive
	writeFile(t, filepath.Join(srcDir, "nested3/target.txt"), []byte("target"))
	// link to the file above
	require.NoError(t, os.Symlink(filepath.Join(srcDir, "nested3/target.txt"), filepath.Join(srcDir, "link.to.target.txt")))
	// directory that is a target of the symlink should not be archived
	writeFile(t, filepath.Join(srcDir, "nested4/stub.txt"), []byte("symlinked"))
	// link to the folder above
	require.NoError(t, os.Symlink(filepath.Join(srcDir, "nested4"), filepath.Join(srcDir, "link.to.nested4")))

	tarPath := filepath.Join(tempDir, "out.tar")
	archFile, err := os.Create(tarPath)
	require.NoError(t, err)
	defer func() { _ = archFile.Close() }()

	err = writeTarball(
		ctx,
		archFile,
		srcDir,
		"a.txt",
		strings.Repeat("b", 150)+".txt",
		"nested1",
		"nested2/nested/nested/nested/nested/d.txt",
		"link.to.target.txt",
		"link.to.nested4",
	)
	require.NoError(t, err)
	require.NoError(t, archFile.Close())

	cfg := testcfg.Build(t)

	dstDir := filepath.Join(tempDir, "dst")
	require.NoError(t, os.Mkdir(dstDir, perm.PublicDir))
	output, err := exec.Command("tar", "-xf", tarPath, "-C", dstDir).CombinedOutput()
	require.NoErrorf(t, err, "%s", output)
	diff := gittest.ExecOpts(t, cfg, gittest.ExecConfig{ExpectedExitCode: 1}, "diff", "--no-index", "--name-only", "--exit-code", dstDir, srcDir)
	expected := strings.Join(
		[]string{
			filepath.Join(srcDir, "excluded.txt"),
			filepath.Join(srcDir, "nested2/nested/nested/c.txt"),
			filepath.Join(srcDir, "nested3/target.txt"),
			filepath.Join(srcDir, "nested4/stub.txt\n"),
		},
		"\n",
	)
	require.Equal(t, expected, string(diff))
}

func writeFile(tb testing.TB, path string, data []byte) {
	tb.Helper()
	require.NoError(tb, os.MkdirAll(filepath.Dir(path), perm.PrivateDir))
	require.NoError(tb, os.WriteFile(path, data, perm.PrivateFile))
}
