package helpers

import (
	"testing"

	"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
	"github.com/jesseduffield/lazygit/pkg/commands/models"
	"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
	"github.com/jesseduffield/lazygit/pkg/utils"
	"github.com/samber/lo"
	"github.com/stefanhaller/git-todo-parser/todo"
	"github.com/stretchr/testify/assert"
)

func TestCaptureLocalCommitSelectionRange(t *testing.T) {
	testCases := []struct {
		name          string
		commits       []*models.Commit
		selectedIdx   int
		rangeStartIdx int
		expected      *localCommitSelectionRange
	}{
		{
			name:          "captures selected commit and range start",
			commits:       makeCommits("a", "b"),
			selectedIdx:   1,
			rangeStartIdx: 0,
			expected: &localCommitSelectionRange{
				selectedHash:   "b",
				rangeStartHash: "a",
				selectedIdx:    1,
				rangeStartIdx:  0,
				mode:           traits.RangeSelectModeSticky,
			},
		},
		{
			name:          "ignores invalid range start index",
			commits:       makeCommits("a"),
			selectedIdx:   0,
			rangeStartIdx: 1,
			expected:      nil,
		},
		{
			name:          "ignores empty selected hash",
			commits:       append(makeCommits("a"), makeTodoCommit(todo.UpdateRef)),
			selectedIdx:   1,
			rangeStartIdx: 0,
			expected:      nil,
		},
		{
			name:          "ignores empty range start hash",
			commits:       append(makeCommits("a"), makeTodoCommit(todo.Exec)),
			selectedIdx:   0,
			rangeStartIdx: 1,
			expected:      nil,
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			selectionRange := captureLocalCommitSelectionRange(
				testCase.commits,
				testCase.selectedIdx,
				testCase.rangeStartIdx,
				traits.RangeSelectModeSticky,
			)

			assert.Equal(t, testCase.expected, selectionRange)
		})
	}
}

func TestFindLocalCommitSelectionRange(t *testing.T) {
	type expectation struct {
		selectedIdx   int
		rangeStartIdx int
		moved         bool
		found         bool
	}

	selectionRange := localCommitSelectionRange{
		selectedHash:   "b",
		rangeStartHash: "c",
		selectedIdx:    1,
		rangeStartIdx:  2,
		mode:           traits.RangeSelectModeSticky,
	}

	testCases := []struct {
		name     string
		commits  []*models.Commit
		expected expectation
	}{
		{
			name:    "finds selection after commits are inserted above it",
			commits: makeCommits("new", "a", "b", "c"),
			expected: expectation{
				selectedIdx:   2,
				rangeStartIdx: 3,
				moved:         true,
				found:         true,
			},
		},
		{
			name:    "finds selection that did not move",
			commits: makeCommits("a", "b", "c"),
			expected: expectation{
				selectedIdx:   1,
				rangeStartIdx: 2,
				found:         true,
			},
		},
		{
			name:     "reports not found when a hash is missing",
			commits:  makeCommits("a", "b"),
			expected: expectation{},
		},
		{
			name: "skips todo entries with the same hash as a selected commit",
			commits: []*models.Commit{
				makeTodoCommitWithHash("b", todo.Revert),
				makeCommits("a")[0],
				makeCommits("b")[0],
				makeCommits("c")[0],
			},
			expected: expectation{
				selectedIdx:   2,
				rangeStartIdx: 3,
				moved:         true,
				found:         true,
			},
		},
		{
			name: "falls back to a todo entry when the selected commit became one",
			commits: []*models.Commit{
				makeTodoCommitWithHash("b", todo.Pick),
				makeCommits("c")[0],
			},
			expected: expectation{
				selectedIdx:   0,
				rangeStartIdx: 1,
				moved:         true,
				found:         true,
			},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			selectedIdx, rangeStartIdx, moved, found := findLocalCommitSelectionRange(testCase.commits, &selectionRange)
			actual := expectation{
				selectedIdx:   selectedIdx,
				rangeStartIdx: rangeStartIdx,
				moved:         moved,
				found:         found,
			}

			assert.Equal(t, testCase.expected, actual)
		})
	}
}

