package helpers

import (
	"testing"

	"github.com/jesseduffield/lazygit/pkg/commands/models"
	"github.com/jesseduffield/lazygit/pkg/utils"
	"github.com/samber/lo"
	"github.com/stretchr/testify/assert"
)

func TestFixupHelper_parseDiff(t *testing.T) {
	scenarios := []struct {
		name                     string
		diff                     string
		expectedDeletedLineHunks []*hunk
		expectedAddedLineHunks   []*hunk
	}{
		{
			name:                     "no diff",
			diff:                     "",
			expectedDeletedLineHunks: []*hunk{},
			expectedAddedLineHunks:   []*hunk{},
		},
		{
			name: "hunk with only deleted lines",
			diff: `
diff --git a/file1.txt b/file1.txt
index 9ce8efb33..aaf2a4666 100644
--- a/file1.txt
+++ b/file1.txt
@@ -3 +2,0 @@ bbb
-xxx
`,
			expectedDeletedLineHunks: []*hunk{
				{
					filename:     "file1.txt",
					startLineIdx: 3,
					numLines:     1,
				},
			},
			expectedAddedLineHunks: []*hunk{},
		},
		{
			name: "hunk with deleted and added lines",
			diff: `
diff --git a/file1.txt b/file1.txt
index 9ce8efb33..eb246cf98 100644
--- a/file1.txt
+++ b/file1.txt
@@ -3 +3 @@ bbb
-xxx
+yyy
`,
			expectedDeletedLineHunks: []*hunk{
				{
					filename:     "file1.txt",
					startLineIdx: 3,
					numLines:     1,
				},
			},
			expectedAddedLineHunks: []*hunk{},
		},
		{
			name: "hunk with only added lines",
			diff: `
diff --git a/file1.txt b/file1.txt
index 9ce8efb33..fb5e469e7 100644
--- a/file1.txt
+++ b/file1.txt
@@ -4,0 +5,2 @@ ddd
+xxx
+yyy
`,
			expectedDeletedLineHunks: []*hunk{},
			expectedAddedLineHunks: []*hunk{
				{
					filename:     "file1.txt",
					startLineIdx: 4,
					numLines:     2,
				},
			},
		},
		{
			name: "hunk with dashed lines",
			diff: `
diff --git a/file1.txt b/file1.txt
index 9ce8efb33..fb5e469e7 100644
--- a/file1.txt
+++ b/file1.txt
@@ -3,1 +3,1 @@
--- xxx
+-- yyy
`,
			expectedDeletedLineHunks: []*hunk{
				{
					filename:     "file1.txt",
					startLineIdx: 3,
					numLines:     1,
				},
			},
			expectedAddedLineHunks: []*hunk{},
		},
		{
			name: "several hunks in different files",
			diff: `
diff --git a/file1.txt b/file1.txt
index 9ce8efb33..0632e41b0 100644
--- a/file1.txt
+++ b/file1.txt
@@ -2 +1,0 @@ aaa
-bbb
@@ -4 +3 @@ ccc
-ddd
+xxx
@@ -6,0 +6 @@ fff
+zzz
diff --git a/file2.txt b/file2.txt
index 9ce8efb33..0632e41b0 100644
--- a/file2.txt
+++ b/file2.txt
@@ -0,3 +1,0 @@ aaa
-aaa
-bbb
-ccc
`,
			expectedDeletedLineHunks: []*hunk{
				{
					filename:     "file1.txt",
					startLineIdx: 2,
					numLines:     1,
				},
				{
					filename:     "file1.txt",
					startLineIdx: 4,
					numLines:     1,
				},
				{
					filename:     "file2.txt",
					startLineIdx: 0,
					numLines:     3,
				},
			},
			expectedAddedLineHunks: []*hunk{
				{
					filename:     "file1.txt",
					startLineIdx: 6,
					numLines:     1,
				},
			},
		},
	}
	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			deletedLineHunks, addedLineHunks := parseDiff(s.diff)
			assert.Equal(t, s.expectedDeletedLineHunks, deletedLineHunks)
			assert.Equal(t, s.expectedAddedLineHunks, addedLineHunks)
		})
	}
}

