package cluster

import (
	"context"
	"errors"
	"fmt"
	"math/rand/v2"
	"os"
	"strconv"
	"sync"
	"testing"
	"time"

	"github.com/docker/docker/api/types/container"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/modules/mysql"
	pkgv2db "github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/pkgv2/gormutil"

	"github.com/tidbcloud/tidb-management-service/serverless-global-service/app/regional_proxy"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/app/register"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/adapter"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/repository"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/service/cluster"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/service/region"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/service/sqluser"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/testdata"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/infra/adapterimpl"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/infra/adapterimpl/cli"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/infra/persistence/db"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/infra/persistence/migrate"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/infra/persistence/repo"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/pkg/config"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterproxy"
	"github.com/tidbcloud/tidb-management-service/utilities/featuregate"
	"github.com/tidbcloud/tidb-management-service/utilities/idgenerator"
	"github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

var (
	provider = testdata.GetAwsProvider()
	regionEn = testdata.GetAwsRegionUsWest2()

	awsUsWest1Region = testdata.GetAwsRegionUsWest1()
)

func TestMain(m *testing.M) {
	var (
		database = "local_test"
		password = "password"
		user     = "root"
	)

	ctx := context.Background()

	// Refer to https://golang.testcontainers.org/features/garbage_collector/#ryuk.
	if err := os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true"); err != nil {
		panic(err)
	}
	if err := os.Setenv("DOCKER_API_VERSION", "1.44"); err != nil {
		panic(err)
	}
	mysqlContainer, err := mysql.RunContainer(
		ctx,
		testcontainers.WithImage("gcr.io/pingcap-public/third-party/mysql:8.0"),
		mysql.WithDatabase(database),
		mysql.WithUsername(user),
		mysql.WithPassword(password),
		testcontainers.WithHostConfigModifier(func(hostConfig *container.HostConfig) {
			hostConfig.Tmpfs = map[string]string{"/var/lib/mysql": "rw"} // Use tmpfs to speed up tests.
		}),
	)
	if err != nil {
		panic(err)
	}
	defer func() {
		if err := mysqlContainer.Terminate(ctx); err != nil {
			panic(err)
		}
	}()

	host, _ := mysqlContainer.Host(ctx)
	portS, _ := mysqlContainer.MappedPort(ctx, "3306/tcp")
	port, _ := strconv.ParseUint(portS.Port(), 10, 16)

	config.Global().MysqlConfig = gormutil.Config{
		User:     user,
		Host:     host,
		Database: database,
		Password: password,
		Port:     uint16(port),
	}
	config.Global().RegionalServiceConfig.RegionalProxyServiceConfig.Enabled = true

	initFakeDependencies()
	if err := register.DiCall(migrate.RunDBMigrate); err != nil {
		panic(err)
	}

	if err := register.DiCall(func(providerRepo repository.CloudProviderRepo, regionRepo repository.RegionRepo) error {
		_, err := providerRepo.Save(ctx, provider)
		if err != nil {
			return err
		}

		_, err = regionRepo.Save(ctx, regionEn)
		if err != nil {
			return err
		}

		_, err = regionRepo.Save(ctx, awsUsWest1Region)
		if err != nil {
			return err
		}

		return nil
	}); err != nil {
		panic(err)
	}

	if err := register.DiCall(func(workflowImpl workflow.Workflow) error {
		return workflowImpl.Start()
	}); err != nil {
		panic(err)
	}

	m.Run()
}

var (
	allocatedIds = map[int64]struct{}{}

	lock = sync.Mutex{}
)

func allocateClusterId() string {
	lock.Lock()
	defer lock.Unlock()

	for range 100 {
		id := rand.Int64N(time.Now().Unix())
		if _, ok := allocatedIds[id]; !ok {
			allocatedIds[id] = struct{}{}
			return fmt.Sprintf("%d", id)
		}
	}

	panic(errors.New("allocate cluster id failed"))
}

func initFakeDependencies() {
	register.Provide(db.NewGormDB)
	register.Provide(db.NewDBProvider)
	register.Provide(func() featuregate.Service {
		return featuregate.NewFeatureGate(config.Global().ServerConfig.FeatureGates, nil)
	})
	register.Provide(func(dbProvider pkgv2db.TransProvider) adapter.WALCommitter {
		return clusterevent.NewWALCommitter(dbProvider)
	})
	register.Provide(func(dbProvider pkgv2db.TransProvider, handler clusterevent.Publisher) clusterevent.WALWatcher {
		return clusterevent.NewWALWatcher(dbProvider, handler, "test")
	})
	register.Provide(clusterevent.NewPublisher)
	// Universal unique id
	register.Provide(func() idgenerator.IDGenerator {
		nid := idgenerator.NodeID(0)
		return idgenerator.NewSonyflakeID(nid, "")
	})
	register.Provide(func(provider pkgv2db.TransProvider, idGenerator idgenerator.IDGenerator) workflow.Workflow {
		w, err := workflow.NewWorkflow(
			workflow.ID("serverless-global-test-1"),
			register.GetMysqlDefaultConfig(),
			provider,
			idGenerator,
			workflow.WithWorkerMode(workflow.ModeCompetitive),
		)
		if err != nil {
			panic(err)
		}
		return w
	})

	register.Provide(repo.NewCloudProviderRepo)
	register.Provide(repo.NewClusterRepoImpl)
	register.Provide(repo.NewRegionRepoImpl)

	register.Provide(adapterimpl.NewFakeDataflowServiceImpl)
	register.Provide(adapterimpl.NewFakeServerlessFacade)
	register.Provide(adapterimpl.NewFakeEncryptor)
	register.Provide(adapterimpl.NewFakeBilling)
	register.Provide(adapterimpl.NewFakeSys9Lifecycle)

	register.Provide(regional_proxy.NewService)
	register.Provide(cli.NewFakeRegionalProxyClientImpl)
	register.Provide(adapterimpl.NewRegionalServiceProxy)
	register.Provide(adapterimpl.NewFakeRegionService)

	register.Provide(clusterproxy.NewFakeClusterProxyProvider)
	register.Provide(sqluser.NewSqlUserService)
	register.Provide(adapterimpl.NewFakeAccountImpl)

	register.Provide(cluster.NewClusterService)
	register.Provide(region.NewRegionServiceImpl)

	register.Provide(NewService)
}
