package rest_test

import (
	"fmt"
	"math"
	"net/http"
	"strconv"
	"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 TestStackRESTAndAsyncMergeGitHubSurface(t *testing.T) {
	h := testharness.New(t)
	compatSeedRepo(t, h, "stack-surface")
	ctx := h.Tenant.Context()
	fullName := "testuser/stack-surface"

	if err := h.Tenant.Git().CreateBranch(ctx, fullName, "stack-one", "main"); err != nil {
		t.Fatal(err)
	}
	if _, err := h.Tenant.Git().WriteFile(ctx, fullName, "stack-one", "one.txt", "one", []byte("one\n")); err != nil {
		t.Fatal(err)
	}
	if err := h.Tenant.Git().CreateBranch(ctx, fullName, "stack-two", "stack-one"); err != nil {
		t.Fatal(err)
	}
	if _, err := h.Tenant.Git().WriteFile(ctx, fullName, "stack-two", "two.txt", "two", []byte("two\n")); err != nil {
		t.Fatal(err)
	}
	authCtx := testharness.ContextWithUser(t, ctx, h.User)
	first, err := h.Svc.CreatePR(authCtx, service.CreatePRInput{
		RepoFullName: fullName, Title: "one", HeadRef: "stack-one", BaseRef: "main", AuthorLogin: h.User.Login,
	})
	if err != nil {
		t.Fatal(err)
	}
	second, err := h.Svc.CreatePR(authCtx, service.CreatePRInput{
		RepoFullName: fullName, Title: "two", HeadRef: "stack-two", BaseRef: "stack-one", AuthorLogin: h.User.Login,
	})
	if err != nil {
		t.Fatal(err)
	}

	created := h.DoRESTJSON(t, http.MethodPost, "/api/v3/repos/"+fullName+"/stacks", map[string]any{
		"pull_requests": []int{first.Number, second.Number},
	})
	assertStatusCode(t, created, http.StatusCreated)
	stack := testharness.DecodeJSON(t, created)
	if stack["number"] != float64(1) || len(stack["pull_requests"].([]any)) != 2 {
		t.Fatalf("created Stack = %#v", stack)
	}

	listed := h.DoREST(t, http.MethodGet, "/api/v3/repos/"+fullName+"/stacks?pull_request=2", nil)
	assertStatusCode(t, listed, http.StatusOK)
	if stacks := testharness.DecodeJSONArray(t, listed); len(stacks) != 1 {
		t.Fatalf("listed Stacks = %d, want 1", len(stacks))
	}
	prResponse := h.DoREST(t, http.MethodGet, fmt.Sprintf("/api/v3/repos/%s/pulls/%d", fullName, second.Number), nil)
	assertStatusCode(t, prResponse, http.StatusOK)
	if membership := testharness.DecodeJSON(t, prResponse)["stack"]; membership == nil {
		t.Fatal("full pull request response omitted Stack membership")
	}

	unstacked := h.DoRESTJSON(t, http.MethodPost, "/api/v3/repos/"+fullName+"/stacks/1/unstack", nil)
	assertStatusCode(t, unstacked, http.StatusNoContent)

	accepted := h.DoRESTJSON(t, http.MethodPut, fmt.Sprintf("/api/v3/repos/%s/pulls/%d/merge-async", fullName, first.Number), map[string]any{
		"merge_method": "squash", "merge_action": "direct_merge", "sha": first.HeadSHA,
	})
	assertStatusCode(t, accepted, http.StatusAccepted)
	result := testharness.DecodeJSON(t, accepted)
	details := result["details"].(map[string]any)
	requestID, _ := details["uuid"].(string)
	if result["status"] != "pending" || requestID == "" {
		t.Fatalf("accepted async merge = %#v", result)
	}

	deadline := time.Now().Add(10 * time.Second)
	for time.Now().Before(deadline) {
		polled := h.DoREST(t, http.MethodGet, fmt.Sprintf("/api/v3/repos/%s/pulls/%d/merge-async/%s", fullName, first.Number, requestID), nil)
		assertStatusCode(t, polled, http.StatusOK)
		result = testharness.DecodeJSON(t, polled)
		if result["status"] != "pending" {
			break
		}
		time.Sleep(20 * time.Millisecond)
	}
	if result["status"] != "merged" {
		t.Fatalf("async merge result = %#v", result)
	}
}

