package graphql

import (
	"context"
	"encoding/base64"
	"fmt"
	"testing"

	"github.com/glebarez/sqlite"
	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/service"
	"gorm.io/gorm"
)

func openMutationTargetTestDatabase(t *testing.T) *gorm.DB {
	t.Helper()
	database, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	if err != nil {
		t.Fatal(err)
	}
	sqlDatabase, err := database.DB()
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() {
		if err := sqlDatabase.Close(); err != nil {
			t.Errorf("close test database: %v", err)
		}
	})
	return database
}

func TestResolveMutationRepositoriesUsesExactRepositoryScopedNode(t *testing.T) {
	database := openMutationTargetTestDatabase(t)
	if err := database.AutoMigrate(&db.Repository{}, &db.Issue{}); err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&db.Repository{ID: 7, Name: "native", FullName: "acme/native"}).Error; err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&db.Issue{ID: 9, Number: 1, RepositoryID: 7, Title: "cutoff"}).Error; err != nil {
		t.Fatal(err)
	}
	server := &Server{Svc: &service.Service{DB: database}}

	for _, test := range []struct {
		name string
		body string
	}{
		{
			name: "repository variable",
			body: `{"query":"mutation($input: UpdateRepositoryInput!) { updateRepository(input: $input) { repository { id } } }","variables":{"input":{"repositoryId":"Repository_7"}}}`,
		},
		{
			name: "indirect base64 issue variable",
			body: fmt.Sprintf(`{"query":"mutation($input: CloseIssueInput!) { closeIssue(input: $input) { issue { id } } }","variables":{"input":{"issueId":%q}}}`,
				base64.StdEncoding.EncodeToString([]byte("Issue_9"))),
		},
		{
			name: "inline repository node",
			body: `{"query":"mutation { archiveRepository(input: {repositoryId: \"Repository_7\"}) { repository { id } } }"}`,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			repositories, mutation, err := server.ResolveMutationRepositories(context.Background(), []byte(test.body))
			if err != nil {
				t.Fatal(err)
			}
			if !mutation {
				t.Fatal("mutation was not detected")
			}
			if len(repositories) != 1 || repositories[0] != "acme/native" {
				t.Fatalf("repositories = %v, want [acme/native]", repositories)
			}
		})
	}
}

func TestResolveMutationRepositoriesKeepsQueriesAndLegacyTargetDistinct(t *testing.T) {
	database := openMutationTargetTestDatabase(t)
	if err := database.AutoMigrate(&db.Repository{}); err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&db.Repository{ID: 8, Name: "legacy", FullName: "acme/legacy"}).Error; err != nil {
		t.Fatal(err)
	}
	server := &Server{Svc: &service.Service{DB: database}}

	repositories, mutation, err := server.ResolveMutationRepositories(context.Background(), []byte(
		`{"query":"query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { id } }","variables":{"owner":"acme","name":"native"}}`,
	))
	if err != nil {
		t.Fatal(err)
	}
	if mutation || len(repositories) != 0 {
		t.Fatalf("query resolved mutation=%v repositories=%v", mutation, repositories)
	}

	repositories, mutation, err = server.ResolveMutationRepositories(context.Background(), []byte(
		`{"query":"mutation($input: UpdateRepositoryInput!) { updateRepository(input: $input) { repository { id } } }","variables":{"input":{"repositoryId":"Repository_8"}}}`,
	))
	if err != nil {
		t.Fatal(err)
	}
	if !mutation || len(repositories) != 1 || repositories[0] != "acme/legacy" {
		t.Fatalf("legacy target resolved mutation=%v repositories=%v", mutation, repositories)
	}
}

