package service_test

import (
	"context"
	"errors"
	"strings"
	"testing"

	"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 setupProtectedPR(t testing.TB, svc *service.Service, tenantHandle *testharness.TenantHandle, login, repoName string, allowAutoMerge bool) (db.PullRequest, context.Context, db.User) {
	t.Helper()
	ctx := tenantHandle.Context()

	user := db.User{Login: login, Name: login, Type: db.TypeUser}
	if err := tenantHandle.DB().Create(&user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}

	repo, err := svc.CreateRepo(ctx, service.CreateRepoInput{
		OwnerLogin:     login,
		Name:           repoName,
		DefaultBranch:  "main",
		AutoInit:       true,
		AllowAutoMerge: &allowAutoMerge,
	})
	if err != nil {
		t.Fatalf("create repo: %v", err)
	}

	if err := tenantHandle.Git().CreateBranch(ctx, repo.FullName, "feature", "main"); err != nil {
		t.Fatalf("create branch: %v", err)
	}
	if _, err := tenantHandle.Git().WriteFile(ctx, repo.FullName, "feature", "hello.txt", "add test file", []byte("hello world\n")); err != nil {
		t.Fatalf("write file: %v", err)
	}

	authCtx := testContextWithUser(ctx, user)
	pr, err := svc.CreatePR(authCtx, service.CreatePRInput{
		RepoFullName: repo.FullName,
		Title:        "Protected PR",
		Body:         "test body",
		HeadRef:      "feature",
		BaseRef:      "main",
		AuthorLogin:  login,
	})
	if err != nil {
		t.Fatalf("create PR: %v", err)
	}
	return pr, authCtx, user
}

func protectBranch(t testing.TB, svc *service.Service, ctx context.Context, repoID uint, branch, requiredStatusJSON, requiredReviewJSON string, enforceAdmins bool) {
	t.Helper()
	err := svc.UpdateBranchProtection(ctx, &db.BranchProtection{
		RepositoryID:             repoID,
		BranchName:               branch,
		RequiredStatusChecksJSON: requiredStatusJSON,
		RequiredPullRequestJSON:  requiredReviewJSON,
		EnforceAdmins:            enforceAdmins,
	})
	if err != nil {
		t.Fatalf("UpdateBranchProtection: %v", err)
	}
}

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

	pr, authCtx, _ := setupProtectedPR(t, svc, tenantHandle, "review-user", "review-repo", false)
	protectBranch(t, svc, tenantHandle.Context(), pr.RepositoryID, pr.BaseRef, "", `{"required_approving_review_count":1}`, true)

	_, err := svc.MergePR(authCtx, "review-user/review-repo", pr.Number, "merge", "")
	if !errors.Is(err, service.ErrInvalidState) {
		t.Fatalf("expected ErrInvalidState, got %v", err)
	}

	stored, getErr := svc.GetPRByID(authCtx, pr.ID)
	if getErr != nil {
		t.Fatalf("GetPRByID: %v", getErr)
	}
	if stored.Merged {
		t.Fatal("expected PR to remain unmerged")
	}
}

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

	pr, authCtx, user := setupProtectedPR(t, svc, tenantHandle, "bypass-user", "bypass-repo", false)
	protectBranch(t, svc, tenantHandle.Context(), pr.RepositoryID, pr.BaseRef, "", `{"required_approving_review_count":1,"bypass_pull_request_allowances":{"users":["`+user.Login+`"]}}`, true)

	merged, err := svc.MergePR(authCtx, "bypass-user/bypass-repo", pr.Number, "merge", "")
	if err != nil {
		t.Fatalf("MergePR: %v", err)
	}
	if !merged.Merged {
		t.Fatal("expected PR to be merged")
	}
}

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

	pr, authCtx, user := setupProtectedPR(t, svc, tenantHandle, "bypass-status-user", "bypass-status-repo", false)
	protectBranch(t, svc, tenantHandle.Context(), pr.RepositoryID, pr.BaseRef, `{"contexts":["CI"]}`, `{"required_approving_review_count":1,"bypass_pull_request_allowances":{"users":["`+user.Login+`"]}}`, true)

	_, err := svc.MergePR(authCtx, "bypass-status-user/bypass-status-repo", pr.Number, "merge", "")
	if !errors.Is(err, service.ErrInvalidState) {
		t.Fatalf("expected ErrInvalidState before required status checks pass, got %v", err)
	}

	status := &db.CommitStatus{
		RepositoryID: pr.RepositoryID,
		CommitSHA:    pr.HeadSHA,
		State:        "success",
		Context:      "CI",
		CreatorID:    user.ID,
	}
	if err := svc.CreateCommitStatus(authCtx, status); err != nil {
		t.Fatalf("CreateCommitStatus: %v", err)
	}

	merged, err := svc.MergePR(authCtx, "bypass-status-user/bypass-status-repo", pr.Number, "merge", "")
	if err != nil {
		t.Fatalf("MergePR after status success: %v", err)
	}
	if !merged.Merged {
		t.Fatal("expected PR to merge once required status checks pass")
	}
}

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

	pr, authCtx, user := setupProtectedPR(t, svc, tenantHandle, "provider-check-user", "provider-check-repo", false)
	protectBranch(t, svc, authCtx, pr.RepositoryID, pr.BaseRef,
		`{"checks":[{"context":"CI","app_id":42}]}`, "", true)

	wrongApp := int64(7)
	if err := svc.CreateCommitStatus(authCtx, &db.CommitStatus{
		RepositoryID: pr.RepositoryID, CommitSHA: pr.HeadSHA, State: "success",
		Context: "CI", IntegrationID: &wrongApp, CreatorID: user.ID,
	}); err != nil {
		t.Fatal(err)
	}
	if _, err := svc.MergePR(authCtx, pr.Repository.FullName, pr.Number, "merge", ""); !errors.Is(err, service.ErrInvalidState) {
		t.Fatalf("same-name status from wrong App satisfied required check: %v", err)
	}

	requiredApp := int64(42)
	if err := svc.CreateCommitStatus(authCtx, &db.CommitStatus{
		RepositoryID: pr.RepositoryID, CommitSHA: pr.HeadSHA, State: "success",
		Context: "CI", IntegrationID: &requiredApp, CreatorID: user.ID,
	}); err != nil {
		t.Fatal(err)
	}
	merged, err := svc.MergePR(authCtx, pr.Repository.FullName, pr.Number, "merge", "")
	if err != nil || !merged.Merged {
		t.Fatalf("required App status did not satisfy check: merged=%v err=%v", merged.Merged, err)
	}
}

