package testharness

import (
	"context"
	"fmt"
	"os"
	"sync"
	"sync/atomic"
	"testing"

	"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"
	"gorm.io/gorm"
	gormlogger "gorm.io/gorm/logger"
)

var serviceSchemaTemplate struct {
	once sync.Once
	name string
	pool *testdb.SchemaPool
	err  error
}

var serviceTenantSeq atomic.Uint64

// ServiceConfig tunes the bare-service fixture produced by NewService. A zero
// value yields an isolated migrated TiDB database, an isolated gitstore, and the
// NopEmbedder.
type ServiceConfig struct {
	// CanonicalBaseURL fixes the tenant's public origin before the registry is
	// published. The zero value uses the stable service-test origin.
	CanonicalBaseURL string

	// ConsoleBaseURL fixes the tenant's browser-console origin independently
	// from the API origin. The zero value keeps the single-origin fallback.
	ConsoleBaseURL string

	// MaxOpenConns optionally overrides the default TiDB test connection pool
	// limit for tests that need stricter concurrency behavior.
	MaxOpenConns int

	// Embedder overrides the default NopEmbedder. Most tests want the default;
	// only tests exercising the embedding code path set this.
	Embedder embedding.Embedder

	// Workflow fixes tenant-owned execution policy before the resource
	// registry is published.
	Workflow service.WorkflowPolicy

	// AllowAnyToken fixes the tenant-owned token compatibility policy before the
	// resource registry is published.
	AllowAnyToken bool

	// OIDC and ConnectedLogin install tenant-owned authentication providers
	// before the resource registry is published.
	OIDC           service.OIDCProvider
	ConnectedLogin service.ConnectedLoginProvider

	// DBLogger configures the fixture database session before it enters the
	// tenant resource registry.
	DBLogger gormlogger.Interface
}

type serviceTenantProvider struct {
	resources *service.TenantResources
}

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

// TenantHandle is the explicit tenant capability returned with a service
// fixture. It carries the exact registry-owned policy and resources for that
// fixture; no package-level or service-level default tenant exists.
type TenantHandle struct {
	parent    context.Context
	info      tenant.Info
	resources *service.TenantResources
}

func (h *TenantHandle) Context() context.Context {
	if h == nil {
		panic("testharness: nil tenant handle")
	}
	return tenant.WithInfo(h.parent, h.info)
}

// Bind adds the fixture's exact immutable tenant policy to parent.
func (h *TenantHandle) Bind(parent context.Context) context.Context {
	if h == nil {
		panic("testharness: nil tenant handle")
	}
	if parent == nil {
		panic("testharness: nil parent context")
	}
	return tenant.WithInfo(parent, h.info)
}

func (h *TenantHandle) Info() tenant.Info {
	if h == nil {
		panic("testharness: nil tenant handle")
	}
	return h.info.Clone()
}

func (h *TenantHandle) DB() *gorm.DB {
	if h == nil || h.resources == nil || h.resources.DB == nil {
		panic("testharness: incomplete tenant database capability")
	}
	return h.resources.DB.WithContext(h.Context())
}

func (h *TenantHandle) Git() *gitstore.Store {
	if h == nil || h.resources == nil || h.resources.Git == nil {
		panic("testharness: incomplete tenant git capability")
	}
	return h.resources.Git
}

func (h *TenantHandle) WikiCatalog() *wikicatalog.Catalog {
	if h == nil || h.resources == nil || h.resources.WikiCatalog == nil {
		panic("testharness: incomplete tenant wiki catalog capability")
	}
	return h.resources.WikiCatalog
}

func (h *TenantHandle) WikiBlob() *wikicatalog.BlobStore {
	if h == nil || h.resources == nil || h.resources.WikiBlob == nil {
		panic("testharness: incomplete tenant wiki blob capability")
	}
	return h.resources.WikiBlob
}

// ContextWithUser binds a test user to the tenant already present on ctx.
func ContextWithUser(tb testing.TB, ctx context.Context, user db.User) context.Context {
	tb.Helper()
	bound, err := service.ContextWithUserForTest(ctx, user)
	if err != nil {
		tb.Fatalf("testharness: bind user to tenant: %v", err)
	}
	return bound
}