func TestResolveMutationRepositoriesDoesNotTreatMutationTextAsNodeIdentity(t *testing.T) {
	database := openMutationTargetTestDatabase(t)
	server := &Server{Svc: &service.Service{DB: database}}

	repositories, mutation, err := server.ResolveMutationRepositories(context.Background(), []byte(
		`{"query":"mutation($input: AddCommentInput!) { addComment(input: $input) { commentEdge { node { id } } } }","variables":{"input":{"body":"Repository_7 is documentation, not a node target"}}}`,
	))
	if err != nil {
		t.Fatal(err)
	}
	if !mutation || len(repositories) != 0 {
		t.Fatalf("text payload resolved mutation=%v repositories=%v", mutation, repositories)
	}
}

func TestResolveMutationRepositoriesDoesNotFenceReadOnlyTemplateReference(t *testing.T) {
	database := openMutationTargetTestDatabase(t)
	if err := database.AutoMigrate(&db.Repository{}); err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&db.Repository{ID: 7, Name: "template", FullName: "acme/template"}).Error; err != nil {
		t.Fatal(err)
	}
	server := &Server{Svc: &service.Service{DB: database}}

	repositories, mutation, err := server.ResolveMutationRepositories(context.Background(), []byte(
		`{"query":"mutation($input: CloneTemplateRepositoryInput!) { cloneTemplateRepository(input: $input) { repository { id } } }","variables":{"input":{"repositoryId":"Repository_7","name":"copy"}}}`,
	))
	if err != nil {
		t.Fatal(err)
	}
	if !mutation || len(repositories) != 0 {
		t.Fatalf("template source resolved mutation=%v repositories=%v", mutation, repositories)
	}
}

func TestResolveMutationRepositoriesCloneTemplateCommentDoesNotHideMutationTarget(t *testing.T) {
	database := openMutationTargetTestDatabase(t)
	if err := database.AutoMigrate(&db.Repository{}); err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&db.Repository{ID: 7, Name: "native", FullName: "acme/native"}).Error; err != nil {
		t.Fatal(err)
	}
	server := &Server{Svc: &service.Service{DB: database}}

	repositories, mutation, err := server.ResolveMutationRepositories(context.Background(), []byte(
		`{"query":"mutation($input: CreateBlobInput!) { # cloneTemplateRepository is documentation only\n createBlob(input: $input) { blob { oid } } }","variables":{"input":{"repositoryId":"Repository_7","content":"hello"}}}`,
	))
	if err != nil {
		t.Fatal(err)
	}
	if !mutation || len(repositories) != 1 || repositories[0] != "acme/native" {
		t.Fatalf("comment token resolved mutation=%v repositories=%v", mutation, repositories)
	}
}

func TestResolveMutationRepositoriesUpdatePullRequestBranchUsesHeadRepository(t *testing.T) {
	database := openMutationTargetTestDatabase(t)
	if err := database.AutoMigrate(&db.Repository{}, &db.PullRequest{}); err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&[]db.Repository{
		{ID: 7, Name: "native", FullName: "acme/native"},
		{ID: 8, Name: "legacy", FullName: "acme/legacy"},
	}).Error; err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&db.PullRequest{ID: 9, RepositoryID: 8, HeadRepositoryID: 7}).Error; err != nil {
		t.Fatal(err)
	}
	server := &Server{Svc: &service.Service{DB: database}}

	repositories, mutation, err := server.ResolveMutationRepositories(context.Background(), []byte(
		`{"query":"mutation($input: UpdatePullRequestBranchInput!) { updatePullRequestBranch(input: $input) { pullRequest { id } } }","variables":{"input":{"pullRequestId":"PullRequest_9"}}}`,
	))
	if err != nil {
		t.Fatal(err)
	}
	if !mutation || len(repositories) != 1 || repositories[0] != "acme/native" {
		t.Fatalf("cross-repository branch update resolved mutation=%v repositories=%v", mutation, repositories)
	}
}