func TestFixupHelper_IsFixupCommit(t *testing.T) {
	scenarios := []struct {
		subject                string
		expectedTrimmedSubject string
		expectedIsFixup        bool
	}{
		{
			subject:                "Bla",
			expectedTrimmedSubject: "Bla",
			expectedIsFixup:        false,
		},
		{
			subject:                "fixup Bla",
			expectedTrimmedSubject: "fixup Bla",
			expectedIsFixup:        false,
		},
		{
			subject:                "fixup! Bla",
			expectedTrimmedSubject: "Bla",
			expectedIsFixup:        true,
		},
		{
			subject:                "fixup! fixup! Bla",
			expectedTrimmedSubject: "Bla",
			expectedIsFixup:        true,
		},
		{
			subject:                "amend! squash! Bla",
			expectedTrimmedSubject: "Bla",
			expectedIsFixup:        true,
		},
		{
			subject:                "fixup!",
			expectedTrimmedSubject: "fixup!",
			expectedIsFixup:        false,
		},
	}
	for _, s := range scenarios {
		t.Run(s.subject, func(t *testing.T) {
			trimmedSubject, isFixupCommit := IsFixupCommit(s.subject)
			assert.Equal(t, s.expectedTrimmedSubject, trimmedSubject)
			assert.Equal(t, s.expectedIsFixup, isFixupCommit)
		})
	}
}

func TestFixupHelper_removeFixupCommits(t *testing.T) {
	hashPool := &utils.StringPool{}

	type commitDesc struct {
		Hash string
		Name string
	}

	scenarios := []struct {
		name           string
		commits        []commitDesc
		expectedResult []commitDesc
	}{
		{
			name:           "empty list",
			commits:        []commitDesc{},
			expectedResult: []commitDesc{},
		},
		{
			name: "single commit",
			commits: []commitDesc{
				{"abc123", "Some feature"},
			},
			expectedResult: []commitDesc{
				{"abc123", "Some feature"},
			},
		},
		{
			name: "two unrelated commits",
			commits: []commitDesc{
				{"abc123", "First feature"},
				{"def456", "Second feature"},
			},
			expectedResult: []commitDesc{
				{"abc123", "First feature"},
				{"def456", "Second feature"},
			},
		},
		{
			name: "fixup commit for last commit",
			commits: []commitDesc{
				{"abc123", "fixup! Some feature"},
				{"def456", "Some feature"},
			},
			expectedResult: []commitDesc{
				{"def456", "Some feature"},
			},
		},
		{
			name: "amend and squash commits for last commit",
			commits: []commitDesc{
				{"abc123", "squash! Some feature"},
				{"def456", "amend! Some feature"},
				{"ghi789", "Some feature"},
			},
			expectedResult: []commitDesc{
				{"ghi789", "Some feature"},
			},
		},
		{
			name: "fixup commit for different commit",
			commits: []commitDesc{
				{"abc123", "fixup! Other feature"},
				{"def456", "Some feature"},
			},
			expectedResult: []commitDesc{
				{"abc123", "fixup! Other feature"},
				{"def456", "Some feature"},
			},
		},
		{
			name: "last commit is a fixup itself",
			commits: []commitDesc{
				{"abc123", "fixup! Some feature"},
				{"def456", "fixup! Some feature"},
			},
			expectedResult: []commitDesc{
				{"abc123", "fixup! Some feature"},
				{"def456", "fixup! Some feature"},
			},
		},
		{
			name: "nested fixup commit",
			commits: []commitDesc{
				{"abc123", "fixup! fixup! Some feature"},
				{"def456", "amend! squash! fixup! Some feature"},
				{"ghi789", "Some feature"},
			},
			expectedResult: []commitDesc{
				{"ghi789", "Some feature"},
			},
		},
		{
			name: "fixup commits mixed with unrelated commits",
			commits: []commitDesc{
				{Hash: "abc123", Name: "fixup! Base commit"},
				{Hash: "def456", Name: "Unrelated commit"},
				{Hash: "ghi789", Name: "fixup! Base commit"},
				{Hash: "jkl012", Name: "Base commit"},
			},
			expectedResult: []commitDesc{
				{Hash: "def456", Name: "Unrelated commit"},
				{Hash: "jkl012", Name: "Base commit"},
			},
		},
		{
			name: "only fixup commits for last commit removed, others preserved",
			commits: []commitDesc{
				{Hash: "abc123", Name: "fixup! First feature"},
				{Hash: "def456", Name: "fixup! Second feature"},
				{Hash: "ghi789", Name: "Second feature"},
				{Hash: "jkl012", Name: "First feature"},
			},
			expectedResult: []commitDesc{
				{Hash: "def456", Name: "fixup! Second feature"},
				{Hash: "ghi789", Name: "Second feature"},
				{Hash: "jkl012", Name: "First feature"},
			},
		},
	}

	makeCommitFromDesc := func(desc commitDesc, _ int) *models.Commit {
		return models.NewCommit(hashPool, models.NewCommitOpts{Hash: desc.Hash, Name: desc.Name})
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			commits := lo.Map(s.commits, makeCommitFromDesc)
			result := removeFixupCommits(commits)
			expectedCommits := lo.Map(s.expectedResult, makeCommitFromDesc)
			assert.Equal(t, expectedCommits, result)
		})
	}
}
