package git

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"slices"
	"sort"
	"testing"
	"time"

	"github.com/go-git/go-billy/v6/memfs"
	"github.com/go-git/go-billy/v6/util"
	fixtures "github.com/go-git/go-git-fixtures/v6"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	_ "unsafe"

	"github.com/go-git/go-git/v6/config"
	"github.com/go-git/go-git/v6/plumbing"
	"github.com/go-git/go-git/v6/plumbing/cache"
	"github.com/go-git/go-git/v6/plumbing/filemode"
	formatcfg "github.com/go-git/go-git/v6/plumbing/format/config"
	"github.com/go-git/go-git/v6/plumbing/format/index"
	"github.com/go-git/go-git/v6/plumbing/object"
	"github.com/go-git/go-git/v6/plumbing/storer"
	"github.com/go-git/go-git/v6/plumbing/transport"
	"github.com/go-git/go-git/v6/storage/filesystem"
	"github.com/go-git/go-git/v6/storage/memory"
	"github.com/go-git/go-git/v6/x/plugin"
)

// This tests that buildTreeHelper correctly
// handles file entries with zero hashes. This was a bug where files with
// zero hashes would incorrectly trigger recursive tree building, causing
// a nil pointer dereference panic.
func TestBuildTreeHelper_1773(t *testing.T) {
	t.Parallel()

	fs := memfs.New()
	storage := memory.NewStorage()

	h := &buildTreeHelper{
		fs: fs,
		s:  storage,
	}

	// Create an index with a file that has a zero hash.
	idx := &index.Index{
		Entries: []*index.Entry{
			{
				Name: "file.txt",
				Mode: filemode.Regular,
				Hash: plumbing.ZeroHash,
			},
		},
	}

	_, err := h.BuildTree(idx, &CommitOptions{})
	require.NoError(t, err)
}

func (s *WorktreeSuite) TestCommitEmptyOptions() {
	fs := memfs.New()
	r, err := Init(memory.NewStorage(), WithWorkTree(fs))
	s.Require().NoError(err)
	defer func() { _ = r.Close() }()

	w, err := r.Worktree()
	s.Require().NoError(err)

	util.WriteFile(fs, "foo", []byte("foo"), 0o644)

	_, err = w.Add("foo")
	s.Require().NoError(err)

	hash, err := w.Commit("foo", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)
	s.False(hash.IsZero())

	commit, err := r.CommitObject(hash)
	s.Require().NoError(err)
	s.NotEqual("", commit.Author.Name)
}

func (s *WorktreeSuite) TestCommitInitial() {
	expected := plumbing.NewHash("98c4ac7c29c913f7461eae06e024dc18e80d23a4")

	fs := memfs.New()
	storage := memory.NewStorage()

	r, err := Init(storage, WithWorkTree(fs))
	s.Require().NoError(err)
	defer func() { _ = r.Close() }()

	w, err := r.Worktree()
	s.Require().NoError(err)

	util.WriteFile(fs, "foo", []byte("foo"), 0o644)

	_, err = w.Add("foo")
	s.Require().NoError(err)

	hash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature()})
	s.Equal(expected, hash)
	s.Require().NoError(err)

	assertStorageStatus(s, r, 1, 1, 1, expected)
}

func (s *WorktreeSuite) TestCommitInitialObjectFormats() {
	tests := []struct {
		name         string
		objectFormat formatcfg.ObjectFormat
		expectedHash string
	}{
		{
			name:         "sha1",
			objectFormat: formatcfg.SHA1,
			expectedHash: "98c4ac7c29c913f7461eae06e024dc18e80d23a4",
		},
		{
			name:         "sha256",
			objectFormat: formatcfg.SHA256,
			expectedHash: "9b86829db2f5159fe91032056e9aa60fc400c233bb2fc91ed7e966b996b2cd30",
		},
	}

	for _, tt := range tests {
		s.Run(tt.name, func() {
			expected := plumbing.NewHash(tt.expectedHash)
			fs := memfs.New()
			storage := memory.NewStorage(memory.WithObjectFormat(tt.objectFormat))

			r, err := Init(storage, WithWorkTree(fs), WithObjectFormat(tt.objectFormat))
			s.Require().NoError(err)
			defer func() { _ = r.Close() }()

			w, err := r.Worktree()
			s.Require().NoError(err)

			err = util.WriteFile(fs, "foo", []byte("foo"), 0o644)
			s.Require().NoError(err)

			_, err = w.Add("foo")
			s.Require().NoError(err)

			hash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature()})
			s.Require().NoError(err)
			s.Equal(expected, hash)
			s.Len(hash.String(), tt.objectFormat.HexSize())
			s.Require().NoError(storage.HasEncodedObject(hash))

			commit, err := r.CommitObject(hash)
			s.Require().NoError(err)
			s.Equal(hash, commit.Hash)
			s.Equal("foo\n", commit.Message)
			s.Len(commit.TreeHash.String(), tt.objectFormat.HexSize())
			s.Require().NoError(storage.HasEncodedObject(commit.TreeHash))

			tree, err := commit.Tree()
			s.Require().NoError(err)
			file, err := tree.File("foo")
			s.Require().NoError(err)
			s.Len(file.Hash.String(), tt.objectFormat.HexSize())
			s.Require().NoError(storage.HasEncodedObject(file.Hash))
			content, err := file.Contents()
			s.Require().NoError(err)
			s.Equal("foo", content)

			assertStorageStatus(s, r, 1, 1, 1, hash)
		})
	}
}

