package rest

import (
	"net/http/httptest"
	"reflect"
	"sort"
	"testing"

	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/rest/transform"
	"github.com/pingcap/agent-git-service/internal/service"
	"github.com/pingcap/agent-git-service/internal/tenant"
)

func TestStackMembershipJSONExactFields(t *testing.T) {
	got := stackMembershipJSON(service.PullRequestStackMembership{
		ID: 7, Number: 3, Size: 4, Position: 2, BaseRef: "main", BaseSHA: "abc",
	})
	assertMapKeys(t, got, "base", "id", "number", "position", "size")
	assertMapKeys(t, got["base"].(map[string]any), "ref", "sha")
}

func TestListStackPullRequestJSONExactFields(t *testing.T) {
	got := minimalStackPullRequest(db.PullRequest{
		Number: 12, State: db.StateOpen, Draft: true, HeadRef: "feature", HeadSHA: "abc",
	})
	assertMapKeys(t, got, "draft", "head", "merged_at", "number", "state")
	assertMapKeys(t, got["head"].(map[string]any), "ref", "sha")
}

func TestStackPullRequestJSONExactFields(t *testing.T) {
	got := simpleStackPullRequest(transform.New("https://api.example.test"), db.PullRequest{
		ID: 8, Number: 12, Title: "stacked", State: db.StateOpen, Draft: true,
		HeadRef: "feature", HeadSHA: "head", BaseRef: "main", BaseSHA: "base",
		Repository: db.Repository{ID: 3, Name: "repo", FullName: "octo/repo"},
		Author:     db.User{ID: 4, Login: "octo", Type: db.TypeUser},
	})
	assertMapKeys(t, got, "base", "draft", "head", "html_url", "id", "merged_at", "node_id", "number", "state", "title", "url", "user")
	assertMapKeys(t, got["head"].(map[string]any), "ref", "repo", "sha")
	assertMapKeys(t, got["base"].(map[string]any), "ref", "repo", "sha")
	assertMapKeys(t, got["head"].(map[string]any)["repo"].(map[string]any), "id", "name", "url")
	assertMapKeys(t, got["user"].(map[string]any),
		"avatar_url", "events_url", "followers_url", "following_url", "gists_url", "gravatar_id",
		"html_url", "id", "login", "node_id", "organizations_url", "received_events_url", "repos_url",
		"site_admin", "starred_url", "subscriptions_url", "type", "url", "user_view_type")
}

func TestStackResourceJSONExactFields(t *testing.T) {
	deps := &Deps{}
	req := httptest.NewRequest("GET", "/", nil)
	req = req.WithContext(tenant.WithInfo(req.Context(), tenant.Info{
		ID:               "stack-contract",
		CanonicalBaseURL: "https://api.example.test",
	}))
	got := deps.pullRequestStackJSON(req, service.PullRequestStackView{
		PullRequestStack: db.PullRequestStack{ID: 9, Number: 2, BaseRef: "main", Repository: db.Repository{FullName: "octo/repo"}},
		Entries: []db.PullRequestStackEntry{{PullRequest: db.PullRequest{
			ID: 8, Number: 12, Title: "stacked", State: db.StateOpen,
			Repository: db.Repository{ID: 3, Name: "repo", FullName: "octo/repo"},
			Author:     db.User{ID: 4, Login: "octo", Type: db.TypeUser},
		}}},
	})
	assertMapKeys(t, got, "base", "created_at", "id", "node_id", "number", "open", "pull_requests", "url")
	assertMapKeys(t, got["base"].(map[string]any), "ref")
}