func TestListPullRequestStacksPagination(t *testing.T) {
	h := testharness.New(t)
	compatSeedRepo(t, h, "stack-pagination")
	fullName := "testuser/stack-pagination"
	repo, err := h.Svc.GetRepo(h.Tenant.Context(), fullName)
	if err != nil {
		t.Fatal(err)
	}

	const stackCount = 101
	prs := make([]db.PullRequest, stackCount*2)
	for i := range prs {
		prs[i] = db.PullRequest{
			Number:           i + 1,
			RepositoryID:     repo.ID,
			HeadRepositoryID: repo.ID,
			Title:            fmt.Sprintf("stack member %d", i+1),
			State:            db.StateOpen,
			AuthorID:         h.User.ID,
			HeadRef:          fmt.Sprintf("stack-member-%d", i+1),
			BaseRef:          "main",
		}
	}
	if err := h.DB.Create(&prs).Error; err != nil {
		t.Fatal(err)
	}

	stacks := make([]db.PullRequestStack, stackCount)
	for i := range stacks {
		stacks[i] = db.PullRequestStack{RepositoryID: repo.ID, Number: i + 1, BaseRef: "main"}
	}
	if err := h.DB.Create(&stacks).Error; err != nil {
		t.Fatal(err)
	}
	entries := make([]db.PullRequestStackEntry, 0, stackCount*2)
	for i := range stacks {
		entries = append(entries,
			db.PullRequestStackEntry{StackID: stacks[i].ID, PullRequestID: prs[i*2].ID, Position: 1},
			db.PullRequestStackEntry{StackID: stacks[i].ID, PullRequestID: prs[i*2+1].ID, Position: 2},
		)
	}
	if err := h.DB.Create(&entries).Error; err != nil {
		t.Fatal(err)
	}

	firstPage := h.DoREST(t, http.MethodGet, "/api/v3/repos/"+fullName+"/stacks?per_page=100", nil)
	assertStatusCode(t, firstPage, http.StatusOK)
	firstPageStacks := testharness.DecodeJSONArray(t, firstPage)
	if len(firstPageStacks) != 100 {
		t.Fatalf("first page Stack count = %d, want 100", len(firstPageStacks))
	}
	if got := firstPageStacks[0]["number"]; got != float64(101) {
		t.Fatalf("first page first Stack number = %v, want 101", got)
	}
	if got := firstPageStacks[99]["number"]; got != float64(2) {
		t.Fatalf("first page last Stack number = %v, want 2", got)
	}
	stackListURL := h.BaseURL + "/api/v3/repos/" + fullName + "/stacks"
	wantFirstLink := fmt.Sprintf(`<%s?page=2&per_page=100>; rel="next", <%s?page=2&per_page=100>; rel="last"`, stackListURL, stackListURL)
	if got := firstPage.Header().Get("Link"); got != wantFirstLink {
		t.Fatalf("first page Link header = %q, want %q", got, wantFirstLink)
	}

	secondPage := h.DoREST(t, http.MethodGet, "/api/v3/repos/"+fullName+"/stacks?page=2&per_page=100", nil)
	assertStatusCode(t, secondPage, http.StatusOK)
	secondPageStacks := testharness.DecodeJSONArray(t, secondPage)
	if len(secondPageStacks) != 1 {
		t.Fatalf("second page Stack count = %d, want 1", len(secondPageStacks))
	}
	if got := secondPageStacks[0]["number"]; got != float64(1) {
		t.Fatalf("second page Stack number = %v, want 1", got)
	}
	wantSecondLink := fmt.Sprintf(`<%s?page=1&per_page=100>; rel="prev", <%s?page=1&per_page=100>; rel="first"`, stackListURL, stackListURL)
	if got := secondPage.Header().Get("Link"); got != wantSecondLink {
		t.Fatalf("second page Link header = %q, want %q", got, wantSecondLink)
	}

	outOfRange := h.DoREST(t, http.MethodGet, "/api/v3/repos/"+fullName+"/stacks?page=3&per_page=100", nil)
	assertStatusCode(t, outOfRange, http.StatusOK)
	if stacks := testharness.DecodeJSONArray(t, outOfRange); len(stacks) != 0 {
		t.Fatalf("out-of-range Stack count = %d, want 0", len(stacks))
	}
	wantOutOfRangeLink := fmt.Sprintf(`<%s?page=2&per_page=100>; rel="prev", <%s?page=2&per_page=100>; rel="last", <%s?page=1&per_page=100>; rel="first"`, stackListURL, stackListURL, stackListURL)
	if got := outOfRange.Header().Get("Link"); got != wantOutOfRangeLink {
		t.Fatalf("out-of-range Link header = %q, want %q", got, wantOutOfRangeLink)
	}

	overflowPage := h.DoREST(t, http.MethodGet, "/api/v3/repos/"+fullName+"/stacks?page="+strconv.Itoa(math.MaxInt)+"&per_page=100", nil)
	assertStatusCode(t, overflowPage, http.StatusOK)
	if stacks := testharness.DecodeJSONArray(t, overflowPage); len(stacks) != 0 {
		t.Fatalf("overflow-page Stack count = %d, want 0", len(stacks))
	}
	if got := overflowPage.Header().Get("Link"); got != wantOutOfRangeLink {
		t.Fatalf("overflow-page Link header = %q, want %q", got, wantOutOfRangeLink)
	}

	filtered := h.DoREST(t, http.MethodGet, "/api/v3/repos/"+fullName+"/stacks?pull_request=1&per_page=1", nil)
	assertStatusCode(t, filtered, http.StatusOK)
	filteredStacks := testharness.DecodeJSONArray(t, filtered)
	if len(filteredStacks) != 1 || filteredStacks[0]["number"] != float64(1) {
		t.Fatalf("filtered Stacks = %#v, want only Stack 1", filteredStacks)
	}
	if link := filtered.Header().Get("Link"); link != "" {
		t.Fatalf("filtered Link header = %q, want none for one matching Stack", link)
	}
}
