package service

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

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

func TestMergePRRecordConflictLeavesRefAndProjectionUnchanged(t *testing.T) {
	fixture := newPrivateMergeServiceFixture(t, "merge-conflict", 0)
	session, err := fixture.service.repositoryMutations.AcquireSession(t.Context(), fixture.repository.ID, fixture.repository.FullName)
	if err != nil {
		t.Fatalf("AcquireSession: %v", err)
	}
	if _, err := session.WriteFile(t.Context(), "main", "collision.txt", "main collision", []byte("main\n"), ""); err != nil {
		_ = session.Release()
		t.Fatalf("WriteFile(main collision): %v", err)
	}
	if _, err := session.WriteFile(t.Context(), "feature", "collision.txt", "feature collision", []byte("feature\n"), ""); err != nil {
		_ = session.Release()
		t.Fatalf("WriteFile(feature collision): %v", err)
	}
	if err := session.Release(); err != nil {
		t.Fatalf("Release: %v", err)
	}
	baseSHA, err := fixture.service.Git.HeadSHA(t.Context(), fixture.repository.FullName, "main")
	if err != nil {
		t.Fatalf("HeadSHA(main): %v", err)
	}
	err = fixture.service.mergePRRecordWithActor(t.Context(), fixture.actor, &fixture.pullRequest, "merge", "conflict")
	if !errors.Is(err, ErrConflict) {
		t.Fatalf("mergePRRecordWithActor = %v, want conflict", err)
	}
	assertPrivateMergeProjectionUnchanged(t, fixture, baseSHA)
}

func TestMergePRRecordProjectionCancellationRestoresBaseRef(t *testing.T) {
	fixture := newPrivateMergeServiceFixture(t, "projection-cancel", 0)
	baseSHA, err := fixture.service.Git.HeadSHA(t.Context(), fixture.repository.FullName, "main")
	if err != nil {
		t.Fatalf("HeadSHA(main): %v", err)
	}
	ctx, cancel := context.WithCancel(t.Context())
	if err := fixture.database.Callback().Update().Before("gorm:update").Register("cancel-merge-projection", func(tx *gorm.DB) {
		if tx.Statement.Table == "pull_requests" {
			cancel()
			tx.AddError(ctx.Err())
		}
	}); err != nil {
		t.Fatalf("register projection cancellation: %v", err)
	}
	err = fixture.service.mergePRRecordWithActor(ctx, fixture.actor, &fixture.pullRequest, "rebase", "canceled projection")
	if !errors.Is(err, context.Canceled) {
		t.Fatalf("mergePRRecordWithActor = %v, want context.Canceled", err)
	}
	assertPrivateMergeProjectionUnchanged(t, fixture, baseSHA)
}

func TestMergePRRecordProjectionFailureRestoresBaseRef(t *testing.T) {
	fixture := newPrivateMergeServiceFixture(t, "projection-failure", 0)
	baseSHA, err := fixture.service.Git.HeadSHA(t.Context(), fixture.repository.FullName, "main")
	if err != nil {
		t.Fatalf("HeadSHA(main): %v", err)
	}
	projectionFailure := errors.New("injected merge projection failure")
	if err := fixture.database.Callback().Update().Before("gorm:update").Register("fail-merge-projection", func(tx *gorm.DB) {
		if tx.Statement.Table == "pull_requests" {
			tx.AddError(projectionFailure)
		}
	}); err != nil {
		t.Fatalf("register projection failure: %v", err)
	}

	err = fixture.service.mergePRRecordWithActor(t.Context(), fixture.actor, &fixture.pullRequest, "merge", "projection failure")
	if !errors.Is(err, projectionFailure) {
		t.Fatalf("mergePRRecordWithActor = %v, want projection failure", err)
	}
	assertPrivateMergeProjectionUnchanged(t, fixture, baseSHA)
}