func TestAsyncMergeJSONExactPublicShapes(t *testing.T) {
	pending := asyncMergePendingJSON(db.AsyncMergeRequest{
		UUID: "request-id", Status: service.AsyncMergePending, MergeMethod: "squash",
		MergeAction: service.AsyncMergeActionDefault, ExpectedHeadSHA: "abc",
	}, "Merge request enqueued.")
	assertMapKeys(t, pending, "details", "status")
	if pending["status"] != service.AsyncMergePending {
		t.Fatalf("status = %v, want pending", pending["status"])
	}
	assertMapKeys(t, pending["details"].(map[string]any),
		"expected_head_sha", "merge_action", "merge_method", "message", "uuid")

	enqueued := asyncMergeResultJSON(db.AsyncMergeRequest{Status: service.AsyncMergeEnqueued})
	if enqueued["status"] != service.AsyncMergeEnqueued {
		t.Fatalf("status = %v, want enqueued", enqueued["status"])
	}
	assertMapKeys(t, enqueued["details"].(map[string]any), "message")

	merged := asyncMergeResultJSON(db.AsyncMergeRequest{Status: service.AsyncMergeMerged, ResultSHA: "def"})
	assertMapKeys(t, merged["details"].(map[string]any), "message", "sha")

	failed := asyncMergeResultJSON(db.AsyncMergeRequest{Status: service.AsyncMergeFailed, FailureMessage: "blocked"})
	assertMapKeys(t, failed["details"].(map[string]any), "message")
}

func TestStackAndAsyncMergeOpenAPISchemasArePublished(t *testing.T) {
	schemas := githubStackOpenAPISchemas()
	if got := schemas["PullRequestStack"]; got == nil {
		t.Fatal("PullRequestStack schema is missing")
	}
	stack := schemas["PullRequestStack"].(map[string]any)
	assertMapKeys(t, stack["properties"].(map[string]any), "base", "created_at", "id", "node_id", "number", "open", "pull_requests", "url")
	items := stack["properties"].(map[string]any)["pull_requests"].(map[string]any)["items"].(map[string]any)
	assertMapKeys(t, items["properties"].(map[string]any), "base", "draft", "head", "html_url", "id", "merged_at", "node_id", "number", "state", "title", "url", "user")
	if got := schemas["PullRequestAsyncMergeResult"].(map[string]any)["oneOf"].([]any); len(got) != 4 {
		t.Fatalf("async merge variants = %d, want 4", len(got))
	}

	paths := buildGitHubCompatibleOpenAPIPaths()
	for _, path := range []string{
		"/api/v3/repos/{owner}/{repo}/stacks",
		"/api/v3/repos/{owner}/{repo}/stacks/{stack_number}",
		"/api/v3/repos/{owner}/{repo}/stacks/{stack_number}/add",
		"/api/v3/repos/{owner}/{repo}/stacks/{stack_number}/unstack",
		"/api/v3/repos/{owner}/{repo}/pulls/{pull_number}/merge-async",
		"/api/v3/repos/{owner}/{repo}/pulls/{pull_number}/merge-async/{uuid}",
	} {
		if _, ok := paths[path]; !ok {
			t.Errorf("GitHub-compatible OpenAPI path %q is missing", path)
		}
	}
}

func TestImportMappingsOpenAPIHasCursorPagination(t *testing.T) {
	paths := buildRESTOpenAPIPaths()
	path, ok := paths["/api/ext/v1/imports/{import_id}/mapping"].(map[string]any)
	if !ok {
		t.Fatal("import mapping OpenAPI path is missing")
	}
	operation, ok := path["get"].(map[string]any)
	if !ok {
		t.Fatal("import mapping GET operation is missing")
	}
	parameters, ok := operation["parameters"].([]map[string]any)
	if !ok {
		t.Fatalf("mapping parameters type = %T", operation["parameters"])
	}
	for _, parameter := range parameters {
		if parameter["name"] == "after_id" && parameter["in"] == "query" {
			return
		}
	}
	t.Fatal("mapping cursor parameter after_id is missing")
}

func assertMapKeys(t *testing.T, value map[string]any, want ...string) {
	t.Helper()
	got := make([]string, 0, len(value))
	for key := range value {
		got = append(got, key)
	}
	sort.Strings(got)
	sort.Strings(want)
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("keys = %v, want %v", got, want)
	}
}
