package service_test

import (
	"errors"
	"testing"
	"time"

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

func TestPullRequestStackLifecycle(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := tenantHandle.Context()

	owner := db.User{Login: "stack-owner", Type: db.TypeUser}
	if err := tenantHandle.DB().Create(&owner).Error; err != nil {
		t.Fatal(err)
	}
	repo := db.Repository{Name: "stack-repo", FullName: "stack-owner/stack-repo", OwnerID: owner.ID, DefaultBranch: "main"}
	if err := tenantHandle.DB().Create(&repo).Error; err != nil {
		t.Fatal(err)
	}
	prs := []db.PullRequest{
		stackTestPR(repo.ID, owner.ID, 1, "main", "stack-one"),
		stackTestPR(repo.ID, owner.ID, 2, "stack-one", "stack-two"),
		stackTestPR(repo.ID, owner.ID, 3, "stack-two", "stack-three"),
	}
	for i := range prs {
		if err := tenantHandle.DB().Create(&prs[i]).Error; err != nil {
			t.Fatal(err)
		}
	}

	stack, err := svc.CreatePullRequestStack(ctx, repo.FullName, []int{1, 2})
	if err != nil {
		t.Fatalf("CreatePullRequestStack: %v", err)
	}
	if stack.Number != 1 || stack.BaseRef != "main" || len(stack.Entries) != 2 {
		t.Fatalf("unexpected stack: number=%d base=%q entries=%d", stack.Number, stack.BaseRef, len(stack.Entries))
	}
	if stack.Entries[0].PullRequest.Number != 1 || stack.Entries[1].PullRequest.Number != 2 {
		t.Fatalf("entries are not bottom-to-top: %+v", stack.Entries)
	}

	stack, err = svc.AddPullRequestsToStack(ctx, repo.FullName, stack.Number, []int{3})
	if err != nil {
		t.Fatalf("AddPullRequestsToStack: %v", err)
	}
	if len(stack.Entries) != 3 || stack.Entries[2].Position != 3 || stack.Entries[2].PullRequest.Number != 3 {
		t.Fatalf("unexpected appended stack: %+v", stack.Entries)
	}

	memberships, err := svc.ListPullRequestStackMemberships(ctx, []uint{prs[0].ID, prs[1].ID, prs[2].ID})
	if err != nil {
		t.Fatalf("ListPullRequestStackMemberships: %v", err)
	}
	if memberships[prs[1].ID].Position != 2 || memberships[prs[1].ID].Size != 3 || memberships[prs[1].ID].BaseRef != "main" {
		t.Fatalf("unexpected membership: %+v", memberships[prs[1].ID])
	}

	result, err := svc.UnstackPullRequestStack(ctx, repo.FullName, stack.Number)
	if err != nil {
		t.Fatalf("UnstackPullRequestStack: %v", err)
	}
	if result != nil {
		t.Fatalf("expected dissolved stack, got %+v", result)
	}
	if _, err := svc.GetPullRequestStack(ctx, repo.FullName, stack.Number); !errors.Is(err, service.ErrNotFound) {
		t.Fatalf("expected dissolved stack to be not found, got %v", err)
	}
}

func TestPullRequestStackRejectsNonLinearAndDuplicateMembership(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := tenantHandle.Context()

	owner := db.User{Login: "stack-validator", Type: db.TypeUser}
	if err := tenantHandle.DB().Create(&owner).Error; err != nil {
		t.Fatal(err)
	}
	repo := db.Repository{Name: "validator", FullName: "stack-validator/validator", OwnerID: owner.ID, DefaultBranch: "main"}
	if err := tenantHandle.DB().Create(&repo).Error; err != nil {
		t.Fatal(err)
	}
	prs := []db.PullRequest{
		stackTestPR(repo.ID, owner.ID, 1, "main", "one"),
		stackTestPR(repo.ID, owner.ID, 2, "unrelated", "two"),
		stackTestPR(repo.ID, owner.ID, 3, "one", "three"),
		stackTestPR(repo.ID, owner.ID, 4, "three", "four"),
	}
	for i := range prs {
		if err := tenantHandle.DB().Create(&prs[i]).Error; err != nil {
			t.Fatal(err)
		}
	}

	if _, err := svc.CreatePullRequestStack(ctx, repo.FullName, []int{1, 2}); !errors.Is(err, service.ErrValidation) {
		t.Fatalf("expected non-linear validation error, got %v", err)
	}
	if _, err := svc.CreatePullRequestStack(ctx, repo.FullName, []int{1, 3}); err != nil {
		t.Fatalf("create valid stack: %v", err)
	}
	if _, err := svc.CreatePullRequestStack(ctx, repo.FullName, []int{3, 4}); !errors.Is(err, service.ErrValidation) {
		t.Fatalf("expected duplicate membership validation error, got %v", err)
	}
}

func TestPullRequestStackRejectsMembersWithActiveMergeWork(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := tenantHandle.Context()

	owner := db.User{Login: "stack-active-owner", Type: db.TypeUser}
	if err := tenantHandle.DB().Create(&owner).Error; err != nil {
		t.Fatal(err)
	}
	repo := db.Repository{Name: "stack-active-repo", FullName: "stack-active-owner/stack-active-repo", OwnerID: owner.ID, DefaultBranch: "main"}
	if err := tenantHandle.DB().Create(&repo).Error; err != nil {
		t.Fatal(err)
	}
	prs := []db.PullRequest{
		stackTestPR(repo.ID, owner.ID, 1, "main", "one"),
		stackTestPR(repo.ID, owner.ID, 2, "one", "two"),
		stackTestPR(repo.ID, owner.ID, 3, "main", "three"),
		stackTestPR(repo.ID, owner.ID, 4, "three", "four"),
		stackTestPR(repo.ID, owner.ID, 5, "four", "five"),
	}
	for i := range prs {
		if err := tenantHandle.DB().Create(&prs[i]).Error; err != nil {
			t.Fatal(err)
		}
	}

	now := time.Now().UTC()
	expiredAt := now.Add(-time.Hour)
	createdAt := now.Add(-2 * time.Hour)
	activeCreate := db.AsyncMergeRequest{
		UUID: "active-create-stack", RepositoryID: repo.ID, PullRequestID: prs[0].ID, ActorID: owner.ID,
		Status: service.AsyncMergePending, MergeMethod: "squash", MergeAction: service.AsyncMergeActionDirect,
		EffectiveMergeAction: service.AsyncMergeActionDirect, ExpectedHeadSHA: prs[0].HeadSHA,
		CreatedAt: createdAt, UpdatedAt: createdAt, ExpiresAt: expiredAt,
	}
	if err := tenantHandle.DB().Create(&activeCreate).Error; err != nil {
		t.Fatal(err)
	}
	if _, err := svc.CreatePullRequestStack(ctx, repo.FullName, []int{1, 2}); !errors.Is(err, service.ErrConflict) {
		t.Fatalf("expected active merge work to block stack creation, got %v", err)
	}

	stack, err := svc.CreatePullRequestStack(ctx, repo.FullName, []int{3, 4})
	if err != nil {
		t.Fatalf("create append target stack: %v", err)
	}
	activeAdd := db.AsyncMergeRequest{
		UUID: "active-add-stack", RepositoryID: repo.ID, PullRequestID: prs[4].ID, ActorID: owner.ID,
		Status: service.AsyncMergePending, MergeMethod: "squash", MergeAction: service.AsyncMergeActionDirect,
		EffectiveMergeAction: service.AsyncMergeActionDirect, ExpectedHeadSHA: prs[4].HeadSHA,
		CreatedAt: createdAt, UpdatedAt: createdAt, ExpiresAt: expiredAt,
	}
	if err := tenantHandle.DB().Create(&activeAdd).Error; err != nil {
		t.Fatal(err)
	}
	if _, err := svc.AddPullRequestsToStack(ctx, repo.FullName, stack.Number, []int{5}); !errors.Is(err, service.ErrConflict) {
		t.Fatalf("expected active merge work to block stack append, got %v", err)
	}

	activeUnstack := db.AsyncMergeRequest{
		UUID: "active-unstack-prefix", RepositoryID: repo.ID, PullRequestID: prs[3].ID, ActorID: owner.ID,
		Status: service.AsyncMergePending, MergeMethod: "squash", MergeAction: service.AsyncMergeActionDirect,
		EffectiveMergeAction: service.AsyncMergeActionDirect, ExpectedHeadSHA: prs[3].HeadSHA,
		CreatedAt: createdAt, UpdatedAt: createdAt, ExpiresAt: expiredAt,
	}
	if err := tenantHandle.DB().Create(&activeUnstack).Error; err != nil {
		t.Fatal(err)
	}
	if _, err := svc.UnstackPullRequestStack(ctx, repo.FullName, stack.Number); !errors.Is(err, service.ErrValidation) {
		t.Fatalf("expected active direct Stack merge to preserve its full prefix, got %v", err)
	}
	current, err := svc.GetPullRequestStack(ctx, repo.FullName, stack.Number)
	if err != nil || len(current.Entries) != 2 {
		t.Fatalf("Stack changed while direct merge was active: %+v, %v", current, err)
	}
}

func TestPullRequestStackMembershipUsesCurrentUltimateBaseSHA(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := tenantHandle.Context()

	owner := db.User{Login: "stack-base-owner", Type: db.TypeUser}
	if err := tenantHandle.DB().Create(&owner).Error; err != nil {
		t.Fatal(err)
	}
	repo, err := svc.CreateRepo(ctx, service.CreateRepoInput{
		OwnerLogin: owner.Login, Name: "stack-base-repo", DefaultBranch: "main", AutoInit: true,
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := tenantHandle.Git().CreateBranch(ctx, repo.FullName, "stack-one", "main"); err != nil {
		t.Fatal(err)
	}
	if _, err := tenantHandle.Git().WriteFile(ctx, repo.FullName, "stack-one", "one.txt", "one", []byte("one\n")); err != nil {
		t.Fatal(err)
	}
	if err := tenantHandle.Git().CreateBranch(ctx, repo.FullName, "stack-two", "stack-one"); err != nil {
		t.Fatal(err)
	}
	if _, err := tenantHandle.Git().WriteFile(ctx, repo.FullName, "stack-two", "two.txt", "two", []byte("two\n")); err != nil {
		t.Fatal(err)
	}
	authCtx := testharness.ContextWithUser(t, ctx, owner)
	first, err := svc.CreatePR(authCtx, service.CreatePRInput{
		RepoFullName: repo.FullName, Title: "one", HeadRef: "stack-one", BaseRef: "main", AuthorLogin: owner.Login,
	})
	if err != nil {
		t.Fatal(err)
	}
	second, err := svc.CreatePR(authCtx, service.CreatePRInput{
		RepoFullName: repo.FullName, Title: "two", HeadRef: "stack-two", BaseRef: "stack-one", AuthorLogin: owner.Login,
	})
	if err != nil {
		t.Fatal(err)
	}
	stack, err := svc.CreatePullRequestStack(authCtx, repo.FullName, []int{first.Number, second.Number})
	if err != nil {
		t.Fatal(err)
	}
	newBaseSHA, err := tenantHandle.Git().WriteFile(ctx, repo.FullName, "main", "base.txt", "advance base", []byte("base\n"))
	if err != nil {
		t.Fatal(err)
	}
	if newBaseSHA == stack.BaseSHA {
		t.Fatal("test did not advance the ultimate base")
	}
	memberships, err := svc.ListPullRequestStackMemberships(authCtx, []uint{second.ID})
	if err != nil {
		t.Fatal(err)
	}
	if got := memberships[second.ID].BaseSHA; got != newBaseSHA {
		t.Fatalf("membership base SHA = %q, want current ultimate base %q", got, newBaseSHA)
	}
}

func stackTestPR(repoID, authorID uint, number int, base, head string) db.PullRequest {
	return db.PullRequest{
		Number: number, RepositoryID: repoID, HeadRepositoryID: repoID,
		Title: head, State: db.StateOpen, BaseRef: base, HeadRef: head,
		BaseSHA: "base-sha", HeadSHA: "head-sha", AuthorID: authorID,
	}
}
