package service

import (
	"context"
	"errors"
	"path/filepath"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/glebarez/sqlite"
	git "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/gitstore"
	"gorm.io/gorm"
)

func TestUpdateRepoDefaultBranchRetainsOwnerThroughDBSaveAndCompensatesFailure(t *testing.T) {
	database, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "default-branch.db")), &gorm.Config{})
	if err != nil {
		t.Fatalf("open database: %v", err)
	}
	if err := database.AutoMigrate(
		&db.User{}, &db.Repository{}, &db.Label{}, &db.RepositoryMigrationFreeze{}, &db.RepositoryPushRecovery{},
		&db.RepoRedirect{}, &db.Collaborator{}, &db.Autolink{},
	); err != nil {
		t.Fatalf("AutoMigrate: %v", err)
	}
	root := filepath.Join(t.TempDir(), "repositories")
	store, err := gitstore.New(root)
	if err != nil {
		t.Fatalf("gitstore.New: %v", err)
	}
	svc, err := New(Config{DB: database, Git: store})
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	owner := db.User{Login: "default-owner", Type: db.TypeUser, SiteAdmin: true}
	if err := database.Create(&owner).Error; err != nil {
		t.Fatalf("create owner: %v", err)
	}
	repository := db.Repository{ID: 91, OwnerID: owner.ID, Name: "repo", FullName: "default-owner/repo", DefaultBranch: "main"}
	if err := database.Create(&repository).Error; err != nil {
		t.Fatalf("create repository: %v", err)
	}
	setup, err := svc.repositoryMutations.AcquireSession(t.Context(), repository.ID, repository.FullName)
	if err != nil {
		t.Fatalf("AcquireSession(setup): %v", err)
	}
	if err := setup.InitializeRepository("main", true); err != nil {
		t.Fatalf("InitializeRepository: %v", err)
	}
	if err := setup.CreateBranch(t.Context(), "feature", "main"); err != nil {
		t.Fatalf("CreateBranch(feature): %v", err)
	}
	if err := setup.CreateBranch(t.Context(), "feature+plus", "main"); err != nil {
		t.Fatalf("CreateBranch(feature+plus): %v", err)
	}
	if err := setup.Release(); err != nil {
		t.Fatalf("Release(setup): %v", err)
	}

	saveStarted := make(chan struct{})
	saveContinue := make(chan struct{})
	continueSave := sync.OnceFunc(func() { close(saveContinue) })
	defer continueSave()
	var blockSave atomic.Bool
	blockSave.Store(true)
	var saveStartedOnce sync.Once
	const blockingCallback = "test:update_repo_default_branch_block_save"
	if err := database.Callback().Update().Before("gorm:update").Register(blockingCallback, func(tx *gorm.DB) {
		if !blockSave.Load() || tx.Statement.Schema == nil || tx.Statement.Schema.Name != "Repository" {
			return
		}
		saveStartedOnce.Do(func() { close(saveStarted) })
		<-saveContinue
	}); err != nil {
		t.Fatalf("register blocking update callback: %v", err)
	}

	feature := "feature"
	updateDone := make(chan error, 1)
	go func() {
		_, err := svc.UpdateRepo(t.Context(), repository.FullName, UpdateRepoInput{DefaultBranch: &feature})
		updateDone <- err
	}()
	select {
	case <-saveStarted:
	case <-time.After(5 * time.Second):
		t.Fatal("default-branch DB save did not start")
	}

	acquired := make(chan error, 1)
	go func() {
		session, err := svc.repositoryMutations.AcquireSession(t.Context(), repository.ID, repository.FullName)
		if err == nil {
			err = session.Release()
		}
		acquired <- err
	}()
	select {
	case err := <-acquired:
		t.Fatalf("AcquireSession completed before DB save: %v", err)
	case <-time.After(150 * time.Millisecond):
	}
	blockSave.Store(false)
	continueSave()
	if err := <-updateDone; err != nil {
		t.Fatalf("UpdateRepo(feature): %v", err)
	}
	if err := <-acquired; err != nil {
		t.Fatalf("AcquireSession after DB save: %v", err)
	}
	assertRepositoryDefaultBranch(t, database, store, repository, "feature")

	invalid := " feature "
	if _, err := svc.UpdateRepo(t.Context(), repository.FullName, UpdateRepoInput{DefaultBranch: &invalid}); !errors.Is(err, gitstore.ErrInvalidRefName) {
		t.Fatalf("UpdateRepo(invalid branch) = %v, want ErrInvalidRefName", err)
	}
	missing := "missing"
	if _, err := svc.UpdateRepo(t.Context(), repository.FullName, UpdateRepoInput{DefaultBranch: &missing}); err == nil {
		t.Fatal("UpdateRepo accepted missing default branch")
	}
	assertRepositoryDefaultBranch(t, database, store, repository, "feature")

	if err := database.Callback().Update().Remove(blockingCallback); err != nil {
		t.Fatalf("remove blocking update callback: %v", err)
	}
	setRepositoryDefaultBranchFixture(t, svc, database, store, repository, "feature+plus")
	assertRepositoryDefaultBranch(t, database, store, repository, "feature+plus")
	forcedSaveFailure := errors.New("forced repository save failure")
	const failingCallback = "test:update_repo_default_branch_fail_save"
	if err := database.Callback().Update().Before("gorm:update").Register(failingCallback, func(tx *gorm.DB) {
		if tx.Statement.Schema != nil && tx.Statement.Schema.Name == "Repository" {
			tx.AddError(forcedSaveFailure)
		}
	}); err != nil {
		t.Fatalf("register failing update callback: %v", err)
	}

	main := "main"
	if _, err := svc.UpdateRepo(t.Context(), repository.FullName, UpdateRepoInput{DefaultBranch: &main}); !errors.Is(err, forcedSaveFailure) {
		t.Fatalf("UpdateRepo(DB failure) = %v, want forced failure", err)
	}
	assertRepositoryDefaultBranch(t, database, store, repository, "feature+plus")
	if err := database.Callback().Update().Remove(failingCallback); err != nil {
		t.Fatalf("remove failing update callback: %v", err)
	}

	cancelCtx, cancel := context.WithCancel(t.Context())
	const cancelingCallback = "test:update_repo_default_branch_cancel_save"
	if err := database.Callback().Update().Before("gorm:update").Register(cancelingCallback, func(tx *gorm.DB) {
		if tx.Statement.Schema != nil && tx.Statement.Schema.Name == "Repository" {
			cancel()
		}
	}); err != nil {
		t.Fatalf("register canceling update callback: %v", err)
	}
	if _, err := svc.UpdateRepo(cancelCtx, repository.FullName, UpdateRepoInput{DefaultBranch: &main}); err == nil || !errors.Is(cancelCtx.Err(), context.Canceled) {
		t.Fatalf("UpdateRepo(canceled DB save) = %v, context error=%v", err, cancelCtx.Err())
	}
	assertRepositoryDefaultBranch(t, database, store, repository, "feature+plus")
	if err := database.Callback().Update().Remove(cancelingCallback); err != nil {
		t.Fatalf("remove canceling update callback: %v", err)
	}

	newName := "renamed"
	renamed, err := svc.UpdateRepo(t.Context(), repository.FullName, UpdateRepoInput{Name: &newName, DefaultBranch: &main})
	if err != nil {
		t.Fatalf("UpdateRepo(rename and default branch): %v", err)
	}
	if renamed.FullName != "default-owner/renamed" {
		t.Fatalf("renamed full name = %q, want default-owner/renamed", renamed.FullName)
	}
	assertRepositoryDefaultBranch(t, database, store, renamed, "main")
}