func assertPrivateMergeProjectionUnchanged(t *testing.T, fixture privateMergeServiceFixture, baseSHA string) {
	t.Helper()
	afterSHA, err := fixture.service.Git.HeadSHA(t.Context(), fixture.repository.FullName, "main")
	if err != nil || afterSHA != baseSHA {
		t.Fatalf("base after failed merge = %s, err=%v, want %s", afterSHA, err, baseSHA)
	}
	var stored db.PullRequest
	if err := fixture.database.First(&stored, fixture.pullRequest.ID).Error; err != nil {
		t.Fatalf("reload pull request: %v", err)
	}
	if stored.Merged || stored.State != db.StateOpen || stored.MergeCommitSHA != "" || fixture.pullRequest.Merged {
		t.Fatalf("pull request changed after failed merge: stored=%+v memory=%+v", stored, fixture.pullRequest)
	}
}

func TestMergePRRecordFailsClosedWithoutPrivateOwner(t *testing.T) {
	fixture := newPrivateMergeServiceFixture(t, "owner-missing", 0)
	baseSHA, err := fixture.service.Git.HeadSHA(t.Context(), fixture.repository.FullName, "main")
	if err != nil {
		t.Fatalf("HeadSHA(main): %v", err)
	}
	fixture.service.repositoryMutations = nil
	err = fixture.service.mergePRRecordWithActor(t.Context(), fixture.actor, &fixture.pullRequest, "merge", "blocked")
	if !errors.Is(err, ErrRepositoryMigrationConflict) {
		t.Fatalf("mergePRRecordWithActor = %v, want migration conflict", err)
	}
	afterSHA, err := fixture.service.Git.HeadSHA(t.Context(), fixture.repository.FullName, "main")
	if err != nil || afterSHA != baseSHA {
		t.Fatalf("base without owner = %s, err=%v, want %s", afterSHA, err, baseSHA)
	}
	var stored db.PullRequest
	if err := fixture.database.First(&stored, fixture.pullRequest.ID).Error; err != nil {
		t.Fatalf("reload pull request: %v", err)
	}
	if stored.Merged || stored.State != db.StateOpen {
		t.Fatalf("pull request changed without owner: %+v", stored)
	}
}

func TestMergePRRecordDatabaseCostDoesNotGrowWithUnrelatedPullRequests(t *testing.T) {
	run := func(label string, unrelated int) (int64, int64) {
		fixture := newPrivateMergeServiceFixture(t, label, unrelated)
		var queries atomic.Int64
		var updates atomic.Int64
		if err := fixture.database.Callback().Query().Before("gorm:query").Register("count-merge-query-"+label, func(*gorm.DB) {
			queries.Add(1)
		}); err != nil {
			t.Fatalf("register query counter: %v", err)
		}
		if err := fixture.database.Callback().Update().Before("gorm:update").Register("count-merge-update-"+label, func(*gorm.DB) {
			updates.Add(1)
		}); err != nil {
			t.Fatalf("register update counter: %v", err)
		}
		if err := fixture.service.mergePRRecordWithActor(t.Context(), fixture.actor, &fixture.pullRequest, "squash", "bounded projection"); err != nil {
			t.Fatalf("mergePRRecordWithActor(%d unrelated): %v", unrelated, err)
		}
		if !fixture.pullRequest.Merged || fixture.pullRequest.MergeCommitSHA == "" {
			t.Fatalf("merge result incomplete: %+v", fixture.pullRequest)
		}
		return queries.Load(), updates.Load()
	}

	zeroQueries, zeroUpdates := run("zero", 0)
	manyQueries, manyUpdates := run("many", 64)
	if zeroQueries != manyQueries || zeroUpdates != manyUpdates || zeroUpdates != 1 {
		t.Fatalf("merge DB cost = zero:%d/%d many:%d/%d, want equal queries and one update", zeroQueries, zeroUpdates, manyQueries, manyUpdates)
	}
}