func (s *WorktreeSuite) TestSetReferencesInSHA256Repository() {
	fs := memfs.New()
	storage := memory.NewStorage(memory.WithObjectFormat(formatcfg.SHA256))

	r, err := Init(storage, WithWorkTree(fs), WithObjectFormat(formatcfg.SHA256))
	s.Require().NoError(err)
	defer func() { _ = r.Close() }()

	w, err := r.Worktree()
	s.Require().NoError(err)

	err = util.WriteFile(fs, "foo", []byte("foo"), 0o644)
	s.Require().NoError(err)

	_, err = w.Add("foo")
	s.Require().NoError(err)

	hash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)
	s.Len(hash.String(), formatcfg.SHA256.HexSize())

	tests := []struct {
		name string
		ref  plumbing.ReferenceName
	}{
		{name: "branch", ref: plumbing.ReferenceName("refs/heads/feature")},
		{name: "tag", ref: plumbing.ReferenceName("refs/tags/v1.0.0")},
	}

	for _, tt := range tests {
		s.Run(tt.name, func() {
			ref := plumbing.NewHashReference(tt.ref, hash)
			err := r.Storer.SetReference(ref)
			s.Require().NoError(err)

			got, err := r.Reference(tt.ref, false)
			s.Require().NoError(err)
			s.Equal(ref.Name(), got.Name())
			s.Equal(hash, got.Hash())
			s.Len(got.Hash().String(), formatcfg.SHA256.HexSize())
		})
	}
}

func (s *WorktreeSuite) TestNothingToCommit() {
	expected := plumbing.NewHash("838ea833ce893e8555907e5ef224aa076f5e274a")

	r, err := Init(memory.NewStorage(), WithWorkTree(memfs.New()))
	s.Require().NoError(err)
	defer func() { _ = r.Close() }()

	w, err := r.Worktree()
	s.Require().NoError(err)

	hash, err := w.Commit("failed empty commit\n", &CommitOptions{Author: defaultSignature()})
	s.Equal(plumbing.ZeroHash, hash)
	s.ErrorIs(err, ErrEmptyCommit)

	hash, err = w.Commit("enable empty commits\n", &CommitOptions{Author: defaultSignature(), AllowEmptyCommits: true})
	s.Equal(expected, hash)
	s.Require().NoError(err)
}

func (s *WorktreeSuite) TestNothingToCommitNonEmptyRepo() {
	fs := memfs.New()
	r, err := Init(memory.NewStorage(), WithWorkTree(fs))
	s.Require().NoError(err)
	defer func() { _ = r.Close() }()

	w, err := r.Worktree()
	s.Require().NoError(err)

	err = util.WriteFile(fs, "foo", []byte("foo"), 0o644)
	s.Require().NoError(err)

	w.Add("foo")
	_, err = w.Commit("previous commit\n", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)

	hash, err := w.Commit("failed empty commit\n", &CommitOptions{Author: defaultSignature()})
	s.Equal(plumbing.ZeroHash, hash)
	s.ErrorIs(err, ErrEmptyCommit)

	_, err = w.Commit("enable empty commits\n", &CommitOptions{Author: defaultSignature(), AllowEmptyCommits: true})
	s.Require().NoError(err)
}

func (s *WorktreeSuite) TestRemoveAndCommitToMakeEmptyRepo() {
	fs := memfs.New()
	r, err := Init(memory.NewStorage(), WithWorkTree(fs))
	s.Require().NoError(err)
	defer func() { _ = r.Close() }()

	w, err := r.Worktree()
	s.Require().NoError(err)

	err = util.WriteFile(fs, "foo", []byte("foo"), 0o644)
	s.Require().NoError(err)

	_, err = w.Add("foo")
	s.Require().NoError(err)

	_, err = w.Commit("Add in Repo\n", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)

	err = fs.Remove("foo")
	s.Require().NoError(err)

	_, err = w.Add("foo")
	s.Require().NoError(err)

	_, err = w.Commit("Remove foo\n", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)
}

func (s *WorktreeSuite) TestCommitParent() {
	expected := plumbing.NewHash("ef3ca05477530b37f48564be33ddd48063fc7a22")

	fs := memfs.New()
	w := &Worktree{
		r:          s.Repository,
		filesystem: newWorktreeFilesystem(fs, defaultProtectNTFS(), defaultProtectHFS()),
	}

	err := w.Checkout(&CheckoutOptions{})
	s.Require().NoError(err)

	err = util.WriteFile(fs, "foo", []byte("foo"), 0o644)
	s.Require().NoError(err)

	_, err = w.Add("foo")
	s.Require().NoError(err)

	hash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature()})
	s.Equal(expected, hash)
	s.Require().NoError(err)

	assertStorageStatus(s, s.Repository, 13, 11, 10, expected)
}

func (s *WorktreeSuite) TestCommitAmendWithoutChanges() {
	fs := memfs.New()
	w := &Worktree{
		r:          s.Repository,
		filesystem: newWorktreeFilesystem(fs, defaultProtectNTFS(), defaultProtectHFS()),
	}

	err := w.Checkout(&CheckoutOptions{})
	s.Require().NoError(err)

	err = util.WriteFile(fs, "foo", []byte("foo"), 0o644)
	s.Require().NoError(err)

	_, err = w.Add("foo")
	s.Require().NoError(err)

	prevHash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)

	amendedHash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature(), Amend: true})
	s.Require().NoError(err)

	headRef, err := w.r.Head()
	s.Require().NoError(err)

	s.Equal(headRef.Hash(), amendedHash)
	s.Equal(prevHash, amendedHash)

	commit, err := w.r.CommitObject(headRef.Hash())
	s.Require().NoError(err)
	s.Equal("foo\n", commit.Message)

	assertStorageStatus(s, s.Repository, 13, 11, 10, amendedHash)
}

