package gittransport

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

	reposync "github.com/pingcap/agent-git-service/internal/sync"
	"github.com/pingcap/agent-git-service/internal/sync/canonical"
)

func TestApplyRefBatchUsesLeasesAndPreservesUnmanagedRefs(t *testing.T) {
	root := t.TempDir()
	work := filepath.Join(root, "work")
	source := filepath.Join(root, "source.git")
	target := filepath.Join(root, "target.git")
	runTestGit(t, "", "init", work)
	runTestGit(t, work, "config", "user.email", "test@example.com")
	runTestGit(t, work, "config", "user.name", "Test")
	if err := os.WriteFile(filepath.Join(work, "file.txt"), []byte("one"), 0o644); err != nil {
		t.Fatal(err)
	}
	runTestGit(t, work, "add", "file.txt")
	runTestGit(t, work, "commit", "-m", "one")
	first := gitOutput(t, work, "rev-parse", "HEAD")
	runTestGit(t, "", "init", "--bare", source)
	runTestGit(t, "", "init", "--bare", target)
	runTestGit(t, work, "push", source, "HEAD:refs/heads/main", "HEAD:refs/heads/feature")
	runTestGit(t, work, "push", target, "HEAD:refs/heads/main", "HEAD:refs/heads/feature", "HEAD:refs/heads/unmanaged")
	if err := os.WriteFile(filepath.Join(work, "file.txt"), []byte("two"), 0o644); err != nil {
		t.Fatal(err)
	}
	runTestGit(t, work, "commit", "-am", "two")
	second := gitOutput(t, work, "rev-parse", "HEAD")
	runTestGit(t, work, "push", source, "+HEAD:refs/heads/main")

	update := refItem(t, reposync.ActionUpdate, "refs/heads/main", second, first)
	if err := ApplyRefBatch(context.Background(), reposync.GitRemote{URL: source}, reposync.GitRemote{URL: target}, []reposync.PlannedItem{update}); err != nil {
		t.Fatalf("ApplyRefBatch update: %v", err)
	}
	if got := gitOutput(t, "", "--git-dir", target, "rev-parse", "refs/heads/main"); got != second {
		t.Fatalf("target main = %s, want %s", got, second)
	}
	if got := gitOutput(t, "", "--git-dir", target, "rev-parse", "refs/heads/unmanaged"); got != first {
		t.Fatalf("unmanaged ref changed: %s", got)
	}

	deleteItem := refItem(t, reposync.ActionDelete, "refs/heads/feature", first, first)
	if err := ApplyRefBatch(context.Background(), reposync.GitRemote{URL: source}, reposync.GitRemote{URL: target}, []reposync.PlannedItem{deleteItem}); err != nil {
		t.Fatalf("ApplyRefBatch delete: %v", err)
	}
	command := exec.Command("git", "--git-dir", target, "show-ref", "--verify", "refs/heads/feature")
	if err := command.Run(); err == nil {
		t.Fatal("managed ref still exists after conditional delete")
	}
}

func TestApplyRefBatchRejectsStaleLease(t *testing.T) {
	root := t.TempDir()
	source := filepath.Join(root, "source.git")
	target := filepath.Join(root, "target.git")
	runTestGit(t, "", "init", "--bare", source)
	runTestGit(t, "", "init", "--bare", target)
	item := refItem(t, reposync.ActionDelete, "refs/heads/main", "", strings.Repeat("1", 40))
	if err := ApplyRefBatch(context.Background(), reposync.GitRemote{URL: source}, reposync.GitRemote{URL: target}, []reposync.PlannedItem{item}); err == nil {
		t.Fatal("stale force-with-lease unexpectedly succeeded")
	}
}

func TestFastForwardableRefsOnlyAdoptsVerifiedBranchAdvance(t *testing.T) {
	root := t.TempDir()
	work := filepath.Join(root, "work")
	source := filepath.Join(root, "source.git")
	target := filepath.Join(root, "target.git")
	runTestGit(t, "", "init", work)
	runTestGit(t, work, "config", "user.email", "test@example.com")
	runTestGit(t, work, "config", "user.name", "Test")
	if err := os.WriteFile(filepath.Join(work, "file.txt"), []byte("one"), 0o644); err != nil {
		t.Fatal(err)
	}
	runTestGit(t, work, "add", "file.txt")
	runTestGit(t, work, "commit", "-m", "one")
	first := gitOutput(t, work, "rev-parse", "HEAD")
	runTestGit(t, "", "init", "--bare", source)
	runTestGit(t, "", "init", "--bare", target)
	runTestGit(t, work, "push", source, "HEAD:refs/heads/main")
	runTestGit(t, work, "push", target, "HEAD:refs/heads/main")
	if err := os.WriteFile(filepath.Join(work, "file.txt"), []byte("two"), 0o644); err != nil {
		t.Fatal(err)
	}
	runTestGit(t, work, "commit", "-am", "two")
	second := gitOutput(t, work, "rev-parse", "HEAD")
	runTestGit(t, work, "push", source, "HEAD:refs/heads/main")

	candidate := RefAncestryCandidate{
		Key:    "refs/heads/main",
		Source: reposync.InventoryObject{ObjectType: canonical.TypeGitRef, Identity: "refs/heads/main", ObjectKey: "git_refs:refs/heads/main", UniqueKey: "refs/heads/main", Revision: second, ContentHash: second},
		Target: reposync.InventoryObject{ObjectType: canonical.TypeGitRef, Identity: "refs/heads/main", ObjectKey: "git_refs:refs/heads/main", UniqueKey: "refs/heads/main", Revision: first, ContentHash: first},
	}
	refs, err := FastForwardableRefs(context.Background(), reposync.GitRemote{URL: source}, reposync.GitRemote{URL: target}, []RefAncestryCandidate{candidate})
	if err != nil {
		t.Fatal(err)
	}
	if !refs["refs/heads/main"] {
		t.Fatalf("fast-forward branch was not accepted: %#v", refs)
	}

	reversed := candidate
	reversed.Source, reversed.Target = candidate.Target, candidate.Source
	refs, err = FastForwardableRefs(context.Background(), reposync.GitRemote{URL: target}, reposync.GitRemote{URL: source}, []RefAncestryCandidate{reversed})
	if err != nil {
		t.Fatal(err)
	}
	if refs["refs/heads/main"] {
		t.Fatalf("behind authority branch was incorrectly accepted: %#v", refs)
	}
}

