package gitstore_test

import (
	"context"
	"math/rand"
	"os"
	"testing"

	"github.com/pingcap/agent-git-service/internal/gitstore"
)

// TestStore_CanMerge_Mergeable tests that CanMerge reports a clean merge.
func TestStore_CanMerge_Mergeable(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "gitstore-canmerge-ok-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := gitstore.New(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	repoName := "user/repo-canmerge-ok"

	if err := initializeBareRepositoryFixture(store, ctx, repoName, "main", true); err != nil {
		t.Fatalf("Init failed: %v", err)
	}

	if err := createBranchFixture(t, store, ctx, repoName, "feature", "main"); err != nil {
		t.Fatalf("CreateBranch failed: %v", err)
	}

	if _, err := store.WriteFile(ctx, repoName, "feature", "feature.txt", "add feature", []byte("feature\n")); err != nil {
		t.Fatalf("WriteFile feature failed: %v", err)
	}

	status := store.CanMerge(ctx, repoName, "main", "feature")
	if status != "MERGEABLE" {
		t.Fatalf("expected MERGEABLE, got %s", status)
	}
}

// TestStore_CanMerge_Conflicting tests that CanMerge reports conflicts.
func TestStore_CanMerge_Conflicting(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "gitstore-canmerge-conflict-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := gitstore.New(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	repoName := "user/repo-canmerge-conflict"

	if err := initializeBareRepositoryFixture(store, ctx, repoName, "main", true); err != nil {
		t.Fatalf("Init failed: %v", err)
	}

	if _, err := store.WriteFile(ctx, repoName, "main", "conflict.txt", "initial", []byte("line1\nshared\nline3\n")); err != nil {
		t.Fatalf("WriteFile initial failed: %v", err)
	}

	if err := createBranchFixture(t, store, ctx, repoName, "feature", "main"); err != nil {
		t.Fatalf("CreateBranch failed: %v", err)
	}

	if _, err := store.WriteFile(ctx, repoName, "main", "conflict.txt", "main change", []byte("line1\nmain-change\nline3\n")); err != nil {
		t.Fatalf("WriteFile main failed: %v", err)
	}

	if _, err := store.WriteFile(ctx, repoName, "feature", "conflict.txt", "feature change", []byte("line1\nfeature-change\nline3\n")); err != nil {
		t.Fatalf("WriteFile feature failed: %v", err)
	}

	status := store.CanMerge(ctx, repoName, "main", "feature")
	if status != "CONFLICTING" {
		t.Fatalf("expected CONFLICTING, got %s", status)
	}
}

// TestStore_CanMerge_InvalidRepoName tests that CanMerge reports unknown on invalid input.
func TestStore_CanMerge_InvalidRepoName(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "gitstore-canmerge-invalid-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := gitstore.New(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	status := store.CanMerge(ctx, "invalid", "main", "feature")
	if status != "UNKNOWN" {
		t.Fatalf("expected UNKNOWN, got %s", status)
	}
}

// TestStore_IsEmpty_EmptyRepo tests IsEmpty on an empty repo.
func TestStore_IsEmpty_EmptyRepo(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "gitstore-isempty-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := gitstore.New(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	repoName := "user/repo-empty"

	if err := initializeBareRepositoryFixture(store, ctx, repoName, "main", false); err != nil {
		t.Fatalf("Init failed: %v", err)
	}

	if !store.IsEmpty(ctx, repoName) {
		t.Fatal("expected repo to be empty")
	}
}

// TestStore_IsEmpty_NonEmptyRepo tests IsEmpty after a commit is created.
func TestStore_IsEmpty_NonEmptyRepo(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "gitstore-isempty-nonempty-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := gitstore.New(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	repoName := "user/repo-nonempty"

	if err := initializeBareRepositoryFixture(store, ctx, repoName, "main", true); err != nil {
		t.Fatalf("Init failed: %v", err)
	}

	if _, err := store.WriteFile(ctx, repoName, "main", "file.txt", "add file", []byte("content\n")); err != nil {
		t.Fatalf("WriteFile failed: %v", err)
	}

	if store.IsEmpty(ctx, repoName) {
		t.Fatal("expected repo to be non-empty")
	}
}

// TestStore_IsEmpty_NonExistentRepo tests IsEmpty on a missing repo.
func TestStore_IsEmpty_NonExistentRepo(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "gitstore-isempty-missing-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := gitstore.New(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	if !store.IsEmpty(ctx, "user/repo-missing") {
		t.Fatal("expected missing repo to be reported as empty")
	}
}

// TestStore_DiskUsageKB_MissingRepo tests DiskUsageKB on a missing repo.
func TestStore_DiskUsageKB_MissingRepo(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "gitstore-disk-missing-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := gitstore.New(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	if got := store.DiskUsageKB(ctx, "user/repo-missing"); got != 0 {
		t.Fatalf("expected 0 KB for missing repo, got %d", got)
	}
}

