package service_test

import (
	"context"
	"fmt"
	"testing"
	"time"

	"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/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"
)

type contextTestTenantProvider map[tenant.ID]*service.TenantResources

func (p contextTestTenantProvider) ForTenant(id tenant.ID) (*service.TenantResources, error) {
	resources, ok := p[id]
	if !ok {
		return nil, fmt.Errorf("unknown tenant %q", id)
	}
	return resources, nil
}

func mustNewContextService(t *testing.T, provider service.TenantResourcesProvider) *service.Service {
	t.Helper()
	svc, err := service.NewService(t.Context(), provider)
	if err != nil {
		t.Fatalf("service.NewService: %v", err)
	}
	return svc
}

func openTenantTestDB(t *testing.T, prefix string) *gorm.DB {
	t.Helper()
	gdb, cleanup := testdb.OpenRaw(t, prefix)
	t.Cleanup(cleanup)
	return gdb
}

func contextTestResources(t *testing.T, id tenant.ID, database *gorm.DB) *service.TenantResources {
	t.Helper()
	store, err := gitstore.New(t.TempDir())
	if err != nil {
		t.Fatalf("gitstore.New: %v", err)
	}
	blobStore := wikicatalog.NewBlobStore(t.TempDir())
	secretBox, err := crypto.NewSecretBox("")
	if err != nil {
		t.Fatalf("crypto.NewSecretBox: %v", err)
	}
	info := tenant.Info{
		ID:               id,
		CanonicalBaseURL: "https://" + string(id) + ".example.test",
	}
	return &service.TenantResources{
		Info:        info,
		DB:          database,
		Git:         store,
		WikiCatalog: wikicatalog.New(database, blobStore),
		WikiBlob:    blobStore,
		SecretBox:   secretBox,
		Embedder:    embedding.NopEmbedder{},
	}
}

func TestWikiBackgroundGitIngestStateIsTenantScoped_Issue1448(t *testing.T) {
	databaseA := openMigratedServiceTestDB(t)
	databaseB := openMigratedServiceTestDB(t)
	resourcesA := contextTestResources(t, "tenant-a", databaseA)
	resourcesB := contextTestResources(t, "tenant-b", databaseB)
	svc := mustNewContextService(t, contextTestTenantProvider{
		resourcesA.Info.ID: resourcesA,
		resourcesB.Info.ID: resourcesB,
	})

	ctxA := tenant.WithInfo(context.Background(), resourcesA.Info)
	ctxB := tenant.WithInfo(context.Background(), resourcesB.Info)
	for _, tc := range []struct {
		gdb  *gorm.DB
		user string
	}{
		{gdb: databaseA, user: "shared-owner"},
		{gdb: databaseB, user: "shared-owner"},
	} {
		owner := db.User{Login: tc.user, Name: tc.user, Type: db.TypeUser}
		if err := tc.gdb.Create(&owner).Error; err != nil {
			t.Fatalf("create %s owner: %v", tc.user, err)
		}
		repoFullName := owner.Login + "/shared-name"
		if err := tc.gdb.Create(&db.Repository{
			OwnerID:       owner.ID,
			Name:          "shared-name",
			FullName:      repoFullName,
			DefaultBranch: "main",
			HasWiki:       true,
		}).Error; err != nil {
			t.Fatalf("create %s repository: %v", tc.user, err)
		}
	}

	repo := "shared-owner/shared-name"
	if !svc.ClaimWikiBackgroundGitIngestForTest(ctxA, repo) {
		t.Fatal("expected to claim tenant-a git ingest slot")
	}
	defer svc.ReleaseWikiBackgroundGitIngestForTest(ctxA, repo)

	runningA, err := svc.IsWikiBackgroundGitIngestRunning(ctxA, repo)
	if err != nil {
		t.Fatalf("tenant-a ingest state: %v", err)
	}
	if !runningA {
		t.Fatal("expected tenant-a git ingest state to be visible in tenant-a context")
	}
	runningB, err := svc.IsWikiBackgroundGitIngestRunning(ctxB, repo)
	if err != nil {
		t.Fatalf("tenant-b ingest state: %v", err)
	}
	if runningB {
		t.Fatal("tenant-a git ingest state leaked into tenant-b context")
	}
}

