package graphql_test

import (
	"errors"
	"fmt"
	"strings"
	"sync/atomic"
	"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"
	"gorm.io/gorm"
)

func TestGraphQLMergeQueueMutationsReturnFullQueueReference(t *testing.T) {
	svc, mux, owner, tenantHandle, cleanup := setupTestEnvironment(t)
	defer cleanup()
	ctx := tenantHandle.Context()

	repo, err := svc.CreateRepo(ctx, service.CreateRepoInput{
		OwnerLogin: owner.Login, Name: "queue-mutation-repo", DefaultBranch: "main", AutoInit: true,
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := tenantHandle.Git().CreateBranch(ctx, repo.FullName, "feature", "main"); err != nil {
		t.Fatal(err)
	}
	if _, err := tenantHandle.Git().WriteFile(ctx, repo.FullName, "feature", "feature.txt", "feature", []byte("feature\n")); err != nil {
		t.Fatal(err)
	}
	authCtx := testharness.ContextWithUser(t, ctx, owner)
	pr, err := svc.CreatePR(authCtx, service.CreatePRInput{
		RepoFullName: repo.FullName, Title: "queued", HeadRef: "feature", BaseRef: "main", AuthorLogin: owner.Login,
	})
	if err != nil {
		t.Fatal(err)
	}
	rules := `[{"type":"merge_queue","parameters":{"check_response_timeout_minutes":60,"grouping_strategy":"ALLGREEN","merge_method":"SQUASH","max_entries_to_build":0,"max_entries_to_merge":1,"min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":0}}]`
	if err := svc.CreateRuleset(authCtx, repo.FullName, &db.Ruleset{
		Name: "merge queue", Target: "branch", Enforcement: "active",
		ConditionsJSON: `{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":[]}}`, RulesJSON: rules,
	}); err != nil {
		t.Fatal(err)
	}

	selection := `{
      clientMutationId
      mergeQueueEntry {
        id state
        mergeQueue {
          id url resourcePath nextEntryEstimatedTimeToMerge
          repository { nameWithOwner }
          entries { totalCount }
          configuration {
            checkResponseTimeout maximumEntriesToBuild maximumEntriesToMerge
            mergeMethod mergingStrategy minimumEntriesToMerge minimumEntriesToMergeWaitTime
          }
        }
      }
    }`
	prID := fmt.Sprintf("PullRequest_%d", pr.ID)
	enqueued := doGql(t, mux, ctx, "mutation($input: EnqueuePullRequestInput!) { enqueuePullRequest(input: $input) "+selection+" }", map[string]any{
		"input": map[string]any{"pullRequestId": prID, "expectedHeadOid": pr.HeadSHA, "clientMutationId": "enqueue"},
	})["enqueuePullRequest"].(map[string]any)
	assertFullMergeQueueMutationPayload(t, enqueued, "enqueue")

	dequeued := doGql(t, mux, ctx, "mutation($input: DequeuePullRequestInput!) { dequeuePullRequest(input: $input) "+selection+" }", map[string]any{
		"input": map[string]any{"id": prID, "clientMutationId": "dequeue"},
	})["dequeuePullRequest"].(map[string]any)
	assertFullMergeQueueMutationPayload(t, dequeued, "dequeue")
}

func TestGraphQLStackUsesUltimateBaseMergeQueue(t *testing.T) {
	svc, mux, owner, tenantHandle, cleanup := setupTestEnvironment(t)
	defer cleanup()
	ctx := tenantHandle.Context()

	repo, err := svc.CreateRepo(ctx, service.CreateRepoInput{
		OwnerLogin: owner.Login, Name: "stack-queue-target", DefaultBranch: "main", AutoInit: true,
	})
	if err != nil {
		t.Fatal(err)
	}
	authCtx := testharness.ContextWithUser(t, ctx, owner)
	prs := make([]db.PullRequest, 0, 3)
	baseRef := repo.DefaultBranch
	for i := 1; i <= 3; i++ {
		headRef := fmt.Sprintf("stack-%d", i)
		if err := tenantHandle.Git().CreateBranch(authCtx, repo.FullName, headRef, baseRef); err != nil {
			t.Fatal(err)
		}
		if _, err := tenantHandle.Git().WriteFile(authCtx, repo.FullName, headRef, fmt.Sprintf("member-%d.txt", i), "member", []byte("member\n")); err != nil {
			t.Fatal(err)
		}
		pr, err := svc.CreatePR(authCtx, service.CreatePRInput{
			RepoFullName: repo.FullName, Title: fmt.Sprintf("member %d", i), HeadRef: headRef, BaseRef: baseRef, AuthorLogin: owner.Login,
		})
		if err != nil {
			t.Fatal(err)
		}
		prs = append(prs, pr)
		baseRef = headRef
	}
	numbers := []int{prs[0].Number, prs[1].Number, prs[2].Number}
	if _, err := svc.CreatePullRequestStack(authCtx, repo.FullName, numbers); err != nil {
		t.Fatal(err)
	}
	rules := `[{
      "type":"merge_queue",
      "parameters":{
        "check_response_timeout_minutes":60,
        "grouping_strategy":"ALLGREEN",
        "merge_method":"SQUASH",
        "max_entries_to_build":0,
        "max_entries_to_merge":1,
        "min_entries_to_merge":1,
        "min_entries_to_merge_wait_minutes":0
      }
    }]`
	if err := svc.CreateRuleset(authCtx, repo.FullName, &db.Ruleset{
		Name: "merge queue", Target: "branch", Enforcement: "active",
		ConditionsJSON: `{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":[]}}`, RulesJSON: rules,
	}); err != nil {
		t.Fatal(err)
	}
	var stackTargetQueries atomic.Int64
	callbackName := "test:count_stack_merge_queue_target_queries"
	if err := tenantHandle.DB().Callback().Query().After("gorm:query").Register(callbackName, func(tx *gorm.DB) {
		if strings.Contains(tx.Statement.SQL.String(), "pull_request_stack_entries") {
			stackTargetQueries.Add(1)
		}
	}); err != nil {
		t.Fatal(err)
	}
	defer tenantHandle.DB().Callback().Query().Remove(callbackName)

	enabledOnlyQuery := `query($owner: String!, $repo: String!, $number: Int!) {
		repository(owner: $owner, name: $repo) {
			pullRequest(number: $number) { isMergeQueueEnabled }
		}
	}`
	variables := map[string]any{"owner": owner.Login, "repo": repo.Name, "number": prs[2].Number}
	enabledOnly := doGql(t, mux, ctx, enabledOnlyQuery, variables)["repository"].(map[string]any)["pullRequest"].(map[string]any)
	if enabledOnly["isMergeQueueEnabled"] != true {
		t.Fatalf("enabled-only isMergeQueueEnabled = %#v, want true", enabledOnly["isMergeQueueEnabled"])
	}
	var queueCount int64
	if err := tenantHandle.DB().Model(&db.MergeQueue{}).Where("repository_id = ?", repo.ID).Count(&queueCount).Error; err != nil {
		t.Fatal(err)
	}
	if queueCount != 0 {
		t.Fatalf("enabled-only GraphQL query created %d Queue rows, want none", queueCount)
	}
	if got := stackTargetQueries.Load(); got != 1 {
		t.Fatalf("enabled-only Stack Queue target queries = %d, want one compact lookup", got)
	}
	stackTargetQueries.Store(0)

	query := `query($owner: String!, $repo: String!, $number: Int!) {
		repository(owner: $owner, name: $repo) {
			pullRequest(number: $number) {
				isMergeQueueEnabled
				isInMergeQueue
				mergeQueue { resourcePath }
			}
		}
	}`
	data := doGql(t, mux, ctx, query, variables)
	pullRequest := data["repository"].(map[string]any)["pullRequest"].(map[string]any)
	if pullRequest["isMergeQueueEnabled"] != true {
		t.Fatalf("isMergeQueueEnabled = %#v, want true for Stack base %q", pullRequest["isMergeQueueEnabled"], repo.DefaultBranch)
	}
	if pullRequest["isInMergeQueue"] != false {
		t.Fatalf("isInMergeQueue = %#v, want false before enqueue", pullRequest["isInMergeQueue"])
	}
	queue, ok := pullRequest["mergeQueue"].(map[string]any)
	if !ok {
		t.Fatalf("mergeQueue = %#v, want Stack final-base Queue", pullRequest["mergeQueue"])
	}
	wantResourcePath := fmt.Sprintf("/%s/queue/%s", repo.FullName, repo.DefaultBranch)
	if queue["resourcePath"] != wantResourcePath {
		t.Fatalf("mergeQueue.resourcePath = %#v, want %q", queue["resourcePath"], wantResourcePath)
	}
	if got := stackTargetQueries.Load(); got != 1 {
		t.Fatalf("Stack Queue target queries = %d, want one compact lookup", got)
	}
}

func TestGraphQLPullRequestTimelinePropagatesBackendReadFailures(t *testing.T) {
	svc, mux, owner, tenantHandle, cleanup := setupTestEnvironment(t)
	defer cleanup()
	ctx := tenantHandle.Context()
	repo, err := svc.CreateRepo(ctx, service.CreateRepoInput{
		OwnerLogin: owner.Login, Name: "timeline-errors-repo", DefaultBranch: "main", AutoInit: true,
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := tenantHandle.Git().CreateBranch(ctx, repo.FullName, "feature", "main"); err != nil {
		t.Fatal(err)
	}
	if _, err := tenantHandle.Git().WriteFile(ctx, repo.FullName, "feature", "feature.txt", "feature", []byte("feature\n")); err != nil {
		t.Fatal(err)
	}
	pr, err := svc.CreatePR(testharness.ContextWithUser(t, ctx, owner), service.CreatePRInput{
		RepoFullName: repo.FullName, Title: "timeline errors", HeadRef: "feature", BaseRef: "main", AuthorLogin: owner.Login,
	})
	if err != nil {
		t.Fatal(err)
	}
	query := `query($owner: String!, $repo: String!, $number: Int!) {
		repository(owner: $owner, name: $repo) { pullRequest(number: $number) {
			timelineItems { totalCount nodes { __typename } }
		} }
	}`
	variables := map[string]any{"owner": owner.Login, "repo": repo.Name, "number": pr.Number}
	for _, table := range []string{"pull_request_reviews", "issue_references", "pull_request_merge_queue_events"} {
		t.Run(table, func(t *testing.T) {
			callbackName := "timeline_failure_" + table
			if err := tenantHandle.DB().Callback().Query().Before("gorm:query").Register(callbackName, func(tx *gorm.DB) {
				if tx.Statement.Table == table {
					tx.AddError(errors.New("forced timeline read failure"))
				}
			}); err != nil {
				t.Fatal(err)
			}
			defer tenantHandle.DB().Callback().Query().Remove(callbackName)

			response := doRawGql(t, mux, query, variables)
			errs, ok := response["errors"].([]any)
			if !ok || len(errs) == 0 {
				t.Fatalf("response hid %s failure: %#v", table, response)
			}
			first := errs[0].(map[string]any)
			path := first["path"].([]any)
			if len(path) != 3 || path[0] != "repository" || path[1] != "pullRequest" || path[2] != "timelineItems" {
				t.Fatalf("GraphQL error path = %#v", path)
			}
			data := response["data"].(map[string]any)
			pullRequest := data["repository"].(map[string]any)["pullRequest"].(map[string]any)
			if value, exists := pullRequest["timelineItems"]; !exists || value != nil {
				t.Fatalf("timelineItems = %#v, want null", value)
			}
		})
	}
}

func assertFullMergeQueueMutationPayload(t *testing.T, payload map[string]any, mutationID string) {
	t.Helper()
	if payload["clientMutationId"] != mutationID {
		t.Fatalf("clientMutationId = %v, want %q", payload["clientMutationId"], mutationID)
	}
	entry, ok := payload["mergeQueueEntry"].(map[string]any)
	if !ok {
		t.Fatalf("mergeQueueEntry = %T", payload["mergeQueueEntry"])
	}
	queue, ok := entry["mergeQueue"].(map[string]any)
	if !ok {
		t.Fatalf("mergeQueue = %T", entry["mergeQueue"])
	}
	for _, field := range []string{"id", "url", "resourcePath", "repository", "entries", "configuration"} {
		if _, ok := queue[field]; !ok {
			t.Errorf("mergeQueue.%s is missing", field)
		}
	}
	configuration, ok := queue["configuration"].(map[string]any)
	if !ok || len(configuration) != 7 {
		t.Fatalf("mergeQueue.configuration = %#v, want all seven GitHub fields", queue["configuration"])
	}
}

func TestGraphQLPullRequestTimelineCombinesExistingItemsAndMergeQueueEventsWithPagination(t *testing.T) {
	svc, mux, owner, tenantHandle, cleanup := setupTestEnvironment(t)
	defer cleanup()
	ctx := tenantHandle.Context()

	repo, err := svc.CreateRepo(ctx, service.CreateRepoInput{
		OwnerLogin: owner.Login, Name: "queue-timeline-repo", DefaultBranch: "main", AutoInit: true,
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := tenantHandle.Git().CreateBranch(ctx, repo.FullName, "feature", "main"); err != nil {
		t.Fatal(err)
	}
	if _, err := tenantHandle.Git().WriteFile(ctx, repo.FullName, "feature", "feature.txt", "feature", []byte("feature\n")); err != nil {
		t.Fatal(err)
	}
	authCtx := testharness.ContextWithUser(t, ctx, owner)
	pr, err := svc.CreatePR(authCtx, service.CreatePRInput{
		RepoFullName: repo.FullName, Title: "queued", HeadRef: "feature", BaseRef: "main", AuthorLogin: owner.Login,
	})
	if err != nil {
		t.Fatal(err)
	}
	if _, err := svc.CreateIssueComment(authCtx, repo.FullName, pr.Number, "before queue", owner.Login, nil); err != nil {
		t.Fatal(err)
	}
	rules := `[{"type":"merge_queue","parameters":{"check_response_timeout_minutes":60,"grouping_strategy":"ALLGREEN","merge_method":"SQUASH","max_entries_to_build":0,"max_entries_to_merge":1,"min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":0}}]`
	if err := svc.CreateRuleset(authCtx, repo.FullName, &db.Ruleset{
		Name: "merge queue", Target: "branch", Enforcement: "active",
		ConditionsJSON: `{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":[]}}`, RulesJSON: rules,
	}); err != nil {
		t.Fatal(err)
	}
	if _, err := svc.EnqueuePullRequest(authCtx, pr.ID, pr.HeadSHA, false); err != nil {
		t.Fatal(err)
	}

	query := `query($owner: String!, $repo: String!, $number: Int!, $first: Int!, $after: String) {
		repository(owner: $owner, name: $repo) { pullRequest(number: $number) {
			timelineItems(first: $first, after: $after, itemTypes: [ISSUE_COMMENT, ADDED_TO_MERGE_QUEUE_EVENT]) {
				totalCount filteredCount pageCount
				pageInfo { hasNextPage hasPreviousPage endCursor }
				nodes { __typename ... on IssueComment { body } ... on AddedToMergeQueueEvent { id } }
			}
		} }
	}`
	variables := map[string]any{"owner": owner.Login, "repo": repo.Name, "number": pr.Number, "first": 1}
	first := doGql(t, mux, ctx, query, variables)["repository"].(map[string]any)["pullRequest"].(map[string]any)["timelineItems"].(map[string]any)
	if first["totalCount"] != float64(2) || first["filteredCount"] != float64(2) || first["pageCount"] != float64(1) {
		t.Fatalf("first timeline connection = %#v", first)
	}
	firstNodes := first["nodes"].([]any)
	if len(firstNodes) != 1 || firstNodes[0].(map[string]any)["__typename"] != "IssueComment" {
		t.Fatalf("first timeline nodes = %#v", firstNodes)
	}
	pageInfo := first["pageInfo"].(map[string]any)
	if pageInfo["hasNextPage"] != true || pageInfo["hasPreviousPage"] != false {
		t.Fatalf("first timeline pageInfo = %#v", pageInfo)
	}
	variables["after"] = pageInfo["endCursor"]
	second := doGql(t, mux, ctx, query, variables)["repository"].(map[string]any)["pullRequest"].(map[string]any)["timelineItems"].(map[string]any)
	secondNodes := second["nodes"].([]any)
	if len(secondNodes) != 1 || secondNodes[0].(map[string]any)["__typename"] != "AddedToMergeQueueEvent" {
		t.Fatalf("second timeline nodes = %#v", secondNodes)
	}

	commitQuery := `query($owner: String!, $repo: String!, $number: Int!) {
		repository(owner: $owner, name: $repo) { pullRequest(number: $number) {
			timelineItems(itemTypes: [PULL_REQUEST_COMMIT]) {
				totalCount nodes { __typename ... on PullRequestCommit { commit { oid } } }
			}
		} }
	}`
	commitTimeline := doGql(t, mux, ctx, commitQuery, variables)["repository"].(map[string]any)["pullRequest"].(map[string]any)["timelineItems"].(map[string]any)
	commitNodes := commitTimeline["nodes"].([]any)
	if commitTimeline["totalCount"] != float64(1) || len(commitNodes) != 1 || commitNodes[0].(map[string]any)["__typename"] != "PullRequestCommit" {
		t.Fatalf("commit timeline connection = %#v", commitTimeline)
	}
}