func (s *WorktreeSuite) TestCommitAmendWithChanges() {
	fs := memfs.New()
	w := &Worktree{
		r:          s.Repository,
		filesystem: newWorktreeFilesystem(fs, defaultProtectNTFS(), defaultProtectHFS()),
	}

	err := w.Checkout(&CheckoutOptions{})
	s.Require().NoError(err)

	util.WriteFile(fs, "foo", []byte("foo"), 0o644)

	_, err = w.Add("foo")
	s.Require().NoError(err)

	_, err = w.Commit("foo\n", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)

	util.WriteFile(fs, "bar", []byte("bar"), 0o644)

	_, err = w.Add("bar")
	s.Require().NoError(err)

	amendedHash, err := w.Commit("bar\n", &CommitOptions{Author: defaultSignature(), Amend: true})
	s.Require().NoError(err)

	headRef, err := w.r.Head()
	s.Require().NoError(err)

	s.Equal(headRef.Hash(), amendedHash)

	commit, err := w.r.CommitObject(headRef.Hash())
	s.Require().NoError(err)
	s.Equal("bar\n", commit.Message)
	s.Equal(1, commit.NumParents())

	stats, err := commit.Stats()
	s.Require().NoError(err)
	s.Len(stats, 2)
	s.Equal(object.FileStat{
		Name:     "bar",
		Addition: 1,
	}, stats[0])
	s.Equal(object.FileStat{
		Name:     "foo",
		Addition: 1,
	}, stats[1])

	assertStorageStatus(s, s.Repository, 14, 12, 11, amendedHash)
}

func (s *WorktreeSuite) TestCommitAmendNothingToCommit() {
	fs := memfs.New()
	w := &Worktree{
		r:          s.Repository,
		filesystem: newWorktreeFilesystem(fs, defaultProtectNTFS(), defaultProtectHFS()),
	}

	err := w.Checkout(&CheckoutOptions{})
	s.Require().NoError(err)

	err = util.WriteFile(fs, "foo", []byte("foo"), 0o644)
	s.Require().NoError(err)

	_, err = w.Add("foo")
	s.Require().NoError(err)

	prevHash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)

	_, err = w.Commit("bar\n", &CommitOptions{Author: defaultSignature(), AllowEmptyCommits: true})
	s.Require().NoError(err)

	amendedHash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature(), Amend: true})
	s.T().Log(prevHash, amendedHash)
	s.ErrorIs(err, ErrEmptyCommit)
	s.Equal(plumbing.ZeroHash, amendedHash)
}

func TestCount(t *testing.T) {
	t.Parallel()
	f := fixtures.Basic().One()
	r := NewRepositoryWithEmptyWorktree(f)
	defer func() { _ = r.Close() }()

	iter, err := r.CommitObjects()
	require.NoError(t, err)

	count := 0
	iter.ForEach(func(*object.Commit) error {
		count++
		return nil
	})
	assert.Equal(t, 9, count, "commits mismatch")

	trees, err := r.TreeObjects()
	require.NoError(t, err)

	count = 0
	trees.ForEach(func(*object.Tree) error {
		count++
		return nil
	})
	assert.Equal(t, 12, count, "trees mismatch")

	blobs, err := r.BlobObjects()
	require.NoError(t, err)

	count = 0
	blobs.ForEach(func(*object.Blob) error {
		count++
		return nil
	})
	assert.Equal(t, 10, count, "blobs mismatch")

	objects, err := r.Objects()
	require.NoError(t, err)

	count = 0
	objects.ForEach(func(object.Object) error {
		count++
		return nil
	})
	assert.Equal(t, 31, count, "objects mismatch")
}

func TestAddAndCommitWithSkipStatus(t *testing.T) {
	t.Parallel()
	expected := plumbing.NewHash("375a3808ffde7f129cdd3c8c252fd0fe37cfd13b")

	f := fixtures.Basic().One()
	fs := memfs.New()
	r := NewRepositoryWithEmptyWorktree(f)
	defer func() { _ = r.Close() }()
	w := &Worktree{
		r:          r,
		filesystem: newWorktreeFilesystem(fs, defaultProtectNTFS(), defaultProtectHFS()),
	}

	err := w.Checkout(&CheckoutOptions{})
	require.NoError(t, err)

	util.WriteFile(fs, "LICENSE", []byte("foo"), 0o644)
	util.WriteFile(fs, "foo", []byte("foo"), 0o644)

	err = w.AddWithOptions(&AddOptions{
		Path:       "foo",
		SkipStatus: true,
	})
	require.NoError(t, err)

	hash, err := w.Commit("commit foo only\n", &CommitOptions{
		Author: defaultSignature(),
	})

	assert.Equal(t, expected.String(), hash.String())
	require.NoError(t, err)

	assertStorage(t, r, 13, 11, 10, expected)
}