func TestGetGithubBaseRemote(t *testing.T) {
	cases := []struct {
		name             string
		githubRemotes    []githubRemoteInfo
		configuredRemote string
		expected         string
	}{
		{
			name:             "configured remote wins",
			githubRemotes:    makeGithubRemoteInfoList("origin", "upstream", "fork"),
			configuredRemote: "fork",
			expected:         "fork",
		},
		{
			name:             "configured remote not in github remotes returns nil",
			githubRemotes:    makeGithubRemoteInfoList("origin"),
			configuredRemote: "missing",
			expected:         "",
		},
		{
			name:             "single github remote is auto-picked",
			githubRemotes:    makeGithubRemoteInfoList("myremote"),
			configuredRemote: "",
			expected:         "myremote",
		},
		{
			name:             "upstream is preferred when multiple github remotes exist",
			githubRemotes:    makeGithubRemoteInfoList("origin", "upstream", "fork"),
			configuredRemote: "",
			expected:         "upstream",
		},
		{
			name:             "no upstream and multiple remotes returns nil",
			githubRemotes:    makeGithubRemoteInfoList("origin", "fork"),
			configuredRemote: "",
			expected:         "",
		},
		{
			name:             "empty list returns nil",
			githubRemotes:    nil,
			configuredRemote: "",
			expected:         "",
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			result := getGithubBaseRemote(c.githubRemotes, c.configuredRemote)
			if c.expected == "" {
				assert.Nil(t, result)
			} else {
				assert.NotNil(t, result)
				assert.Equal(t, c.expected, result.remote.Name)
			}
		})
	}
}

func TestGetAuthenticatedGithubRemotes(t *testing.T) {
	githubRemotes := []githubRemoteInfo{
		makeGithubRemoteInfo("origin", "github.com"),
		makeGithubRemoteInfo("fork", "github.com"),
		makeGithubRemoteInfo("enterprise", "ghe.example.com"),
		makeGithubRemoteInfo("missing-auth", "no-token.example.com"),
	}

	callsByHost := map[string]int{}
	result := getAuthenticatedGithubRemotes(githubRemotes, func(host string) string {
		callsByHost[host]++
		switch host {
		case "github.com":
			return "github-token"
		case "ghe.example.com":
			return "ghe-token"
		default:
			return ""
		}
	})

	assert.Equal(t, []githubRemoteInfo{
		makeAuthenticatedGithubRemoteInfo("origin", "github.com", "github-token"),
		makeAuthenticatedGithubRemoteInfo("fork", "github.com", "github-token"),
		makeAuthenticatedGithubRemoteInfo("enterprise", "ghe.example.com", "ghe-token"),
	}, result)
	// Two remotes share github.com; the lookup runs only once.
	assert.Equal(t, map[string]int{
		"github.com":           1,
		"ghe.example.com":      1,
		"no-token.example.com": 1,
	}, callsByHost)
}

func makeGithubRemoteInfoList(names ...string) []githubRemoteInfo {
	return lo.Map(names, func(name string, _ int) githubRemoteInfo {
		return makeGithubRemoteInfo(name, name)
	})
}

func makeGithubRemoteInfo(name string, webDomain string) githubRemoteInfo {
	return githubRemoteInfo{
		remote: &models.Remote{Name: name},
		serviceInfo: hosting_service.ServiceInfo{
			RepoName:  name,
			WebDomain: webDomain,
		},
	}
}

func makeAuthenticatedGithubRemoteInfo(name string, webDomain string, authToken string) githubRemoteInfo {
	info := makeGithubRemoteInfo(name, webDomain)
	info.authToken = authToken
	return info
}

func makeCommits(hashes ...string) []*models.Commit {
	hashPool := &utils.StringPool{}
	return lo.Map(hashes, func(hash string, _ int) *models.Commit {
		return models.NewCommit(hashPool, models.NewCommitOpts{Hash: hash})
	})
}

func makeTodoCommit(action todo.TodoCommand) *models.Commit {
	return models.NewCommit(&utils.StringPool{}, models.NewCommitOpts{Action: action})
}

func makeTodoCommitWithHash(hash string, action todo.TodoCommand) *models.Commit {
	return models.NewCommit(&utils.StringPool{}, models.NewCommitOpts{Hash: hash, Action: action})
}