type privateMergeServiceFixture struct {
	service     *Service
	database    *gorm.DB
	repository  db.Repository
	pullRequest db.PullRequest
	actor       db.User
}

func newPrivateMergeServiceFixture(t *testing.T, label string, unrelated int) privateMergeServiceFixture {
	t.Helper()
	database, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "merge-service.db")), &gorm.Config{})
	if err != nil {
		t.Fatalf("open database: %v", err)
	}
	if err := database.AutoMigrate(
		&db.Repository{},
		&db.RepositoryMigrationFreeze{},
		&db.RepositoryPushRecovery{},
		&db.RepositoryPushOutbox{},
		&db.RepositoryWebhookFanout{},
		&db.Release{},
		&db.PullRequest{},
	); err != nil {
		t.Fatalf("AutoMigrate: %v", err)
	}
	repository := db.Repository{ID: 1, Name: "repo", FullName: "owner/" + label, DefaultBranch: "main"}
	if err := database.Create(&repository).Error; err != nil {
		t.Fatalf("create repository: %v", err)
	}
	pullRequest := db.PullRequest{
		ID: 1, Number: 1, RepositoryID: repository.ID, HeadRepositoryID: repository.ID,
		State: db.StateOpen, BaseRef: "main", HeadRef: "feature",
	}
	if err := database.Create(&pullRequest).Error; err != nil {
		t.Fatalf("create pull request: %v", err)
	}
	for index := 0; index < unrelated; index++ {
		unrelatedPR := db.PullRequest{
			Number: index + 2, RepositoryID: repository.ID, HeadRepositoryID: repository.ID,
			State: db.StateOpen, BaseRef: "main", HeadRef: fmt.Sprintf("unrelated-%d", index),
		}
		if err := database.Create(&unrelatedPR).Error; err != nil {
			t.Fatalf("create unrelated pull request %d: %v", index, err)
		}
	}
	root := filepath.Join(t.TempDir(), "repositories")
	if err := os.MkdirAll(root, 0o750); err != nil {
		t.Fatalf("create repository root: %v", err)
	}
	store, err := gitstore.New(root)
	if err != nil {
		t.Fatalf("gitstore.New: %v", err)
	}
	owner, err := repositorymutation.NewOwner(root, database)
	if err != nil {
		t.Fatalf("NewOwner: %v", err)
	}
	if err := owner.EnsureRepository(t.Context(), repository.ID); err != nil {
		t.Fatalf("EnsureRepository: %v", err)
	}
	session, err := owner.AcquireSession(t.Context(), repository.ID, repository.FullName)
	if err != nil {
		t.Fatalf("AcquireSession: %v", err)
	}
	if _, err := session.CommitFilesAt(t.Context(), "main", "seed", []repositorymutation.FileMutation{{Path: "README.md", Content: []byte("seed\n")}}, time.Now()); err != nil {
		_ = session.Release()
		t.Fatalf("seed repository: %v", err)
	}
	if err := session.CreateBranch(t.Context(), "feature", "main"); err != nil {
		_ = session.Release()
		t.Fatalf("CreateBranch: %v", err)
	}
	if _, err := session.WriteFile(t.Context(), "main", "main.txt", "main change", []byte("main\n"), ""); err != nil {
		_ = session.Release()
		t.Fatalf("WriteFile(main): %v", err)
	}
	if _, err := session.WriteFile(t.Context(), "feature", "feature.txt", "feature change", []byte("feature\n"), ""); err != nil {
		_ = session.Release()
		t.Fatalf("WriteFile(feature): %v", err)
	}
	if err := session.Release(); err != nil {
		t.Fatalf("Release: %v", err)
	}
	pullRequest.Repository = repository
	service := &Service{Ctx: t.Context(), DB: database, Git: store, repositoryMutations: owner}
	return privateMergeServiceFixture{
		service: service, database: database, repository: repository, pullRequest: pullRequest,
		actor: db.User{Login: "merge-actor", Email: "actor@example.com"},
	}
}