func assertStorage(
	t *testing.T, r *Repository,
	treesCount, blobCount, commitCount int, head plumbing.Hash,
) {
	trees, err := r.Storer.IterEncodedObjects(plumbing.TreeObject)
	require.NoError(t, err)
	blobs, err := r.Storer.IterEncodedObjects(plumbing.BlobObject)
	require.NoError(t, err)
	commits, err := r.Storer.IterEncodedObjects(plumbing.CommitObject)
	require.NoError(t, err)

	assert.Equal(t, treesCount, lenIterEncodedObjects(trees), "trees count mismatch")
	assert.Equal(t, blobCount, lenIterEncodedObjects(blobs), "blobs count mismatch")
	assert.Equal(t, commitCount, lenIterEncodedObjects(commits), "commits count mismatch")

	ref, err := r.Head()
	require.NoError(t, err)
	assert.Equal(t, head.String(), ref.Hash().String())
}

func (s *WorktreeSuite) TestAddAndCommitWithSkipStatusPathNotModified() {
	expected := plumbing.NewHash("375a3808ffde7f129cdd3c8c252fd0fe37cfd13b")
	expected2 := plumbing.NewHash("8691273baf8f6ee2cccfc05e910552c04d02d472")

	fs := memfs.New()
	w := &Worktree{
		r:          s.Repository,
		filesystem: newWorktreeFilesystem(fs, defaultProtectNTFS(), defaultProtectHFS()),
	}

	err := w.Checkout(&CheckoutOptions{})
	s.Require().NoError(err)

	util.WriteFile(fs, "foo", []byte("foo"), 0o644)

	status, err := w.Status()
	s.Require().NoError(err)
	foo := status.File("foo")
	s.Equal(Untracked, foo.Staging)
	s.Equal(Untracked, foo.Worktree)

	err = w.AddWithOptions(&AddOptions{
		Path:       "foo",
		SkipStatus: true,
	})
	s.Require().NoError(err)

	status, err = w.Status()
	s.Require().NoError(err)
	foo = status.File("foo")
	s.Equal(Added, foo.Staging)
	s.Equal(Unmodified, foo.Worktree)

	hash, err := w.Commit("commit foo only\n", &CommitOptions{
		All:    true,
		Author: defaultSignature(),
	})
	s.Equal(expected, hash)
	s.Require().NoError(err)

	commit1, err := w.r.CommitObject(hash)
	s.Require().NoError(err)

	status, err = w.Status()
	s.Require().NoError(err)
	foo = status.File("foo")
	s.Equal(Untracked, foo.Staging)
	s.Equal(Untracked, foo.Worktree)

	assertStorageStatus(s, s.Repository, 13, 11, 10, expected)

	err = w.AddWithOptions(&AddOptions{
		Path:       "foo",
		SkipStatus: true,
	})
	s.Require().NoError(err)

	status, err = w.Status()
	s.Require().NoError(err)
	foo = status.File("foo")
	s.Equal(Untracked, foo.Staging)
	s.Equal(Untracked, foo.Worktree)

	hash, err = w.Commit("commit with no changes\n", &CommitOptions{
		Author:            defaultSignature(),
		AllowEmptyCommits: true,
	})
	s.Equal(expected2, hash)
	s.Require().NoError(err)

	commit2, err := w.r.CommitObject(hash)
	s.Require().NoError(err)

	status, err = w.Status()
	s.Require().NoError(err)
	foo = status.File("foo")
	s.Equal(Untracked, foo.Staging)
	s.Equal(Untracked, foo.Worktree)

	patch, err := commit2.Patch(commit1)
	s.Require().NoError(err)
	files := patch.FilePatches()
	s.Nil(files)

	assertStorageStatus(s, s.Repository, 13, 11, 11, expected2)
}

func (s *WorktreeSuite) TestCommitAll() {
	expected := plumbing.NewHash("aede6f8c9c1c7ec9ca8d287c64b8ed151276fa28")

	fs := memfs.New()
	w := &Worktree{
		r:          s.Repository,
		filesystem: newWorktreeFilesystem(fs, defaultProtectNTFS(), defaultProtectHFS()),
	}

	err := w.Checkout(&CheckoutOptions{})
	s.Require().NoError(err)

	util.WriteFile(fs, "LICENSE", []byte("foo"), 0o644)
	util.WriteFile(fs, "foo", []byte("foo"), 0o644)

	hash, err := w.Commit("foo\n", &CommitOptions{
		All:    true,
		Author: defaultSignature(),
	})

	s.Equal(expected, hash)
	s.Require().NoError(err)

	assertStorageStatus(s, s.Repository, 13, 11, 10, expected)
}

func (s *WorktreeSuite) TestRemoveAndCommitAll() {
	expected := plumbing.NewHash("907cd576c6ced2ecd3dab34a72bf9cf65944b9a9")

	fs := memfs.New()
	w := &Worktree{
		r:          s.Repository,
		filesystem: newWorktreeFilesystem(fs, defaultProtectNTFS(), defaultProtectHFS()),
	}

	err := w.Checkout(&CheckoutOptions{})
	s.Require().NoError(err)

	util.WriteFile(fs, "foo", []byte("foo"), 0o644)
	_, err = w.Add("foo")
	s.Require().NoError(err)

	_, errFirst := w.Commit("Add in Repo\n", &CommitOptions{
		Author: defaultSignature(),
	})
	s.Nil(errFirst)

	errRemove := fs.Remove("foo")
	s.Nil(errRemove)

	hash, errSecond := w.Commit("Remove foo\n", &CommitOptions{
		All:    true,
		Author: defaultSignature(),
	})
	s.Nil(errSecond)

	s.Equal(expected, hash)
	s.Require().NoError(err)

	assertStorageStatus(s, s.Repository, 13, 11, 11, expected)
}

