// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0

package gitinterface

import (
	"os"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestStatus(t *testing.T) {
	tmpDir := t.TempDir()
	repo := CreateTestGitRepository(t, tmpDir, false)

	cwd, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	if err := os.Chdir(tmpDir); err != nil {
		t.Fatal(err)
	}
	defer os.Chdir(cwd) //nolint:errcheck

	// NOTE: we don't use traditional methods like WriteBlob or TreeBuilder so
	// we can more closely simulate user actions, such as with updating index,
	// etc.

	filename := "foo"
	filename2 := "bar"

	if err := os.WriteFile(filename, []byte("foo"), 0o644); err != nil { //nolint:gosec
		t.Fatal(err)
	}

	statuses, err := repo.Status()
	assert.Nil(t, err)
	assert.Equal(t, map[string]FileStatus{filename: {X: StatusCodeUntracked, Y: StatusCodeUntracked}}, statuses)

	// Add item to index
	if _, err := repo.executor("add", filename).executeString(); err != nil {
		t.Fatal(err)
	}

	statuses, err = repo.Status()
	assert.Nil(t, err)
	assert.Equal(t, map[string]FileStatus{filename: {X: StatusCodeAdded, Y: StatusCodeUnmodified}}, statuses)

	// Modify file that has been staged
	if err := os.WriteFile(filename, []byte("bar"), 0o644); err != nil { //nolint:gosec
		t.Fatal(err)
	}

	statuses, err = repo.Status()
	assert.Nil(t, err)
	assert.Equal(t, map[string]FileStatus{filename: {X: StatusCodeAdded, Y: StatusCodeModified}}, statuses)

	// Add and commit
	if _, err := repo.executor("add", filename).executeString(); err != nil {
		t.Fatal(err)
	}
	if _, err := repo.executor("commit", "-m", "Commit\n").executeString(); err != nil {
		t.Fatal(err)
	}

	statuses, err = repo.Status()
	assert.Nil(t, err)
	assert.Empty(t, statuses)

	// Modify file again
	if err := os.WriteFile(filename, []byte("foo"), 0o644); err != nil { //nolint:gosec
		t.Fatal(err)
	}

	statuses, err = repo.Status()
	assert.Nil(t, err)
	assert.Equal(t, map[string]FileStatus{filename: {X: StatusCodeModified, Y: StatusCodeUnmodified}}, statuses)

	// Remove file
	if err := os.Remove(filename); err != nil {
		t.Fatal(err)
	}

	statuses, err = repo.Status()
	assert.Nil(t, err)
	assert.Equal(t, map[string]FileStatus{filename: {X: StatusCodeDeleted, Y: StatusCodeUnmodified}}, statuses)

	// Commit
	if _, err := repo.executor("add", filename).executeString(); err != nil {
		t.Fatal(err)
	}
	if _, err := repo.executor("commit", "-m", "Commit\n", "--allow-empty").executeString(); err != nil {
		t.Fatal(err)
	}

	statuses, err = repo.Status()
	assert.Nil(t, err)
	assert.Empty(t, statuses)

	// Add two files, commit, then make one a symlink to the other
	if err := os.WriteFile(filename, []byte("foo"), 0o644); err != nil { //nolint:gosec
		t.Fatal(err)
	}
	if err := os.WriteFile(filename2, []byte("foo"), 0o644); err != nil { //nolint:gosec
		t.Fatal(err)
	}
	if _, err := repo.executor("add", filename, filename2).executeString(); err != nil {
		t.Fatal(err)
	}
	if _, err := repo.executor("commit", "-m", "Commit\n").executeString(); err != nil {
		t.Fatal(err)
	}

	statuses, err = repo.Status()
	assert.Nil(t, err)
	assert.Empty(t, statuses)

	if err := os.Remove(filename2); err != nil {
		t.Fatal(err)
	}
	if err := os.Symlink(filename, filename2); err != nil {
		t.Fatal(err)
	}

	statuses, err = repo.Status()
	assert.Nil(t, err)
	assert.Equal(t, map[string]FileStatus{filename2: {X: StatusCodeTypeChanged, Y: StatusCodeUnmodified}}, statuses)

	// Add and commit
	if _, err := repo.executor("add", filename2).executeString(); err != nil {
		t.Fatal(err)
	}
	if _, err := repo.executor("commit", "-m", "Commit\n").executeString(); err != nil {
		t.Fatal(err)
	}

	statuses, err = repo.Status()
	assert.Nil(t, err)
	assert.Empty(t, statuses)

	// Rename one of the existing files
	renamedPath := "baz"
	if _, err := repo.executor("mv", filename, renamedPath).executeString(); err != nil {
		t.Fatal(err)
	}

	statuses, err = repo.Status()
	assert.Nil(t, err)
	assert.Equal(t, map[string]FileStatus{
		renamedPath: {X: StatusCodeRenamed, Y: StatusCodeUnmodified},
	}, statuses)

	// Commit the rename
	if _, err := repo.executor("commit", "-m", "Commit\n").executeString(); err != nil {
		t.Fatal(err)
	}

	statuses, err = repo.Status()
	assert.Nil(t, err)
	assert.Empty(t, statuses)

	// Test that rename is parsed correctly alongside other changes in the same
	// status output. This validates that the NUL-separated source-path token
	// for the rename is consumed so subsequent entries are not mis-parsed.
	renamedPath2 := "quux"
	newFile := "qux"

	if _, err := repo.executor("mv", renamedPath, renamedPath2).executeString(); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(newFile, []byte("qux"), 0o644); err != nil { //nolint:gosec
		t.Fatal(err)
	}
	if _, err := repo.executor("add", newFile).executeString(); err != nil {
		t.Fatal(err)
	}

	statuses, err = repo.Status()
	assert.Nil(t, err)
	assert.Equal(t, map[string]FileStatus{
		renamedPath2: {X: StatusCodeRenamed, Y: StatusCodeUnmodified},
		newFile:      {X: StatusCodeAdded, Y: StatusCodeUnmodified},
	}, statuses)
}

func TestStatusError(t *testing.T) {
	tmpDir := t.TempDir()
	repo := CreateTestGitRepository(t, tmpDir, true) // bare repo

	_, err := repo.Status()
	assert.ErrorContains(t, err, "unable to check status of repository")
}
func TestNewStatusCodeFromByte(t *testing.T) {
	t.Run("invalid status code", func(t *testing.T) {
		_, err := NewStatusCodeFromByte('X')
		assert.ErrorIs(t, err, ErrInvalidStatusCode)
	})

	t.Run("valid status codes", func(t *testing.T) {
		testCases := []struct {
			input    byte
			expected StatusCode
		}{
			{' ', StatusCodeUnmodified},
			{'M', StatusCodeModified},
			{'T', StatusCodeTypeChanged},
			{'A', StatusCodeAdded},
			{'D', StatusCodeDeleted},
			{'C', StatusCodeCopied},
			{'U', StatusCodeUpdatedUnmerged},
			{'?', StatusCodeUntracked},
			{'!', StatusCodeIgnored},
		}

		for _, tc := range testCases {
			code, err := NewStatusCodeFromByte(tc.input)
			assert.Nil(t, err)
			assert.Equal(t, tc.expected, code)
		}
	})
}

func TestStatusCodeString(t *testing.T) {
	t.Run("invalid status code", func(t *testing.T) {
		assert.Equal(t, "invalid-code", StatusCode(999).String())
	})

	t.Run("valid status codes", func(t *testing.T) {
		testCases := []struct {
			code     StatusCode
			expected string
		}{
			{StatusCodeUnmodified, " "},
			{StatusCodeModified, "M"},
			{StatusCodeTypeChanged, "T"},
			{StatusCodeAdded, "A"},
			{StatusCodeDeleted, "D"},
			{StatusCodeRenamed, "R"},
			{StatusCodeCopied, "C"},
			{StatusCodeUpdatedUnmerged, "U"},
			{StatusCodeUntracked, "?"},
			{StatusCodeIgnored, "!"},
		}

		for _, tc := range testCases {
			assert.Equal(t, tc.expected, tc.code.String())
		}
	})
}

func TestFileStatusUntracked(t *testing.T) {
	t.Run("untracked when X is untracked", func(t *testing.T) {
		fs := FileStatus{X: StatusCodeUntracked, Y: StatusCodeUnmodified}
		assert.True(t, fs.Untracked())
	})

	t.Run("untracked when Y is untracked", func(t *testing.T) {
		fs := FileStatus{X: StatusCodeUnmodified, Y: StatusCodeUntracked}
		assert.True(t, fs.Untracked())
	})

	t.Run("not untracked when neither is untracked", func(t *testing.T) {
		fs := FileStatus{X: StatusCodeModified, Y: StatusCodeUnmodified}
		assert.False(t, fs.Untracked())
	})
}