func TestInventoryPullHeadRefsCreatesStableShadowBranches(t *testing.T) {
	root := t.TempDir()
	work := filepath.Join(root, "work")
	source := filepath.Join(root, "source.git")
	runTestGit(t, "", "init", work)
	runTestGit(t, work, "config", "user.email", "test@example.com")
	runTestGit(t, work, "config", "user.name", "Test")
	if err := os.WriteFile(filepath.Join(work, "file.txt"), []byte("pull head"), 0o644); err != nil {
		t.Fatal(err)
	}
	runTestGit(t, work, "add", "file.txt")
	runTestGit(t, work, "commit", "-m", "pull head")
	sha := gitOutput(t, work, "rev-parse", "HEAD")
	runTestGit(t, "", "init", "--bare", source)
	runTestGit(t, work, "push", source, "HEAD:refs/pull/42/head")

	objects, err := InventoryPullHeadRefs(context.Background(), reposync.GitRemote{URL: source}, nil)
	if err != nil {
		t.Fatal(err)
	}
	if len(objects) != 1 {
		t.Fatalf("objects=%d, want 1", len(objects))
	}
	if objects[0].Identity != "refs/heads/ags-sync/pr/42/head" || objects[0].Revision != sha {
		t.Fatalf("unexpected shadow ref: %#v", objects[0])
	}
	record, err := canonical.Decode(objects[0].Payload)
	if err != nil {
		t.Fatal(err)
	}
	if got := canonical.StringField(record.Fields, "_source_ref"); got != "refs/pull/42/head" {
		t.Fatalf("_source_ref=%q", got)
	}

	skipped, err := InventoryPullHeadRefs(context.Background(), reposync.GitRemote{URL: source}, map[string]bool{"refs/heads/ags-sync/pr/42/head": true})
	if err != nil {
		t.Fatal(err)
	}
	if len(skipped) != 0 {
		t.Fatalf("existing shadow branch was not skipped: %#v", skipped)
	}
}

func TestApplyRefBatchFetchesGitHubPullNamespace(t *testing.T) {
	root := t.TempDir()
	work := filepath.Join(root, "work")
	source := filepath.Join(root, "source.git")
	target := filepath.Join(root, "target.git")
	runTestGit(t, "", "init", work)
	runTestGit(t, work, "config", "user.email", "test@example.com")
	runTestGit(t, work, "config", "user.name", "Test")
	if err := os.WriteFile(filepath.Join(work, "file.txt"), []byte("fork head"), 0o644); err != nil {
		t.Fatal(err)
	}
	runTestGit(t, work, "add", "file.txt")
	runTestGit(t, work, "commit", "-m", "fork head")
	sha := gitOutput(t, work, "rev-parse", "HEAD")
	runTestGit(t, "", "init", "--bare", source)
	runTestGit(t, "", "init", "--bare", target)
	runTestGit(t, work, "push", source, "HEAD:refs/pull/7/head")

	record := canonical.NewRecord(canonical.TypeGitRef, "refs/heads/ags-sync/pr/7/head", "git_refs:refs/heads/ags-sync/pr/7/head", map[string]any{
		"ref":         "refs/heads/ags-sync/pr/7/head",
		"sha":         sha,
		"_source_ref": "refs/pull/7/head",
	})
	record.UniqueKey = "refs/heads/ags-sync/pr/7/head"
	payload, err := record.Payload()
	if err != nil {
		t.Fatal(err)
	}
	item := reposync.PlannedItem{ObjectType: canonical.TypeGitRef, ObjectKey: record.Key, Action: reposync.ActionCreate, Payload: payload}
	if err := ApplyRefBatch(context.Background(), reposync.GitRemote{URL: source}, reposync.GitRemote{URL: target}, []reposync.PlannedItem{item}); err != nil {
		t.Fatal(err)
	}
	if got := gitOutput(t, "", "--git-dir", target, "rev-parse", "refs/heads/ags-sync/pr/7/head"); got != sha {
		t.Fatalf("shadow ref=%s, want %s", got, sha)
	}
}

func refItem(t *testing.T, action, ref, sha, expected string) reposync.PlannedItem {
	t.Helper()
	record := canonical.NewRecord(canonical.TypeGitRef, ref, "git_refs:"+ref, map[string]any{"ref": ref, "sha": sha})
	record.UniqueKey = ref
	payload, err := record.Payload()
	if err != nil {
		t.Fatal(err)
	}
	return reposync.PlannedItem{ObjectType: canonical.TypeGitRef, ObjectKey: record.Key, Action: action, Payload: payload, ExpectedTargetRevision: expected}
}

func gitOutput(t *testing.T, dir string, args ...string) string {
	t.Helper()
	command := exec.Command("git", args...)
	command.Dir = dir
	output, err := command.CombinedOutput()
	if err != nil {
		t.Fatalf("git %v: %v: %s", args, err, output)
	}
	return strings.TrimSpace(string(output))
}