func setRepositoryDefaultBranchFixture(t *testing.T, svc *Service, database *gorm.DB, store *gitstore.Store, repository db.Repository, branch string) {
	t.Helper()
	session, err := svc.repositoryMutations.AcquireSession(t.Context(), repository.ID, repository.FullName)
	if err != nil {
		t.Fatalf("AcquireSession(default branch fixture): %v", err)
	}
	defer func() {
		if session != nil {
			_ = session.Release()
		}
	}()
	path, err := store.GetRepoPath(t.Context(), repository.FullName)
	if err != nil {
		t.Fatalf("GetRepoPath(default branch fixture): %v", err)
	}
	gitRepository, err := git.PlainOpen(path)
	if err != nil {
		t.Fatalf("open default branch fixture: %v", err)
	}
	if err := gitRepository.Storer.SetReference(plumbing.NewSymbolicReference(
		plumbing.HEAD, plumbing.NewBranchReferenceName(branch),
	)); err != nil {
		t.Fatalf("set default branch fixture HEAD: %v", err)
	}
	if err := database.Model(&db.Repository{}).Where("id = ?", repository.ID).Update("default_branch", branch).Error; err != nil {
		t.Fatalf("set default branch fixture row: %v", err)
	}
	if err := session.Release(); err != nil {
		t.Fatalf("Release(default branch fixture): %v", err)
	}
	session = nil
	RepoCacheInvalidate(t.Context(), repository.ID)
}

func assertRepositoryDefaultBranch(t *testing.T, database *gorm.DB, store *gitstore.Store, repository db.Repository, want string) {
	t.Helper()
	var stored db.Repository
	if err := database.Select("id", "default_branch").First(&stored, repository.ID).Error; err != nil {
		t.Fatalf("load repository default branch: %v", err)
	}
	if stored.DefaultBranch != want {
		t.Fatalf("database default branch = %q, want %q", stored.DefaultBranch, want)
	}
	path, err := store.GetRepoPath(t.Context(), repository.FullName)
	if err != nil {
		t.Fatalf("GetRepoPath: %v", err)
	}
	gitRepository, err := git.PlainOpen(path)
	if err != nil {
		t.Fatalf("open repository: %v", err)
	}
	head, err := gitRepository.Reference(plumbing.HEAD, false)
	wantRef := plumbing.NewBranchReferenceName(want)
	if err != nil || head.Type() != plumbing.SymbolicReference || head.Target() != wantRef {
		t.Fatalf("Git HEAD = %v, err=%v, want %s", head, err, wantRef)
	}
}