// NewService builds a bare *service.Service wired to an isolated TiDB database
// and an isolated gitstore under a temp directory. The
// returned cleanup func resets the database and removes the temp directory.
// A package-level template is migrated through db.Migrate once, then test
// databases are pooled and data-reset between tests.
//
// Callers that need the full HTTP surface should keep using testharness.New;
// this fixture is for service-layer tests that never touch the router.
func NewService(tb testing.TB, cfg ServiceConfig) (*service.Service, *TenantHandle, func()) {
	tb.Helper()
	canonicalBaseURL := cfg.CanonicalBaseURL
	if canonicalBaseURL == "" {
		canonicalBaseURL = "http://localhost:8080"
	}
	info := tenant.Info{
		ID:                    tenant.ID(fmt.Sprintf("service-test-%d", serviceTenantSeq.Add(1))),
		CanonicalBaseURL:      canonicalBaseURL,
		ConsoleBaseURL:        cfg.ConsoleBaseURL,
		OIDCEnabled:           cfg.OIDC != nil,
		ConnectedLoginEnabled: cfg.ConnectedLogin != nil,
	}
	parentCtx, cancelService := context.WithCancel(tb.Context())
	tenantCtx := tenant.WithInfo(parentCtx, info)

	tmpDir, err := os.MkdirTemp("", "gh-service-test-")
	if err != nil {
		tb.Fatalf("testharness: mkdir temp: %v", err)
	}

	gdb, dbCleanup := serviceTemplatePool(tb).Open(tb)
	if cfg.DBLogger != nil {
		gdb = gdb.Session(&gorm.Session{Logger: cfg.DBLogger})
	}
	if err := db.ClaimTenant(gdb.WithContext(tenantCtx), info.ID); err != nil {
		tb.Fatalf("testharness: claim tenant database: %v", err)
	}
	if err := db.EnforceTenantProvenance(gdb, info); err != nil {
		tb.Fatalf("testharness: enforce tenant provenance: %v", err)
	}
	sqlDB, err := gdb.DB()
	if err != nil {
		tb.Fatalf("testharness: get underlying sql.DB: %v", err)
	}
	if cfg.MaxOpenConns > 0 {
		sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
		sqlDB.SetMaxIdleConns(cfg.MaxOpenConns)
	}

	store, err := gitstore.NewForTenant(tmpDir, info.ID)
	if err != nil {
		tb.Fatalf("testharness: gitstore.New: %v", err)
	}

	embedder := cfg.Embedder
	if embedder == nil {
		embedder = embedding.NopEmbedder{}
	}

	wikiBlob := wikicatalog.NewBlobStore(tmpDir)
	wikiCat := wikicatalog.New(gdb, wikiBlob)
	secretBox, err := crypto.NewSecretBox("")
	if err != nil {
		tb.Fatalf("testharness: secret box: %v", err)
	}
	resources := &service.TenantResources{
		Info:           info,
		DB:             gdb,
		Git:            store,
		WikiCatalog:    wikiCat,
		WikiBlob:       wikiBlob,
		SecretBox:      secretBox,
		Embedder:       embedder,
		Workflow:       cfg.Workflow,
		AllowAnyToken:  cfg.AllowAnyToken,
		OIDC:           cfg.OIDC,
		ConnectedLogin: cfg.ConnectedLogin,
	}
	var svc *service.Service
	wikiCat.DBFor = func(ctx context.Context) *gorm.DB {
		return svc.DBForTenantResources(ctx, resources)
	}
	wikiCat.OnChangeSetCommitted = func(ctx context.Context, repoID uint, result wikicatalog.ChangeSetResult) error {
		return svc.WikiCatalogPostCommit(ctx, repoID, result)
	}
	if err := resources.Validate(); err != nil {
		tb.Fatalf("testharness: validate tenant resources: %v", err)
	}
	svc, err = service.NewService(parentCtx, serviceTenantProvider{resources: resources})
	if err != nil {
		tb.Fatalf("testharness: service: %v", err)
	}
	handle := &TenantHandle{
		parent:    parentCtx,
		info:      info,
		resources: resources,
	}
	cleanup := func() {
		cancelService()
		svc.Wg.Wait()
		dbCleanup()
		_ = os.RemoveAll(tmpDir)
	}
	return svc, handle, cleanup
}

// OpenServiceDB returns an isolated migrated TiDB database from the same schema
// pool used by NewService. It is for tests that need a tenant DB or bare DB
// without another gitstore-backed Service fixture.
func OpenServiceDB(tb testing.TB) (*gorm.DB, func()) {
	tb.Helper()
	return serviceTemplatePool(tb).Open(tb)
}

func serviceTemplatePool(tb testing.TB) *testdb.SchemaPool {
	tb.Helper()
	serviceSchemaTemplate.once.Do(func() {
		gdb, cleanup := testdb.OpenRaw(tb, "service_template")
		_ = cleanup
		if err := gdb.Raw("SELECT DATABASE()").Scan(&serviceSchemaTemplate.name).Error; err != nil {
			serviceSchemaTemplate.err = err
			return
		}
		if err := db.Migrate(gdb); err != nil {
			serviceSchemaTemplate.err = err
			return
		}
		db.InitVector(gdb, 3)
		serviceSchemaTemplate.pool = &testdb.SchemaPool{
			TemplateDB: serviceSchemaTemplate.name,
			Prefix:     "service",
		}
		if sqlDB, err := gdb.DB(); err == nil {
			_ = sqlDB.Close()
		}
	})
	if serviceSchemaTemplate.err != nil {
		tb.Fatalf("testharness: prepare service schema template: %v", serviceSchemaTemplate.err)
	}
	if serviceSchemaTemplate.name == "" {
		tb.Fatalf("testharness: service schema template has no database name")
	}
	if serviceSchemaTemplate.pool == nil {
		tb.Fatalf("testharness: service schema pool was not initialized")
	}
	return serviceSchemaTemplate.pool
}
