package gitclient

import (
	"bytes"
	"context"
	"errors"
	"net/http/cgi"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

func gitIn(t *testing.T, dir string, stdin []byte, args ...string) string {
	t.Helper()
	cmd := exec.Command("git", args...)
	cmd.Dir = dir
	cmd.Env = append(os.Environ(),
		"GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null",
		"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t")
	cmd.Stdin = bytes.NewReader(stdin)
	out, err := cmd.Output()
	if err != nil {
		t.Fatalf("git %v: %v", args, err)
	}
	return strings.TrimSpace(string(out))
}

func TestStockGit(t *testing.T) {
	gitPath, err := exec.LookPath("git")
	if err != nil {
		t.Skip("no git binary")
	}
	root := t.TempDir()
	repo := filepath.Join(root, "repo")
	git := func(args ...string) string { return gitIn(t, repo, nil, args...) }
	write := func(name, data string, mode os.FileMode) {
		p := filepath.Join(repo, name)
		if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(p, []byte(data), mode); err != nil {
			t.Fatal(err)
		}
	}
	gitIn(t, root, nil, "init", "-q", "-b", "main", "repo")
	git("config", "uploadpack.allowFilter", "true")
	git("config", "uploadpack.allowAnySHA1InWant", "true")
	write("README", "hi\n", 0o644)
	git("add", ".")
	git("commit", "-qm", "first")
	first := git("rev-parse", "HEAD")
	git("tag", "dup")

	write(".tangled/workflows/a.yml", "when: push\n", 0o644)
	write(".tangled/workflows/b.yml", "when: pr\n", 0o755)
	write(".tangled/workflows/sub/c.yml", "nested\n", 0o644)
	if err := os.Symlink("a.yml", filepath.Join(repo, ".tangled/workflows/link")); err != nil {
		t.Fatal(err)
	}
	git("add", ".")
	git("commit", "-qm", "second")
	second := git("rev-parse", "HEAD")
	git("branch", "dup")
	git("tag", "-am", "annotated", "v1")
	git("tag", "light")

	srv := httptest.NewServer(&cgi.Handler{
		Path: gitPath,
		Args: []string{"http-backend"},
		Env:  []string{"GIT_PROJECT_ROOT=" + root, "GIT_HTTP_EXPORT_ALL=1", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null"},
	})
	defer srv.Close()
	ctx := context.Background()
	c := New(srv.Client(), srv.URL+"/repo", 4<<20)

	for ref, want := range map[string]string{
		"main":            second,
		"refs/heads/main": second,
		"dup":             second, // branch before tag
		"refs/tags/dup":   first,
		"v1":              second, // peeled
		"light":           second,
	} {
		got, err := c.Resolve(ctx, ref)
		if err != nil || string(got) != want {
			t.Errorf("Resolve(%s) = %s, %v; want %s", ref, got, err, want)
		}
	}
	if _, err := c.Resolve(ctx, "nope"); !errors.Is(err, ErrRefNotFound) {
		t.Errorf("Resolve(nope) = %v", err)
	}

	files, err := c.ReadDir(ctx, Oid(second), ".tangled/workflows")
	if err != nil {
		t.Fatal(err)
	}
	var got []string
	for _, f := range files {
		got = append(got, f.Mode+" "+f.Name+" "+string(f.Data))
	}
	want := []string{"100644 a.yml when: push\n", "100755 b.yml when: pr\n", "120000 link a.yml", "40000 sub "}
	if strings.Join(got, "|") != strings.Join(want, "|") {
		t.Errorf("ReadDir = %q", got)
	}

	if _, err := c.ReadDir(ctx, Oid(first), ".tangled/workflows"); !errors.Is(err, ErrPathNotFound) {
		t.Errorf("ReadDir(first) = %v", err)
	}
	for _, dir := range []string{".tangled/nope", ".tangled/workflows/a.yml"} {
		if _, err := c.ReadDir(ctx, Oid(second), dir); !errors.Is(err, ErrPathNotFound) {
			t.Errorf("ReadDir(%s) = %v", dir, err)
		}
	}

	if _, err := c.ReadDir(ctx, Oid(second), ".tangled/workflows/a.yml"); !errors.Is(err, ErrNotDir) {
		t.Errorf("ReadDir(a.yml) = %v", err)
	}

	files, err = c.ReadTree(ctx, Oid(second), "/.tangled")
	if err != nil {
		t.Fatal(err)
	}
	got = nil
	for _, f := range files {
		got = append(got, f.Mode+" "+f.Name+" "+string(f.Data))
	}
	want = []string{"40000 workflows ", "100644 workflows/a.yml when: push\n", "100755 workflows/b.yml when: pr\n",
		"120000 workflows/link a.yml", "40000 workflows/sub ", "100644 workflows/sub/c.yml nested\n"}
	if strings.Join(got, "|") != strings.Join(want, "|") {
		t.Errorf("ReadTree = %q", got)
	}
	if _, err := c.ReadTree(ctx, Oid(second), "nope"); !errors.Is(err, ErrPathNotFound) || errors.Is(err, ErrNotDir) {
		t.Errorf("ReadTree(nope) = %v", err)
	}
	if _, err := c.ReadTree(ctx, Oid(second), "README/x"); !errors.Is(err, ErrNotDir) {
		t.Errorf("ReadTree(README/x) = %v", err)
	}

	small := New(srv.Client(), srv.URL+"/repo", 64)
	if _, err := small.ReadDir(ctx, Oid(second), ".tangled/workflows"); !errors.Is(err, ErrTooLarge) {
		t.Errorf("small ReadDir = %v", err)
	}
	gone := New(srv.Client(), srv.URL+"/missing", 4<<20)
	if _, err := gone.Resolve(ctx, "main"); err == nil || errors.Is(err, ErrRefNotFound) {
		t.Errorf("Resolve on missing repo = %v", err)
	}

	// ofs and ref deltas straight from pack-objects
	base := strings.Repeat("line of text\n", 400)
	b1 := gitIn(t, repo, []byte(base), "hash-object", "-w", "--stdin")
	b2 := gitIn(t, repo, []byte(base+"tail\n"), "hash-object", "-w", "--stdin")
	for _, flag := range []string{"--delta-base-offset", "--window=10"} {
		cmd := exec.Command("git", "pack-objects", "--stdout", flag)
		cmd.Dir = repo
		cmd.Stdin = strings.NewReader(b1 + "\n" + b2 + "\n")
		pack, err := cmd.Output()
		if err != nil {
			t.Fatal(err)
		}
		objs, err := readPack(bytes.NewReader(pack), "sha1")
		if err != nil {
			t.Fatalf("%s: %v", flag, err)
		}
		if string(objs[Oid(b2)].Data) != base+"tail\n" || string(objs[Oid(b1)].Data) != base {
			t.Errorf("%s: bad objects", flag)
		}
	}
}

func TestApplyDelta(t *testing.T) {
	// src 5, dst 8: copy 5 bytes from 0, insert "xyz"
	out, err := applyDelta([]byte("hello"), []byte{5, 8, 0x90, 5, 3, 'x', 'y', 'z'})
	if err != nil || string(out) != "helloxyz" {
		t.Fatalf("applyDelta = %q, %v", out, err)
	}
	if _, err := applyDelta([]byte("hello"), []byte{5, 8, 0x90, 6}); err == nil {
		t.Fatal("copy past base accepted")
	}
}

func TestPkt(t *testing.T) {
	var b bytes.Buffer
	writePkt(&b, "a\n")
	writeDelim(&b)
	writeFlush(&b)
	if b.String() != "0006a\n00010000" {
		t.Fatalf("framing %q", b.String())
	}
	p := newPktReader(&b)
	for _, want := range []pktKind{pktData, pktDelim, pktFlush} {
		if kind, _, err := p.next(); err != nil || kind != want {
			t.Fatalf("next = %v, %v", kind, err)
		}
	}
}