func (s *WorktreeSuite) TestCherryPick() {
	fs := memfs.New()

	r, err := Init(memory.NewStorage(), WithWorkTree(fs))
	s.Require().NoError(err, "init the repository")
	defer func() { _ = r.Close() }()

	w, err := r.Worktree()
	s.Require().NoError(err)
	// add README.md to the worktree
	err = util.WriteFile(fs, "README.md", []byte("README File"), 0o644)
	s.Require().NoError(err)
	w.Add("README.md")

	initHash, err := w.Commit("initial commit\n", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)

	// add two files to worktree
	err = util.WriteFile(fs, "foo", []byte("foo"), 0o644)
	s.Require().NoError(err)

	err = util.WriteFile(fs, "foobar", []byte("foo**bar"), 0o644)
	s.Require().NoError(err)

	_, err = w.Add("foo")
	s.Require().NoError(err)
	_, err = w.Add("foobar")
	s.Require().NoError(err)

	// commit the changes
	commitHash1, err := w.Commit("commit 1\n", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)
	s.False(commitHash1.IsZero(), "commit hash is zero")
	// modify the "foo" file
	err = util.WriteFile(fs, "foo", []byte("foo=bar"), 0o644)
	s.Require().NoError(err)
	_, err = w.Add("foo")
	s.Require().NoError(err)
	// commit the new changes to "foo" file
	commitHash2, err := w.Commit("commit 2\n", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)
	s.False(commitHash2.IsZero(), "commit hash is zero")
	// get commit objects to do cherry picking
	commit1, err := r.CommitObject(commitHash1)
	s.Require().NoError(err)
	s.NotEmpty(commit1)

	commit2, err := r.CommitObject(commitHash2)
	s.Require().NoError(err)
	s.NotEmpty(commit2)

	foobarFileContent, err := util.ReadFile(fs, "foobar")
	s.Require().NoError(err)
	s.Equal("foo**bar", string(foobarFileContent))

	err = w.Checkout(&CheckoutOptions{Hash: initHash})
	s.Require().NoError(err, "checkout to the default branch")

	err = w.CherryPick(&CommitOptions{Author: defaultSignature(), AllowEmptyCommits: true}, TheirsMergeStrategy, commit1)
	s.Require().NoError(err)

	foobarFileContent, err = util.ReadFile(fs, "foobar")
	s.Require().NoError(err)
	s.Equal("foo**bar", string(foobarFileContent))

	err = w.CherryPick(&CommitOptions{Author: defaultSignature(), AllowEmptyCommits: true}, TheirsMergeStrategy, commit2)
	s.Require().NoError(err)

	fooFileContent, err := util.ReadFile(fs, "foo")
	s.Require().NoError(err)
	s.Equal("foo=bar", string(fooFileContent))

	// delete the file
	rmHash, err := w.Remove("foo")
	s.Require().NoError(err)
	s.False(rmHash.IsZero())

	rmCommitHash, err := w.Commit("remove the file\n", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)

	rmCommit, err := r.CommitObject(rmCommitHash)
	s.Require().NoError(err)

	// go back to commit 2 and cherry pick the latest commit
	err = w.Checkout(&CheckoutOptions{
		Hash: commitHash2,
	})
	s.Require().NoError(err)

	err = w.CherryPick(&CommitOptions{Author: defaultSignature(), AllowEmptyCommits: true}, TheirsMergeStrategy, rmCommit)
	s.Require().NoError(err)

	fooFileContent, err = util.ReadFile(fs, "foo")
	s.Error(err)
	s.Nil(fooFileContent)

	// go back to commit 1 and cherry pick with "ours" strategy option
	err = w.Checkout(&CheckoutOptions{
		Hash: commitHash1,
	})
	s.Require().NoError(err)

	err = w.CherryPick(&CommitOptions{Author: defaultSignature(), AllowEmptyCommits: true}, OursMergeStrategy, rmCommit)
	s.Require().NoError(err)
	fooFileContent, err = util.ReadFile(fs, "foo")
	s.Require().NoError(err)
	s.Equal("foo", string(fooFileContent))

	// cherry-pick with no commit options
	err = w.CherryPick(nil, OursMergeStrategy, rmCommit)
	s.ErrorIs(err, ErrCannotCherryPickWithoutCommitOptions)
}

func (s *WorktreeSuite) TestCherryPickRejectsInvalidPaths() {
	fs := memfs.New()

	storage := memory.NewStorage()
	r, err := Init(storage, WithWorkTree(fs))
	s.Require().NoError(err)
	defer func() { _ = r.Close() }()

	w, err := r.Worktree()
	s.Require().NoError(err)

	err = util.WriteFile(fs, "foo", []byte("foo"), 0o644)
	s.Require().NoError(err)
	_, err = w.Add("foo")
	s.Require().NoError(err)

	initHash, err := w.Commit("initial commit\n", &CommitOptions{Author: defaultSignature()})
	s.Require().NoError(err)

	initCommit, err := r.CommitObject(initHash)
	s.Require().NoError(err)

	badContent := []byte("content")
	blobHash := s.storeBlob(storage, badContent)

	for _, name := range []string{
		".git/config",
		".git/objects/pack/file",
		"../file",
	} {
		badCommit := s.buildChildCommit(storage, initCommit, initHash, name, blobHash)

		err = w.Checkout(&CheckoutOptions{Hash: initHash})
		s.Require().NoError(err)

		err = w.CherryPick(&CommitOptions{Author: defaultSignature(), AllowEmptyCommits: true}, TheirsMergeStrategy, badCommit)
		s.Error(err, "expected cherry-pick to reject path %q", name)
	}
}

