package graphql

import (
	"context"
	"fmt"
	"os"
	"testing"

	"github.com/prometheus/client_golang/prometheus/testutil"
	"gorm.io/gorm"

	"github.com/pingcap/agent-git-service/internal/crypto"
	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/embedding"
	"github.com/pingcap/agent-git-service/internal/gitstore"
	"github.com/pingcap/agent-git-service/internal/metrics"
	"github.com/pingcap/agent-git-service/internal/service"
	"github.com/pingcap/agent-git-service/internal/tenant"
	"github.com/pingcap/agent-git-service/internal/testharness/testdb"
	"github.com/pingcap/agent-git-service/internal/wikicatalog"
)

const gqlMetricsTenantID tenant.ID = "graphql-metrics"

func gqlMetricsInfo() tenant.Info {
	return tenant.Info{
		ID:               gqlMetricsTenantID,
		CanonicalBaseURL: "https://graphql-metrics.example.test",
	}
}

func gqlMetricsContext() context.Context {
	return tenant.WithInfo(context.Background(), gqlMetricsInfo())
}

type gqlMetricsTenantProvider struct {
	resources *service.TenantResources
}

func (p gqlMetricsTenantProvider) ForTenant(id tenant.ID) (*service.TenantResources, error) {
	if p.resources == nil || id != p.resources.Info.ID {
		return nil, fmt.Errorf("unknown GraphQL metrics tenant %q", id)
	}
	return p.resources, nil
}

func gqlSearchTotal() float64 {
	return testutil.ToFloat64(metrics.DefaultPrometheus().SearchTotal.WithLabelValues(string(gqlMetricsTenantID)))
}

func TestRunSearch_DoesNotRecordMetricOnIssueSearchFailure(t *testing.T) {
	server, _, cleanup := newSearchMetricsTestServer(t)
	defer cleanup()

	ctx, cancel := context.WithCancel(gqlMetricsContext())
	cancel()

	before := gqlSearchTotal()
	conn := server.runSearch(ctx, "repo:tester/missing type:issue", "")
	after := gqlSearchTotal()

	if after != before {
		t.Fatalf("SearchTotal changed on failed issue search: before=%v after=%v", before, after)
	}
	if got := conn["issueCount"]; got != 0 {
		t.Fatalf("issueCount = %v, want 0", got)
	}
}

func TestRunSearch_DoesNotRecordMetricOnPRSearchFailure(t *testing.T) {
	server, _, cleanup := newSearchMetricsTestServer(t)
	defer cleanup()

	ctx, cancel := context.WithCancel(gqlMetricsContext())
	cancel()

	before := gqlSearchTotal()
	conn := server.runSearch(ctx, "repo:tester/missing is:pr", "")
	after := gqlSearchTotal()

	if after != before {
		t.Fatalf("SearchTotal changed on failed PR search: before=%v after=%v", before, after)
	}
	if got := conn["issueCount"]; got != 0 {
		t.Fatalf("issueCount = %v, want 0", got)
	}
}

func TestRunRepoSearch_DoesNotRecordMetricOnFailure(t *testing.T) {
	server, _, cleanup := newSearchMetricsTestServer(t)
	defer cleanup()

	ctx, cancel := context.WithCancel(gqlMetricsContext())
	cancel()

	before := gqlSearchTotal()
	conn := server.runRepoSearch(ctx, "user:tester", "")
	after := gqlSearchTotal()

	if after != before {
		t.Fatalf("SearchTotal changed on failed repo search: before=%v after=%v", before, after)
	}
	if got := conn["repositoryCount"]; got != 0 {
		t.Fatalf("repositoryCount = %v, want 0", got)
	}
}

func TestDoSearch_AliasedSearchRecordsMetricOncePerRequest(t *testing.T) {
	server, database, cleanup := newSearchMetricsTestServer(t)
	defer cleanup()

	ctx := gqlMetricsContext()
	user := db.User{Login: "tester", Name: "tester", Type: db.TypeUser}
	if err := database.Create(&user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}
	if _, err := server.Svc.CreateRepo(ctx, service.CreateRepoInput{
		OwnerLogin: user.Login,
		Name:       "aliased-search-metrics",
		AutoInit:   true,
	}); err != nil {
		t.Fatalf("create repo: %v", err)
	}
	if _, err := server.Svc.CreateIssue(ctx, service.CreateIssueInput{
		RepoFullName: "tester/aliased-search-metrics",
		Title:        "Aliased search issue",
		Body:         "body",
		AuthorLogin:  user.Login,
	}); err != nil {
		t.Fatalf("create issue: %v", err)
	}

	before := gqlSearchTotal()
	res := server.doSearch(ctx, gqlRequest{
		Query: `
			query($viewerQuery: String!, $repoQuery: String!) {
				viewerCreated: search(query: $viewerQuery, type: ISSUE, first: 10) { issueCount }
				repoSearch: search(query: $repoQuery, type: ISSUE, first: 10) { issueCount }
			}`,
		Variables: map[string]any{
			"viewerQuery": "repo:tester/aliased-search-metrics author:tester",
			"repoQuery":   "repo:tester/aliased-search-metrics",
		},
	})
	after := gqlSearchTotal()

	if after != before+1 {
		t.Fatalf("SearchTotal changed by %v, want exactly 1 successful request", after-before)
	}
	data := res["data"].(map[string]any)
	if got := data["viewerCreated"].(map[string]any)["issueCount"]; got != 1 {
		t.Fatalf("viewerCreated issueCount = %v, want 1", got)
	}
	if got := data["repoSearch"].(map[string]any)["issueCount"]; got != 1 {
		t.Fatalf("repoSearch issueCount = %v, want 1", got)
	}
}

func newSearchMetricsTestServer(t *testing.T) (*Server, *gorm.DB, func()) {
	t.Helper()

	tmpDir, err := os.MkdirTemp("", "graphql-search-metrics-")
	if err != nil {
		t.Fatalf("mkdir temp: %v", err)
	}

	gdb, dbCleanup := testdb.OpenRaw(t, "gql_metrics")
	if err := db.Migrate(gdb); err != nil {
		t.Fatalf("migrate: %v", err)
	}

	store, err := gitstore.NewForTenant(tmpDir, gqlMetricsTenantID)
	if err != nil {
		t.Fatalf("gitstore.New: %v", err)
	}

	metrics.Init()
	info := gqlMetricsInfo()
	blobStore := wikicatalog.NewBlobStore(tmpDir)
	secretBox, err := crypto.NewSecretBox("")
	if err != nil {
		t.Fatalf("crypto.NewSecretBox: %v", err)
	}
	resources := &service.TenantResources{
		Info:        info,
		DB:          gdb,
		Git:         store,
		WikiCatalog: wikicatalog.New(gdb, blobStore),
		WikiBlob:    blobStore,
		SecretBox:   secretBox,
		Embedder:    embedding.NopEmbedder{},
	}
	if err := resources.Validate(); err != nil {
		t.Fatalf("validate tenant resources: %v", err)
	}
	svc, err := service.NewService(t.Context(), gqlMetricsTenantProvider{resources: resources})
	if err != nil {
		t.Fatalf("service.NewService: %v", err)
	}

	cleanup := func() {
		dbCleanup()
		_ = os.RemoveAll(tmpDir)
	}
	return NewServer(svc), gdb.WithContext(gqlMetricsContext()), cleanup
}