func TestDetachContextResolvesTenantDatabase(t *testing.T) {
	tenantDB := openTenantTestDB(t, "tenant_bg_tenant")
	tenantID := tenant.ID("tenant-background")
	resources := contextTestResources(t, tenantID, tenantDB)

	svc := mustNewContextService(t, contextTestTenantProvider{tenantID: resources})

	requestCtx := tenant.WithInfo(context.Background(), resources.Info)
	bgCtx, err := svc.DetachContext(requestCtx)
	if err != nil {
		t.Fatalf("DetachContext: %v", err)
	}

	bgDB := svc.DBForCtx(bgCtx)
	bgSQL, _ := bgDB.DB()
	tenantSQL, _ := tenantDB.DB()

	if bgSQL != tenantSQL {
		t.Error("Background goroutine did not use the tenant DB")
	}
}

func TestKickBackgroundWikiGitIngest_UsesCallerTenantContext_Issue1448(t *testing.T) {
	unrelatedDB := openMigratedServiceTestDB(t)
	tenantDB := openMigratedServiceTestDB(t)
	tenantID := tenant.ID("tenant-background")
	tenantInfo := tenant.Info{
		ID:               tenantID,
		CanonicalBaseURL: "https://tenant-background.example.test",
	}

	store, err := gitstore.New(t.TempDir())
	if err != nil {
		t.Fatalf("gitstore.New: %v", err)
	}
	blobStore := wikicatalog.NewBlobStore(t.TempDir())
	wikiCat := wikicatalog.New(tenantDB, blobStore)
	secretBox, err := crypto.NewSecretBox("")
	if err != nil {
		t.Fatalf("crypto.NewSecretBox: %v", err)
	}
	resources := &service.TenantResources{
		Info:        tenantInfo,
		DB:          tenantDB,
		Git:         store,
		WikiCatalog: wikiCat,
		WikiBlob:    blobStore,
		SecretBox:   secretBox,
		Embedder:    embedding.NopEmbedder{},
	}
	svc := mustNewContextService(t, contextTestTenantProvider{tenantID: resources})
	wikiCat.DBFor = func(ctx context.Context) *gorm.DB {
		return svc.DBForTenantResources(ctx, resources)
	}
	wikiCat.OnChangeSetCommitted = svc.WikiCatalogPostCommit

	tenantCtx := tenant.WithInfo(context.Background(), tenantInfo)
	owner := db.User{Login: "tenant-owner", Name: "tenant-owner", Type: db.TypeUser}
	if err := tenantDB.Create(&owner).Error; err != nil {
		t.Fatalf("create tenant owner: %v", err)
	}
	if _, err := svc.CreateRepo(tenantCtx, service.CreateRepoInput{OwnerLogin: owner.Login, Name: "tenant-wiki", AutoInit: true}); err != nil {
		t.Fatalf("CreateRepo: %v", err)
	}
	repoFullName := owner.Login + "/tenant-wiki"
	if err := tenantDB.Model(&db.Repository{}).Where("full_name = ?", repoFullName).Update("has_wiki", true).Error; err != nil {
		t.Fatalf("set has_wiki: %v", err)
	}
	if _, err := svc.PutWikiPage(tenantCtx, repoFullName, "home", "v1", "create home", ""); err != nil {
		t.Fatalf("PutWikiPage home: %v", err)
	}
	if _, err := store.WriteFile(tenantCtx, repoFullName+".wiki", "master", "about.md", "add about", []byte("about body")); err != nil {
		t.Fatalf("git write about: %v", err)
	}

	started := make(chan struct{}, 1)
	svc.SetWikiBackgroundGitIngestStartedHookForTest(func(fullName string) {
		if fullName == repoFullName {
			started <- struct{}{}
		}
	})
	defer svc.SetWikiBackgroundGitIngestStartedHookForTest(nil)

	if err := svc.KickBackgroundWikiGitIngest(tenantCtx, repoFullName); err != nil {
		t.Fatalf("KickBackgroundWikiGitIngest: %v", err)
	}

	select {
	case <-started:
	case <-time.After(3 * time.Second):
		t.Fatal("timed out waiting for background git ingest to start")
	}
	svc.Wg.Wait()

	pages, err := svc.ListWikiPages(tenantCtx, repoFullName, service.ListWikiPagesOptions{Recursive: true})
	if err != nil {
		t.Fatalf("ListWikiPages after background git ingest: %v", err)
	}
	if len(pages) != 2 {
		t.Fatalf("pages = %+v, want 2 pages after background git ingest", pages)
	}
	var unrelatedPages int64
	if err := unrelatedDB.Model(&db.WikiPage{}).Count(&unrelatedPages).Error; err != nil {
		t.Fatalf("count unrelated database wiki pages: %v", err)
	}
	if unrelatedPages != 0 {
		t.Fatalf("unrelated database received %d wiki pages from tenant background ingest", unrelatedPages)
	}
}