func (s *WorktreeSuite) storeBlob(storage *memory.Storage, content []byte) plumbing.Hash {
	s.T().Helper()

	obj := storage.NewEncodedObject()
	obj.SetType(plumbing.BlobObject)
	obj.SetSize(int64(len(content)))
	bw, err := obj.Writer()
	s.Require().NoError(err)
	_, err = bw.Write(content)
	s.Require().NoError(err)
	s.Require().NoError(bw.Close())
	h, err := storage.SetEncodedObject(obj)
	s.Require().NoError(err)
	return h
}

func (s *WorktreeSuite) buildChildCommit(storage *memory.Storage, parent *object.Commit, parentHash plumbing.Hash, filename string, blobHash plumbing.Hash) *object.Commit {
	s.T().Helper()

	parentTree, err := parent.Tree()
	s.Require().NoError(err)

	entries := slices.Clone(parentTree.Entries)
	entries = append(entries, object.TreeEntry{
		Name: filename,
		Mode: filemode.Regular,
		Hash: blobHash,
	})
	sort.Sort(object.TreeEntrySorter(entries))

	// Tree.Encode validates entry names through pathutil.ValidTreePath
	// and would refuse a path component like ".git/config" — the precise
	// shape this test needs to plant. Assemble the tree object bytes
	// directly so the malicious tree reaches the cherry-pick boundary.
	var buf bytes.Buffer
	for _, e := range entries {
		fmt.Fprintf(&buf, "%o %s", e.Mode, e.Name)
		buf.WriteByte(0)
		buf.Write(e.Hash.Bytes())
	}
	treeObj := storage.NewEncodedObject()
	treeObj.SetType(plumbing.TreeObject)
	tw, err := treeObj.Writer()
	s.Require().NoError(err)
	_, err = tw.Write(buf.Bytes())
	s.Require().NoError(err)
	s.Require().NoError(tw.Close())
	treeHash, err := storage.SetEncodedObject(treeObj)
	s.Require().NoError(err)

	commit := &object.Commit{
		Author:       *defaultSignature(),
		Committer:    *defaultSignature(),
		Message:      "add " + filename + "\n",
		TreeHash:     treeHash,
		ParentHashes: []plumbing.Hash{parentHash},
	}
	commitObj := storage.NewEncodedObject()
	err = commit.Encode(commitObj)
	s.Require().NoError(err)
	commitHash, err := storage.SetEncodedObject(commitObj)
	s.Require().NoError(err)

	result, err := object.GetCommit(storage, commitHash)
	s.Require().NoError(err)
	return result
}

func (s *WorktreeSuite) TestCommitTreeSort() {
	fs := s.TemporalFilesystem()

	st := filesystem.NewStorage(fs, cache.NewObjectLRUDefault())
	rInit, err := Init(st)
	s.Require().NoError(err)
	defer func() { _ = rInit.Close() }()

	r, err := Clone(memory.NewStorage(), memfs.New(), &CloneOptions{
		URL: fs.Root(),
	})
	s.ErrorIs(err, transport.ErrEmptyRemoteRepository)
	defer func() { _ = r.Close() }()

	w, err := r.Worktree()
	s.Require().NoError(err)

	mfs := w.filesystem

	err = mfs.MkdirAll("delta", 0o755)
	s.Require().NoError(err)

	for _, p := range []string{"delta_last", "Gamma", "delta/middle", "Beta", "delta-first", "alpha"} {
		util.WriteFile(mfs, p, []byte("foo"), 0o644)
		_, err = w.Add(p)
		s.Require().NoError(err)
	}

	_, err = w.Commit("foo\n", &CommitOptions{
		All:    true,
		Author: defaultSignature(),
	})
	s.Require().NoError(err)

	err = r.Push(&PushOptions{})
	s.Require().NoError(err)

	cmd := exec.Command("git", "fsck")
	cmd.Dir = fs.Root()
	cmd.Env = os.Environ()
	buf := &bytes.Buffer{}
	cmd.Stderr = buf
	cmd.Stdout = buf

	err = cmd.Run()
	s.Require().NoError(err, buf.String())
}

