package config

import (
	"context"

	ccproto "github.com/tidbcloud/config-center/proto/config_center"
	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/test"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/repo"

	gk "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"go.uber.org/mock/gomock"
)

var _ = gk.Describe("ClusterConfigurationChangeableForChange", func() {
	var fw = test.Framework()
	var ctx context.Context

	gk.BeforeEach(func() {
		ctx = log.NewContext(context.Background(), fw.Logger)
	})

	gk.DescribeTable("evaluates coarse cluster changeability for UI entrypoints",
		func(prevChanges []*ccent.ConfigurationChange, wantBlocked bool) {
			comps := Components{
				Repo: repo.NewMockCloudConfigRepo(fw.Ctrl),
			}
			comps.Repo.(*repo.MockCloudConfigRepo).EXPECT().
				ListConfigurationChanges(gomock.Any(), gomock.Any(), gomock.Any()).
				Return(prevChanges, nil)

			reason, err := ClusterConfigurationChangeable(ctx, comps, ccent.ClusterConfigurationLocator{
				ClusterType: ccent.ClusterTypeLogicalCluster,
				ClusterID:   "123",
			})
			Expect(err).NotTo(HaveOccurred())
			if wantBlocked {
				Expect(reason).To(Equal("some cluster configuration changes are already in progress"))
			} else {
				Expect(reason).To(BeEmpty())
			}
		},
		gk.Entry("allows editing while only TiProxy-only changes are in progress",
			[]*ccent.ConfigurationChange{
				clusterChange(ccent.ClusterTypeLogicalCluster, []string{ccproto.LOGICAL_CLUSTER_CONFIG_TIPROXY}, nil, false),
			},
			false,
		),
		gk.Entry("allows editing while only non-TiProxy changes are in progress",
			[]*ccent.ConfigurationChange{
				clusterChange(ccent.ClusterTypeSharedPool, nil, []string{ccproto.SHARED_POOL_COMPONENT_TIKV}, false),
			},
			false,
		),
		gk.Entry("blocks when TiProxy-only and non-TiProxy changes are both in progress",
			[]*ccent.ConfigurationChange{
				clusterChange(ccent.ClusterTypeLogicalCluster, []string{ccproto.LOGICAL_CLUSTER_CONFIG_TIPROXY}, nil, false),
				clusterChange(ccent.ClusterTypeLogicalCluster, []string{ccproto.LOGICAL_CLUSTER_CONFIG_TIDB}, nil, false),
			},
			true,
		),
		gk.Entry("keeps Dedicated globally blocked",
			[]*ccent.ConfigurationChange{
				clusterChange(ccent.ClusterTypeDedicatedTiDB, []string{ccproto.DEDICATED_TIDB_CONFIG_TIPROXY}, nil, false),
			},
			true,
		),
		gk.Entry("keeps mixed scope globally blocked",
			[]*ccent.ConfigurationChange{
				clusterChange(ccent.ClusterTypeLogicalCluster, []string{ccproto.LOGICAL_CLUSTER_CONFIG_TIPROXY, ccproto.LOGICAL_CLUSTER_CONFIG_TIDB}, nil, false),
			},
			true,
		),
	)

	gk.DescribeTable("evaluates TiProxy-only conflicts conservatively",
		func(prevChange, newChange *ccent.ConfigurationChange, wantBlocked bool) {
			comps := Components{
				Repo: repo.NewMockCloudConfigRepo(fw.Ctrl),
			}
			comps.Repo.(*repo.MockCloudConfigRepo).EXPECT().
				ListConfigurationChanges(gomock.Any(), gomock.Any(), gomock.Any()).
				Return([]*ccent.ConfigurationChange{prevChange}, nil)

			reason, err := ClusterConfigurationChangeableForChange(ctx, comps, newChange)
			Expect(err).NotTo(HaveOccurred())
			if wantBlocked {
				Expect(reason).To(Equal("some cluster configuration changes are already in progress"))
			} else {
				Expect(reason).To(BeEmpty())
			}
		},
		gk.Entry("allows logical TiProxy-only beside logical non-TiProxy file change",
			clusterChange(ccent.ClusterTypeLogicalCluster, []string{ccproto.LOGICAL_CLUSTER_CONFIG_TIPROXY}, nil, false),
			clusterChange(ccent.ClusterTypeLogicalCluster, []string{ccproto.LOGICAL_CLUSTER_CONFIG_TIDB}, nil, false),
			false,
		),
		gk.Entry("allows shared-pool non-TiProxy beside shared-pool TiProxy-only component change",
			clusterChange(ccent.ClusterTypeSharedPool, nil, []string{ccproto.SHARED_POOL_COMPONENT_TIKV}, false),
			clusterChange(ccent.ClusterTypeSharedPool, nil, []string{ccproto.SHARED_POOL_COMPONENT_TIPROXY}, false),
			false,
		),
		gk.Entry("blocks two TiProxy-only changes",
			clusterChange(ccent.ClusterTypeLogicalCluster, []string{ccproto.LOGICAL_CLUSTER_CONFIG_TIPROXY}, nil, false),
			clusterChange(ccent.ClusterTypeLogicalCluster, nil, []string{ccproto.LOGICAL_CLUSTER_COMPONENT_TIPROXY}, false),
			true,
		),
		gk.Entry("blocks two non-TiProxy changes",
			clusterChange(ccent.ClusterTypeSharedPool, []string{ccproto.SHARED_POOL_CONFIG_TIKV}, nil, false),
			clusterChange(ccent.ClusterTypeSharedPool, nil, []string{ccproto.SHARED_POOL_COMPONENT_PD}, false),
			true,
		),
		gk.Entry("keeps Dedicated globally blocked",
			clusterChange(ccent.ClusterTypeDedicatedTiDB, []string{ccproto.DEDICATED_TIDB_CONFIG_TIPROXY}, nil, false),
			clusterChange(ccent.ClusterTypeDedicatedTiDB, []string{ccproto.DEDICATED_TIDB_CONFIG_TIDB}, nil, false),
			true,
		),
		gk.Entry("blocks mixed scope",
			clusterChange(ccent.ClusterTypeLogicalCluster, []string{ccproto.LOGICAL_CLUSTER_CONFIG_TIPROXY, ccproto.LOGICAL_CLUSTER_CONFIG_TIDB}, nil, false),
			clusterChange(ccent.ClusterTypeLogicalCluster, nil, []string{ccproto.LOGICAL_CLUSTER_COMPONENT_TIPROXY}, false),
			true,
		),
		gk.Entry("blocks version change",
			clusterChange(ccent.ClusterTypeSharedPool, []string{ccproto.SHARED_POOL_CONFIG_TIPROXY}, nil, true),
			clusterChange(ccent.ClusterTypeSharedPool, []string{ccproto.SHARED_POOL_CONFIG_TIKV}, nil, false),
			true,
		),
		gk.Entry("blocks unknown file scope",
			clusterChange(ccent.ClusterTypeSharedPool, []string{ccproto.SHARED_POOL_CONFIG_INITSQL}, nil, false),
			clusterChange(ccent.ClusterTypeSharedPool, []string{ccproto.SHARED_POOL_CONFIG_TIPROXY}, nil, false),
			true,
		),
		gk.Entry("blocks unknown component scope",
			clusterChange(ccent.ClusterTypeLogicalCluster, nil, []string{ccproto.SHARED_POOL_COMPONENT_TIKV}, false),
			clusterChange(ccent.ClusterTypeLogicalCluster, []string{ccproto.LOGICAL_CLUSTER_CONFIG_TIPROXY}, nil, false),
			true,
		),
	)
})

func clusterChange(clusterType ccent.ClusterType, files []string, components []string, withVersion bool) *ccent.ConfigurationChange {
	change := &ccent.ConfigurationChange{
		TargetCluster: &ccent.ClusterConfigurationLocator{
			ClusterType: clusterType,
			ClusterID:   "123",
		},
	}
	for _, file := range files {
		change.Scope.Files = append(change.Scope.Files, &ccent.ConfigurationFileChange{
			Filename:   file,
			ConfigType: file,
		})
	}
	for _, component := range components {
		change.Scope.Components = append(change.Scope.Components, &ccent.ConfigurationComponentChange{
			Component: component,
		})
	}
	if withVersion {
		change.Scope.Version = &ccent.ConfigurationVersionChange{
			From: "v1",
			To:   "v2",
		}
	}
	return change
}