func TestMergePR_OmittedStatusCheckProviderUsesWriteTimeGitHubSemantics(t *testing.T) {
	for _, test := range []struct {
		name           string
		requiredJSON   string
		wrongAppPasses bool
	}{
		{name: "checks omitted app_id", requiredJSON: `{"checks":[{"context":"CI"}]}`},
		{name: "legacy contexts", requiredJSON: `{"contexts":["CI"]}`},
		{name: "explicit any app", requiredJSON: `{"checks":[{"context":"CI","app_id":-1}]}`, wrongAppPasses: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			svc, tenantHandle, cleanup := setupTestService(t)
			defer cleanup()

			pr, authCtx, user := setupProtectedPR(t, svc, tenantHandle, "provider-write-user-"+strings.ReplaceAll(test.name, " ", "-"), "provider-write-repo", false)
			recentApp := int64(42)
			if err := svc.CreateCommitStatus(authCtx, &db.CommitStatus{
				RepositoryID: pr.RepositoryID, CommitSHA: pr.HeadSHA, State: "failure",
				Context: "CI", IntegrationID: &recentApp, CreatorID: user.ID,
			}); err != nil {
				t.Fatal(err)
			}
			protectBranch(t, svc, authCtx, pr.RepositoryID, pr.BaseRef, test.requiredJSON, "", true)

			wrongApp := int64(7)
			if err := svc.CreateCommitStatus(authCtx, &db.CommitStatus{
				RepositoryID: pr.RepositoryID, CommitSHA: pr.HeadSHA, State: "success",
				Context: "CI", IntegrationID: &wrongApp, CreatorID: user.ID,
			}); err != nil {
				t.Fatal(err)
			}
			merged, err := svc.MergePR(authCtx, pr.Repository.FullName, pr.Number, "merge", "")
			if test.wrongAppPasses {
				if err != nil || !merged.Merged {
					t.Fatalf("explicit app_id -1 did not allow another provider: merged=%v err=%v", merged.Merged, err)
				}
				return
			}
			if !errors.Is(err, service.ErrInvalidState) {
				t.Fatalf("omitted provider accepted a same-name check from another App: %v", err)
			}

			// A new full write must re-run provider selection even when the
			// public JSON is byte-for-byte identical. The most recent provider is
			// now wrongApp, so the same successful status becomes authoritative.
			protectBranch(t, svc, authCtx, pr.RepositoryID, pr.BaseRef, test.requiredJSON, "", true)
			merged, err = svc.MergePR(authCtx, pr.Repository.FullName, pr.Number, "merge", "")
			if err != nil || !merged.Merged {
				t.Fatalf("repeated write did not rebind the most recent provider: merged=%v err=%v", merged.Merged, err)
			}
		})
	}
}

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

	pr, authCtx, user := setupProtectedPR(t, svc, tenantHandle, "provider-migration-user", "provider-migration-repo", false)
	historicalApp := int64(42)
	if err := svc.CreateCommitStatus(authCtx, &db.CommitStatus{
		RepositoryID: pr.RepositoryID, CommitSHA: pr.HeadSHA, State: "failure",
		Context: "CI", IntegrationID: &historicalApp, CreatorID: user.ID,
	}); err != nil {
		t.Fatal(err)
	}
	legacy := db.BranchProtection{
		RepositoryID: pr.RepositoryID, BranchName: pr.BaseRef,
		RequiredStatusChecksJSON: `{"contexts":["CI"]}`, EnforceAdmins: true,
	}
	if err := tenantHandle.DB().Create(&legacy).Error; err != nil {
		t.Fatal(err)
	}

	wrongApp := int64(7)
	if err := svc.CreateCommitStatus(authCtx, &db.CommitStatus{
		RepositoryID: pr.RepositoryID, CommitSHA: pr.HeadSHA, State: "success",
		Context: "CI", IntegrationID: &wrongApp, CreatorID: user.ID,
	}); err != nil {
		t.Fatal(err)
	}
	if _, err := svc.MergePR(authCtx, pr.Repository.FullName, pr.Number, "merge", ""); !errors.Is(err, service.ErrInvalidState) {
		t.Fatalf("post-protection provider rewrote legacy binding: %v", err)
	}
	migrated, err := svc.GetBranchProtection(authCtx, pr.RepositoryID, pr.BaseRef)
	if err != nil {
		t.Fatal(err)
	}
	if migrated.RequiredStatusChecksProviderBindingPending || !strings.Contains(migrated.ResolvedRequiredStatusChecksJSON, `"app_id":42`) {
		t.Fatalf("legacy provider binding = %+v", migrated)
	}

	if err := svc.CreateCommitStatus(authCtx, &db.CommitStatus{
		RepositoryID: pr.RepositoryID, CommitSHA: pr.HeadSHA, State: "success",
		Context: "CI", IntegrationID: &historicalApp, CreatorID: user.ID,
	}); err != nil {
		t.Fatal(err)
	}
	merged, err := svc.MergePR(authCtx, pr.Repository.FullName, pr.Number, "merge", "")
	if err != nil || !merged.Merged {
		t.Fatalf("historically bound provider did not satisfy migrated protection: merged=%v err=%v", merged.Merged, err)
	}
}

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

	pr, authCtx, user := setupProtectedPR(t, svc, tenantHandle, "provider-fail-closed-user", "provider-fail-closed-repo", false)
	legacy := db.BranchProtection{
		RepositoryID: pr.RepositoryID, BranchName: pr.BaseRef,
		RequiredStatusChecksJSON: `{"checks":[{"context":"CI"}]}`, EnforceAdmins: true,
	}
	if err := tenantHandle.DB().Create(&legacy).Error; err != nil {
		t.Fatal(err)
	}
	currentApp := int64(7)
	if err := svc.CreateCommitStatus(authCtx, &db.CommitStatus{
		RepositoryID: pr.RepositoryID, CommitSHA: pr.HeadSHA, State: "success",
		Context: "CI", IntegrationID: &currentApp, CreatorID: user.ID,
	}); err != nil {
		t.Fatal(err)
	}
	if _, err := svc.MergePR(authCtx, pr.Repository.FullName, pr.Number, "merge", ""); !errors.Is(err, service.ErrInvalidState) {
		t.Fatalf("legacy protection without provider evidence did not fail closed: %v", err)
	}
	migrated, err := svc.GetBranchProtection(authCtx, pr.RepositoryID, pr.BaseRef)
	if err != nil {
		t.Fatal(err)
	}
	if !migrated.RequiredStatusChecksProviderBindingPending {
		t.Fatalf("legacy provider migration was not marked pending: %+v", migrated)
	}

	protectBranch(t, svc, authCtx, pr.RepositoryID, pr.BaseRef, legacy.RequiredStatusChecksJSON, "", true)
	migrated, err = svc.GetBranchProtection(authCtx, pr.RepositoryID, pr.BaseRef)
	if err != nil {
		t.Fatal(err)
	}
	if migrated.RequiredStatusChecksProviderBindingPending {
		t.Fatal("fresh protection write did not clear provider migration state")
	}
	merged, err := svc.MergePR(authCtx, pr.Repository.FullName, pr.Number, "merge", "")
	if err != nil || !merged.Merged {
		t.Fatalf("fresh write did not bind current provider: merged=%v err=%v", merged.Merged, err)
	}
}

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

	pr, authCtx, _ := setupProtectedPR(t, svc, tenantHandle, "provider-roundtrip-user", "provider-roundtrip-repo", false)
	bp := &db.BranchProtection{
		RepositoryID: pr.RepositoryID,
		BranchName:   pr.BaseRef,
		// GitHub responses contain both legacy contexts and provider-aware
		// checks. Re-PUTing that shape must treat checks as canonical instead
		// of adding another automatically selected rule for the same context.
		RequiredStatusChecksJSON: `{"contexts":["CI"],"checks":[{"context":"CI","app_id":42}]}`,
	}
	if err := svc.UpdateBranchProtection(authCtx, bp); err != nil {
		t.Fatal(err)
	}
	stored, err := svc.GetBranchProtection(authCtx, pr.RepositoryID, pr.BaseRef)
	if err != nil {
		t.Fatal(err)
	}
	if strings.Count(stored.ResolvedRequiredStatusChecksJSON, `"context":"CI"`) != 1 ||
		!strings.Contains(stored.ResolvedRequiredStatusChecksJSON, `"app_id":42`) {
		t.Fatalf("GET-to-PUT provider binding = %s", stored.ResolvedRequiredStatusChecksJSON)
	}
}

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

	pr, authCtx, user := setupProtectedPR(t, svc, tenantHandle, "auto-workflow-user", "auto-workflow-repo", true)
	protectBranch(t, svc, tenantHandle.Context(), pr.RepositoryID, pr.BaseRef, `{"contexts":["CI"]}`, "", true)

	if _, err := svc.SetPRAutoMerge(authCtx, pr.ID, service.SetPRAutoMergeInput{
		Enabled:         true,
		MergeMethod:     "SQUASH",
		CommitHeadline:  "Queued merge",
		CommitBody:      "Merged by auto-merge",
		ExpectedHeadSHA: pr.HeadSHA,
		AuthorEmail:     "agent@example.com",
	}); err != nil {
		t.Fatalf("SetPRAutoMerge: %v", err)
	}

	wf := db.Workflow{RepositoryID: pr.RepositoryID, Name: "CI", Path: ".github/workflows/ci.yml", State: db.WorkflowActive}
	if err := tenantHandle.DB().Create(&wf).Error; err != nil {
		t.Fatalf("create workflow: %v", err)
	}
	run := db.WorkflowRun{
		RepositoryID: pr.RepositoryID,
		WorkflowID:   wf.ID,
		ActorID:      &user.ID,
		Name:         "CI",
		HeadBranch:   pr.HeadRef,
		HeadSHA:      pr.HeadSHA,
		Status:       db.RunQueued,
		Event:        "pull_request",
	}
	if err := tenantHandle.DB().Create(&run).Error; err != nil {
		t.Fatalf("create workflow run: %v", err)
	}

	svc.CompleteRunForTest(authCtx, run.ID, db.ConclusionSuccess)

	stored, err := svc.GetPRByID(authCtx, pr.ID)
	if err != nil {
		t.Fatalf("GetPRByID: %v", err)
	}
	if !stored.Merged {
		t.Fatal("expected PR to auto-merge after successful workflow")
	}
	if stored.AutoMerge {
		t.Fatal("expected auto-merge request to be cleared after merge")
	}
	if stored.MergedByLogin != user.Login {
		t.Fatalf("merged_by_login: got %q, want %q", stored.MergedByLogin, user.Login)
	}
}

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

	pr, authCtx, user := setupProtectedPR(t, svc, tenantHandle, "auto-review-user", "auto-review-repo", true)
	protectBranch(t, svc, tenantHandle.Context(), pr.RepositoryID, pr.BaseRef, `{"contexts":["CI"]}`, `{"required_approving_review_count":1}`, true)

	if _, err := svc.SetPRAutoMerge(authCtx, pr.ID, service.SetPRAutoMergeInput{
		Enabled:         true,
		MergeMethod:     "MERGE",
		ExpectedHeadSHA: pr.HeadSHA,
	}); err != nil {
		t.Fatalf("SetPRAutoMerge: %v", err)
	}

	status := &db.CommitStatus{
		RepositoryID: pr.RepositoryID,
		CommitSHA:    pr.HeadSHA,
		State:        "success",
		Context:      "CI",
		CreatorID:    user.ID,
	}
	if err := svc.CreateCommitStatus(authCtx, status); err != nil {
		t.Fatalf("CreateCommitStatus: %v", err)
	}

	stored, err := svc.GetPRByID(authCtx, pr.ID)
	if err != nil {
		t.Fatalf("GetPRByID: %v", err)
	}
	if stored.Merged {
		t.Fatal("expected PR to remain open without bypass or approval")
	}
	if !stored.AutoMerge {
		t.Fatal("expected auto-merge request to remain queued")
	}
}

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

	pr, authCtx, user := setupProtectedPR(t, svc, tenantHandle, "auto-race-user", "auto-race-repo", true)
	protectBranch(t, svc, tenantHandle.Context(), pr.RepositoryID, pr.BaseRef, `{"contexts":["ci"]}`, "", true)

	if _, err := svc.SetPRAutoMerge(authCtx, pr.ID, service.SetPRAutoMergeInput{
		Enabled:         true,
		MergeMethod:     "MERGE",
		ExpectedHeadSHA: pr.HeadSHA,
	}); err != nil {
		t.Fatalf("SetPRAutoMerge: %v", err)
	}

	if _, err := tenantHandle.Git().WriteFile(authCtx, "auto-race-user/auto-race-repo", "feature", "second.txt", "second commit", []byte("second change\n")); err != nil {
		t.Fatalf("WriteFile: %v", err)
	}
	if err := svc.PushHeadSHA(authCtx, "auto-race-user/auto-race-repo", pr.Number); err != nil {
		t.Fatalf("PushHeadSHA: %v", err)
	}

	updated, err := svc.GetPRByID(authCtx, pr.ID)
	if err != nil {
		t.Fatalf("GetPRByID after push: %v", err)
	}
	status := &db.CommitStatus{
		RepositoryID: pr.RepositoryID,
		CommitSHA:    updated.HeadSHA,
		State:        "success",
		Context:      "ci",
		CreatorID:    user.ID,
	}
	if err := svc.CreateCommitStatus(authCtx, status); err != nil {
		t.Fatalf("CreateCommitStatus: %v", err)
	}

	stored, err := svc.GetPRByID(authCtx, pr.ID)
	if err != nil {
		t.Fatalf("GetPRByID final: %v", err)
	}
	if stored.Merged {
		t.Fatal("expected PR to remain open when expected head sha no longer matches")
	}
	if !stored.AutoMerge {
		t.Fatal("expected auto-merge request to remain queued after head mismatch")
	}
}