// https://github.com/go-git/go-git/pull/224
func (s *WorktreeSuite) TestJustStoreObjectsNotAlreadyStored() {
	fs := s.TemporalFilesystem()

	fsDotgit, err := fs.Chroot(".git") // real fs to get modified timestamps
	s.Require().NoError(err)
	storage := filesystem.NewStorage(fsDotgit, cache.NewObjectLRUDefault())

	r, err := Init(storage, WithWorkTree(fs))
	s.Require().NoError(err)
	defer func() { _ = r.Close() }()

	w, err := r.Worktree()
	s.Require().NoError(err)

	// Step 1: Write LICENSE
	util.WriteFile(fs, "LICENSE", []byte("license"), 0o644)
	hLicense, err := w.Add("LICENSE")
	s.Require().NoError(err)
	s.Equal(plumbing.NewHash("0484eba0d41636ba71fa612c78559cd6c3006cde"), hLicense)

	hash, err := w.Commit("commit 1\n", &CommitOptions{
		All:    true,
		Author: defaultSignature(),
	})
	s.Require().NoError(err)
	s.Equal(plumbing.NewHash("7a7faee4630d2664a6869677cc8ab614f3fd4a18"), hash)

	infoLicense, err := fsDotgit.Stat(filepath.Join("objects", "04", "84eba0d41636ba71fa612c78559cd6c3006cde"))
	s.Require().NoError(err) // checking objects file exists

	// Step 2: Write foo.
	time.Sleep(5 * time.Millisecond) // uncool, but we need to get different timestamps...
	util.WriteFile(fs, "foo", []byte("foo"), 0o644)
	hFoo, err := w.Add("foo")
	s.Require().NoError(err)
	s.Equal(plumbing.NewHash("19102815663d23f8b75a47e7a01965dcdc96468c"), hFoo)

	hash, err = w.Commit("commit 2\n", &CommitOptions{
		All:    true,
		Author: defaultSignature(),
	})
	s.Require().NoError(err)
	s.Equal(plumbing.NewHash("97c0c5177e6ac57d10e8ea0017f2d39b91e2b364"), hash)

	// Step 3: Check
	// There is no need to overwrite the object of LICENSE, because its content
	// was not changed. Just a write on the object of foo is required. This behaviour
	// is fixed by #224 and tested by comparing the timestamps of the stored objects.
	infoFoo, err := fsDotgit.Stat(filepath.Join("objects", "19", "102815663d23f8b75a47e7a01965dcdc96468c"))
	s.Require().NoError(err)                                // checking objects file exists
	s.True(infoLicense.ModTime().Before(infoFoo.ModTime())) // object of foo has another/greaterThan timestamp than LICENSE

	infoLicenseSecond, err := fsDotgit.Stat(filepath.Join("objects", "04", "84eba0d41636ba71fa612c78559cd6c3006cde"))
	s.Require().NoError(err)

	log.Printf("comparing mod time: %v == %v on %v (%v)", infoLicenseSecond.ModTime(), infoLicense.ModTime(), runtime.GOOS, runtime.GOARCH)
	s.Equal(infoLicense.ModTime(), infoLicenseSecond.ModTime()) // object of LICENSE should have the same timestamp because no additional write operation was performed
}

func (s *WorktreeSuite) TestCommitInvalidCharactersInAuthorInfos() {
	f := fixtures.Basic().One()
	s.Repository = NewRepositoryWithEmptyWorktree(f)
	r1 := s.Repository
	s.T().Cleanup(func() { _ = r1.Close() })

	expected := plumbing.NewHash("e8eecef2524c3a37cf0f0996603162f81e0373f1")

	fs := memfs.New()
	storage := memory.NewStorage()

	r2, err := Init(storage, WithWorkTree(fs))
	s.Require().NoError(err)
	defer func() { _ = r2.Close() }()

	w, err := r2.Worktree()
	s.Require().NoError(err)

	util.WriteFile(fs, "foo", []byte("foo"), 0o644)

	_, err = w.Add("foo")
	s.Require().NoError(err)

	hash, err := w.Commit("foo\n", &CommitOptions{Author: invalidSignature()})
	s.Equal(expected, hash)
	s.Require().NoError(err)

	assertStorageStatus(s, r2, 1, 1, 1, expected)

	// Check HEAD commit contains author informations with '<', '>' and '\n' stripped
	lr, err := r2.Log(&LogOptions{})
	s.Require().NoError(err)

	commit, err := lr.Next()
	s.Require().NoError(err)

	s.Equal("foo bad", commit.Author.Name)
	s.Equal("badfoo@foo.foo", commit.Author.Email)
}

func BenchmarkCommit(b *testing.B) {
	const (
		numFiles      = 100
		numSubdirs    = 5
		numGoroutines = 4
	)

	wt := setupBenchmarkRepo(b, numFiles, numSubdirs, numGoroutines)

	sig := &object.Signature{
		Name:  "Benchmark",
		Email: "benchmark@test.com",
		When:  time.Now(),
	}

	seq := 0
	b.Run("Commit", func(b *testing.B) {
		for range b.N {
			b.StopTimer()
			fileName := filepath.Join("dir0", fmt.Sprintf("bench_%d.txt", seq))
			err := util.WriteFile(wt.filesystem, fileName, fmt.Appendf(nil, "content %d", seq), 0o644)
			require.NoError(b, err)

			_, err = wt.Add(fileName)
			require.NoError(b, err)

			sig.When = time.Now()

			// Isolate the benchmark to the commit operation.
			b.StartTimer()
			_, err = wt.Commit(fmt.Sprintf("commit %d\n", seq), &CommitOptions{
				Author:    sig,
				Committer: sig,
			})
			b.StopTimer()

			require.NoError(b, err)
			seq++
		}
	})
}

func assertStorageStatus(
	s *WorktreeSuite, r *Repository,
	treesCount, blobCount, commitCount int, head plumbing.Hash,
) {
	trees, err := r.Storer.IterEncodedObjects(plumbing.TreeObject)
	s.Require().NoError(err)
	blobs, err := r.Storer.IterEncodedObjects(plumbing.BlobObject)
	s.Require().NoError(err)
	commits, err := r.Storer.IterEncodedObjects(plumbing.CommitObject)
	s.Require().NoError(err)

	s.Equal(treesCount, lenIterEncodedObjects(trees))
	s.Equal(blobCount, lenIterEncodedObjects(blobs))
	s.Equal(commitCount, lenIterEncodedObjects(commits))

	ref, err := r.Head()
	s.Require().NoError(err)
	s.Equal(head, ref.Hash())
}