// TestStore_DiskUsageKB_IncreasesAfterWrite tests DiskUsageKB growth after adding data.
func TestStore_DiskUsageKB_IncreasesAfterWrite(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "gitstore-disk-usage-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := gitstore.New(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	repoName := "user/repo-disk-usage"

	if err := initializeBareRepositoryFixture(store, ctx, repoName, "main", true); err != nil {
		t.Fatalf("Init failed: %v", err)
	}

	beforeKB := store.DiskUsageKB(ctx, repoName)
	if beforeKB <= 0 {
		t.Fatalf("expected positive disk usage, got %d", beforeKB)
	}

	data := make([]byte, 1<<20)
	rng := rand.New(rand.NewSource(1))
	if _, err := rng.Read(data); err != nil {
		t.Fatalf("rand.Read failed: %v", err)
	}

	if _, err := store.WriteFile(ctx, repoName, "main", "big.bin", "add big file", data); err != nil {
		t.Fatalf("WriteFile failed: %v", err)
	}

	afterKB := store.DiskUsageKB(ctx, repoName)
	if afterKB <= beforeKB {
		t.Fatalf("expected disk usage to increase, before %d KB after %d KB", beforeKB, afterKB)
	}
}

// TestStore_IsEmpty_CorruptedHEAD tests IsEmpty when HEAD file exists but is corrupted.
// This tests the error path where git cat-file fails even though repo directory exists.
func TestStore_IsEmpty_CorruptedHEAD(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "gitstore-isempty-corrupt-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := gitstore.New(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	repoName := "user/repo-corrupt-head"

	// Initialize repo with a commit
	if err := initializeBareRepositoryFixture(store, ctx, repoName, "main", true); err != nil {
		t.Fatalf("Init failed: %v", err)
	}

	// Get the repo directory path
	repoDir, err := store.GetRepoPath(ctx, repoName)
	if err != nil {
		t.Fatalf("GetRepoPath failed: %v", err)
	}

	// Corrupt the HEAD file by writing invalid content
	headPath := repoDir + "/HEAD"
	if err := os.WriteFile(headPath, []byte("invalid-head-content\n"), 0644); err != nil {
		t.Fatalf("Failed to corrupt HEAD file: %v", err)
	}

	// IsEmpty should return true when git cat-file fails due to corrupted HEAD
	if !store.IsEmpty(ctx, repoName) {
		t.Fatal("expected IsEmpty to return true for repo with corrupted HEAD")
	}
}

// TestStore_IsEmpty_InvalidRepoName tests IsEmpty when repoPath returns an error
// due to invalid full name format (triggers the err != nil branch).
func TestStore_IsEmpty_InvalidRepoName(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "gitstore-isempty-invalid-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := gitstore.New(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	// Use an invalid repo name that will fail validateFullName (e.g., contains path traversal)
	invalidRepoName := "../escape/repo"

	// IsEmpty should return true when repoPath returns an error
	if !store.IsEmpty(ctx, invalidRepoName) {
		t.Fatal("expected IsEmpty to return true for invalid repo name")
	}
}

// TestStore_DiskUsageKB_PermissionDenied tests DiskUsageKB when du command fails
// due to permission denied on the repository directory.
func TestStore_DiskUsageKB_PermissionDenied(t *testing.T) {
	// Skip if running as root (root can access anything)
	if os.Geteuid() == 0 {
		t.Skip("skipping test when running as root")
	}

	tmpDir, err := os.MkdirTemp("", "gitstore-disk-perm-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := gitstore.New(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	repoName := "user/repo-disk-perm"

	// Initialize repo
	if err := initializeBareRepositoryFixture(store, ctx, repoName, "main", true); err != nil {
		t.Fatalf("Init failed: %v", err)
	}

	// Get the repo directory path
	repoDir, err := store.GetRepoPath(ctx, repoName)
	if err != nil {
		t.Fatalf("GetRepoPath failed: %v", err)
	}

	// Remove read permissions from the repo directory to make du fail
	if err := os.Chmod(repoDir, 0000); err != nil {
		t.Fatalf("Failed to change permissions: %v", err)
	}
	// Restore permissions for cleanup
	defer os.Chmod(repoDir, 0755)

	// DiskUsageKB should return 0 when du command fails
	if got := store.DiskUsageKB(ctx, repoName); got != 0 {
		t.Fatalf("expected 0 KB when du fails due to permissions, got %d", got)
	}
}

// TestStore_DiskUsageKB_InvalidRepoName tests DiskUsageKB when repoPath returns an error
// due to invalid full name format (triggers the err != nil branch).
func TestStore_DiskUsageKB_InvalidRepoName(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "gitstore-disk-invalid-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	store, err := gitstore.New(tmpDir)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	// Use an invalid repo name that will fail validateFullName
	invalidRepoName := "../escape/repo"

	// DiskUsageKB should return 0 when repoPath returns an error
	if got := store.DiskUsageKB(ctx, invalidRepoName); got != 0 {
		t.Fatalf("expected 0 KB for invalid repo name, got %d", got)
	}
}
