package gitx

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

// The fixture builds a bare repository at <tmp>/~alice/demo with a history rich
// enough to exercise every command:
//
//	root ── c2 ── rename ── binary ─┐
//	          \                      merge   (branch: main, HEAD)
//	           feature ─────────────┘        (branch: feature/with-slash)
//
//	root    : add a.txt                       (root commit)
//	c2      : add b.txt, edit a.txt
//	rename  : git mv a.txt -> a-renamed.txt   (tag: v0.9.0)
//	binary  : add bin.dat (4 NUL-ish bytes)   (main^1)
//	feature : add feature.txt (from c2)       (branch feature/with-slash, main^2)
//	merge   : merge feature into main         (main, HEAD; tag: v1.0.0)
//
// The commit dates increase monotonically so tag creator-date ordering is
// deterministic (v1.0.0 newer than v0.9.0).

const (
	fxOwner = "alice"
	fxName  = "demo"
)

// gitTest runs git in dir with a hardened, deterministic environment, failing
// the test on error.
func gitTest(t *testing.T, dir string, date string, args ...string) string {
	t.Helper()
	cmd := exec.Command("git", args...)
	cmd.Dir = dir
	cmd.Env = append(os.Environ(),
		"GIT_CONFIG_GLOBAL=/dev/null",
		"GIT_CONFIG_SYSTEM=/dev/null",
		"GIT_TERMINAL_PROMPT=0",
		"LC_ALL=C",
		"GIT_AUTHOR_NAME=Alice Example",
		"GIT_AUTHOR_EMAIL=alice@example.com",
		"GIT_COMMITTER_NAME=Alice Example",
		"GIT_COMMITTER_EMAIL=alice@example.com",
		"GIT_AUTHOR_DATE="+date,
		"GIT_COMMITTER_DATE="+date,
	)
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
	}
	return string(out)
}

func writeFile(t *testing.T, dir, name string, data []byte) {
	t.Helper()
	p := filepath.Join(dir, name)
	if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(p, data, 0o644); err != nil {
		t.Fatal(err)
	}
}

// newFixtureRepo builds the bare fixture repository and returns reposRoot (the
// directory that Open takes). The bare repo lives at reposRoot/~alice/demo.
func newFixtureRepo(t *testing.T) string {
	t.Helper()
	if _, err := exec.LookPath("git"); err != nil {
		t.Skipf("git not available: %v", err)
	}

	reposRoot := t.TempDir()
	work := t.TempDir()

	d1 := "2024-01-01T00:00:00Z"
	d2 := "2024-01-02T00:00:00Z"
	d3 := "2024-01-03T00:00:00Z"
	d4 := "2024-01-04T00:00:00Z"
	d5 := "2024-01-05T00:00:00Z"
	d6 := "2024-01-06T00:00:00Z"

	gitTest(t, work, d1, "init", "-b", "main")

	// root: add a.txt
	writeFile(t, work, "a.txt", []byte("hello\nworld\n"))
	gitTest(t, work, d1, "add", "a.txt")
	gitTest(t, work, d1, "commit", "-m", "root: add a.txt")

	// c2: add b.txt, edit a.txt
	writeFile(t, work, "a.txt", []byte("hello\nworld\nmore\n"))
	writeFile(t, work, "b.txt", []byte("bee\n"))
	gitTest(t, work, d2, "add", "a.txt", "b.txt")
	gitTest(t, work, d2, "commit", "-m", "c2: add b, edit a")

	// feature branch from c2 (before rename/binary): add feature.txt
	gitTest(t, work, d5, "branch", "feature/with-slash")

	// rename: pure git mv a.txt -> a-renamed.txt
	gitTest(t, work, d3, "mv", "a.txt", "a-renamed.txt")
	gitTest(t, work, d3, "commit", "-m", "rename: a.txt -> a-renamed.txt")
	gitTest(t, work, d3, "tag", "-a", "-m", "release 0.9.0", "v0.9.0")

	// binary: add bin.dat (4 bytes, includes NUL)
	writeFile(t, work, "bin.dat", []byte{0x00, 0x01, 0x02, 0x03})
	gitTest(t, work, d4, "add", "bin.dat")
	gitTest(t, work, d4, "commit", "-m", "binary: add bin.dat")

	// build feature tip
	gitTest(t, work, d5, "checkout", "feature/with-slash")
	writeFile(t, work, "feature.txt", []byte("feature\n"))
	gitTest(t, work, d5, "add", "feature.txt")
	gitTest(t, work, d5, "commit", "-m", "feature: add feature.txt")

	// merge feature into main (no-ff so a merge commit is created)
	gitTest(t, work, d6, "checkout", "main")
	gitTest(t, work, d6, "merge", "--no-ff", "-m", "merge feature/with-slash", "feature/with-slash")
	gitTest(t, work, d6, "tag", "-a", "-m", "release 1.0.0", "v1.0.0")

	// bare clone into reposRoot/~alice/demo
	if err := os.MkdirAll(filepath.Join(reposRoot, "~"+fxOwner), 0o755); err != nil {
		t.Fatal(err)
	}
	bare := filepath.Join(reposRoot, "~"+fxOwner, fxName)
	gitTest(t, work, d6, "clone", "--bare", work, bare)

	return reposRoot
}

// fxRev resolves a revision inside the bare fixture repo to its full SHA. It is
// a test helper for landmark navigation (e.g. "main^1~1" for the rename
// commit); it is independent of the code under test.
func fxRev(t *testing.T, reposRoot, rev string) string {
	t.Helper()
	return fxRevIn(t, reposRoot, fxName, rev)
}

// openFixture opens the fixture repo through the code under test.
func openFixture(t *testing.T, reposRoot string) *Repo {
	t.Helper()
	repo, err := Open(reposRoot, fxOwner, fxName)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	return repo
}