func lenIterEncodedObjects(iter storer.EncodedObjectIter) int {
	count := 0
	iter.ForEach(func(plumbing.EncodedObject) error {
		count++
		return nil
	})

	return count
}

func defaultSignature() *object.Signature {
	when, _ := time.Parse(object.DateFormat, "Thu May 04 00:03:43 2017 +0200")
	return &object.Signature{
		Name:  "foo",
		Email: "foo@foo.foo",
		When:  when,
	}
}

func invalidSignature() *object.Signature {
	when, _ := time.Parse(object.DateFormat, "Thu May 04 00:03:43 2017 +0200")
	return &object.Signature{
		Name:  "foo <bad>\n",
		Email: "<bad>\nfoo@foo.foo",
		When:  when,
	}
}

type mockSigner struct {
	called bool
}

const mockSignature = "-----BEGIN PGP SIGNATURE-----\n\nmock-signature\n-----END PGP SIGNATURE-----"

func (m *mockSigner) Sign(_ context.Context, _ io.Reader) ([]byte, error) {
	m.called = true
	return []byte(mockSignature), nil
}

//go:linkname resetPluginEntry github.com/go-git/go-git/v6/x/plugin.resetEntry
func resetPluginEntry(name plugin.Name)

func TestBuildCommitObjectSignerSelection(t *testing.T) { //nolint:paralleltest // modifies global plugin state
	tests := []struct {
		name           string
		registerPlugin bool
		optionsSigner  Signer
		commitSignGpg  config.OptBool
		wantErr        string
		wantSignature  string
		wantPluginUsed bool
		wantOptionUsed bool
	}{
		{
			name:          "no signer at all produces unsigned commit",
			commitSignGpg: config.OptBoolFalse,
			wantSignature: "",
		},
		{
			name:           "CommitOptions.Signer works without plugin registered",
			optionsSigner:  &mockSigner{},
			commitSignGpg:  config.OptBoolFalse,
			wantSignature:  mockSignature + "\n",
			wantOptionUsed: true,
		},
		{
			name:           "plugin signer is used when CommitOptions.Signer is nil",
			registerPlugin: true,
			commitSignGpg:  config.OptBoolTrue,
			wantSignature:  mockSignature + "\n",
			wantPluginUsed: true,
		},
		{
			name:           "plugin signer is ignored if commit.signGpg=false",
			registerPlugin: true,
			commitSignGpg:  config.OptBoolFalse,
			wantSignature:  "",
			wantPluginUsed: false,
		},
		{
			name:          "error if commit.signGpg=true and no plugin registered",
			commitSignGpg: config.OptBoolTrue,
			wantSignature: "",
			wantErr:       "cannot auto-sign commit: disable commit.gpgSign or register an ObjectSigner plugin",
		},
		{
			name:           "CommitOptions.Signer takes precedence over plugin",
			registerPlugin: true,
			commitSignGpg:  config.OptBoolTrue,
			optionsSigner:  &mockSigner{},
			wantSignature:  mockSignature + "\n",
			wantOptionUsed: true,
		},
	}

	for _, tt := range tests { //nolint:paralleltest // modifies global plugin state
		t.Run(tt.name, func(t *testing.T) {
			resetPluginEntry("object-signer")
			t.Cleanup(func() { resetPluginEntry("object-signer") })

			var pluginSigner *mockSigner
			if tt.registerPlugin {
				pluginSigner = &mockSigner{}
				err := plugin.Register(plugin.ObjectSigner(), func() plugin.Signer { return pluginSigner })
				require.NoError(t, err)
			}

			fs := memfs.New()
			r, err := Init(memory.NewStorage(), WithWorkTree(fs))
			require.NoError(t, err)
			defer func() { _ = r.Close() }()

			cfg, err := r.Config()
			require.NoError(t, err)

			cfg.Commit.GpgSign = tt.commitSignGpg
			err = r.SetConfig(cfg)
			require.NoError(t, err)

			w, err := r.Worktree()
			require.NoError(t, err)

			util.WriteFile(fs, "file.txt", []byte("content"), 0o644)
			_, err = w.Add("file.txt")
			require.NoError(t, err)

			hash, err := w.Commit("test commit\n", &CommitOptions{
				Author: defaultSignature(),
				Signer: tt.optionsSigner,
			})
			if tt.wantErr == "" {
				require.NoError(t, err)
			} else {
				require.ErrorContains(t, err, tt.wantErr)
				return // no need to carry on
			}

			commit, err := r.CommitObject(hash)
			require.NoError(t, err)
			assert.Equal(t, tt.wantSignature, commit.Signature)

			if tt.wantPluginUsed {
				require.NotNil(t, pluginSigner)
				assert.True(t, pluginSigner.called, "expected plugin signer to be called")
			}

			if tt.wantOptionUsed {
				optSigner, ok := tt.optionsSigner.(*mockSigner)
				require.True(t, ok)
				assert.True(t, optSigner.called, "expected options signer to be called")
			}

			if pluginSigner != nil && tt.optionsSigner != nil {
				assert.False(t, pluginSigner.called,
					"plugin signer should not be called when CommitOptions.Signer is set")
			}
		})
	}
}