func TestResolveMutationRepositoriesIgnoresUnusedVariables(t *testing.T) {
	database := openMutationTargetTestDatabase(t)
	if err := database.AutoMigrate(&db.Repository{}); err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&[]db.Repository{
		{ID: 7, Name: "native", FullName: "acme/native"},
		{ID: 8, Name: "legacy", FullName: "acme/legacy"},
	}).Error; err != nil {
		t.Fatal(err)
	}
	server := &Server{Svc: &service.Service{DB: database}}

	repositories, mutation, err := server.ResolveMutationRepositories(context.Background(), []byte(
		`{"query":"mutation($input: UpdateRepositoryInput!, $unused: UpdateRepositoryInput!) { updateRepository(input: $input) { repository { id } } }","variables":{"input":{"repositoryId":"Repository_8"},"unused":{"repositoryId":"Repository_7"}}}`,
	))
	if err != nil {
		t.Fatal(err)
	}
	if !mutation || len(repositories) != 1 || repositories[0] != "acme/legacy" {
		t.Fatalf("unused variable resolved mutation=%v repositories=%v", mutation, repositories)
	}
}

func TestResolveMutationRepositoriesUsesOnlyExecutedMutationField(t *testing.T) {
	database := openMutationTargetTestDatabase(t)
	if err := database.AutoMigrate(&db.Repository{}); err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&[]db.Repository{
		{ID: 7, Name: "native", FullName: "acme/native"},
		{ID: 8, Name: "legacy", FullName: "acme/legacy"},
	}).Error; err != nil {
		t.Fatal(err)
	}
	server := &Server{Svc: &service.Service{DB: database}}

	repositories, mutation, err := server.ResolveMutationRepositories(context.Background(), []byte(
		`{"query":"mutation($input: CreateBlobInput!, $other: UpdateRepositoryInput!) { createBlob(input: $input) { blob { oid } } updateRepository(input: $other) { repository { id } } }","variables":{"input":{"repositoryId":"Repository_8","content":"hello"},"other":{"repositoryId":"Repository_7"}}}`,
	))
	if err != nil {
		t.Fatal(err)
	}
	if !mutation || len(repositories) != 1 || repositories[0] != "acme/legacy" {
		t.Fatalf("non-executed field resolved mutation=%v repositories=%v", mutation, repositories)
	}
}

func TestResolveMutationRepositoriesUsesOperationNameFallbackInput(t *testing.T) {
	database := openMutationTargetTestDatabase(t)
	if err := database.AutoMigrate(&db.Repository{}, &db.PullRequest{}); err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&db.Repository{ID: 7, Name: "native", FullName: "acme/native"}).Error; err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&db.PullRequest{ID: 9, RepositoryID: 7, HeadRepositoryID: 7}).Error; err != nil {
		t.Fatal(err)
	}
	server := &Server{Svc: &service.Service{DB: database}}
	for _, body := range []string{
		`{"operationName":"CreateBlob","query":"mutation CreateBlob($input: CreateBlobInput!) { differentRootField }","variables":{"input":{"repositoryId":"Repository_7","content":"hello"}}}`,
		`{"operationName":"CreateTree","query":"mutation CreateTree($input: CreateTreeInput!) { differentRootField }","variables":{"input":{"repositoryId":"Repository_7"}}}`,
		`{"operationName":"CreateIssue","query":"mutation CreateIssue($input: CreateIssueInput!) { differentRootField }","variables":{"input":{"repositoryId":"Repository_7","title":"issue"}}}`,
		`{"operationName":"CreatePullRequest","query":"mutation CreatePullRequest { differentRootField }","variables":{"repositoryId":"Repository_7"}}`,
		`{"operationName":"MergePullRequest","query":"mutation MergePullRequest($input: MergePullRequestInput!) { differentRootField }","variables":{"input":{"pullRequestId":"PullRequest_9"}}}`,
	} {
		repositories, mutation, err := server.ResolveMutationRepositories(context.Background(), []byte(body))
		if err != nil {
			t.Fatal(err)
		}
		if !mutation || len(repositories) != 1 || repositories[0] != "acme/native" {
			t.Fatalf("operationName fallback resolved mutation=%v repositories=%v for %s", mutation, repositories, body)
		}
	}
}

func TestResolveMutationRepositoriesMirrorsHandlerInputInsteadOfASTAlias(t *testing.T) {
	database := openMutationTargetTestDatabase(t)
	if err := database.AutoMigrate(&db.Repository{}, &db.PullRequest{}); err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&[]db.Repository{
		{ID: 7, Name: "native", FullName: "acme/native"},
		{ID: 8, Name: "legacy", FullName: "acme/legacy"},
	}).Error; err != nil {
		t.Fatal(err)
	}
	if err := database.Create(&[]db.PullRequest{
		{ID: 9, RepositoryID: 7, HeadRepositoryID: 7},
		{ID: 10, RepositoryID: 8, HeadRepositoryID: 8},
	}).Error; err != nil {
		t.Fatal(err)
	}
	server := &Server{Svc: &service.Service{DB: database}}
	for _, body := range []string{
		`{"operationName":"CreateBlob","query":"mutation CreateBlob($decoy: CreateBlobInput!) { createBlob(input: $decoy) { blob { oid } } }","variables":{"decoy":{"repositoryId":"Repository_8"},"input":{"repositoryId":"Repository_7"}}}`,
		`{"operationName":"CreateTree","query":"mutation CreateTree($decoy: CreateTreeInput!) { createTree(input: $decoy) { tree { oid } } }","variables":{"decoy":{"repositoryId":"Repository_8"},"input":{"repositoryId":"Repository_7"}}}`,
		`{"operationName":"CreateIssue","query":"mutation CreateIssue($decoy: CreateIssueInput!) { createIssue(input: $decoy) { issue { id } } }","variables":{"decoy":{"repositoryId":"Repository_8"},"input":{"repositoryId":"Repository_7"}}}`,
		`{"operationName":"CreatePullRequest","query":"mutation CreatePullRequest($decoy: CreatePullRequestInput!) { createPullRequest(input: $decoy) { pullRequest { id } } }","variables":{"decoy":{"repositoryId":"Repository_8"},"input":{"repositoryId":"Repository_7"}}}`,
		`{"operationName":"MergePullRequest","query":"mutation MergePullRequest($decoy: MergePullRequestInput!) { mergePullRequest(input: $decoy) { pullRequest { id } } }","variables":{"decoy":{"pullRequestId":"PullRequest_10"},"input":{"pullRequestId":"PullRequest_9"}}}`,
	} {
		repositories, mutation, err := server.ResolveMutationRepositories(context.Background(), []byte(body))
		if err != nil {
			t.Fatal(err)
		}
		if !mutation || len(repositories) != 1 || repositories[0] != "acme/native" {
			t.Fatalf("handler input resolved mutation=%v repositories=%v for %s", mutation, repositories, body)
		}
	}
}

func TestResolveMutationRepositoriesFailsClosedOnMissingOperationNameFallbackTarget(t *testing.T) {
	database := openMutationTargetTestDatabase(t)
	server := &Server{Svc: &service.Service{DB: database}}
	body := []byte(`{"operationName":"CreateBlob","query":"mutation CreateBlob { differentRootField }","variables":{"input":{"content":"hello"}}}`)

	_, mutation, err := server.ResolveMutationRepositories(context.Background(), body)
	if !mutation || err == nil {
		t.Fatalf("missing fallback target resolved mutation=%v err=%v, want fail closed", mutation, err)
	}
}

func TestParseSelectedQueryUsesOnlyNamedOperation(t *testing.T) {
	fields, _, _, mutation, err := parseSelectedQuery(`
		mutation Write($input: UpdateRepositoryInput!) {
			updateRepository(input: $input) { repository { id } }
		}
		query Read { viewer { login } }
	`, "Read")
	if err != nil {
		t.Fatal(err)
	}
	if mutation {
		t.Fatal("selected query was classified as a mutation")
	}
	if _, found := fields["updateRepository"]; found {
		t.Fatalf("selected query fields include mutation: %v", fields)
	}
	if _, found := fields["viewer"]; !found {
		t.Fatalf("selected query fields = %v, want viewer", fields)
	}
}
