package action

import (
	"context"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/template/render"
	"google.golang.org/grpc"
)

var (
	ticiMetaTestTemplArgs = map[string]string{
		"ClusterId":         "1233438346465886",
		"SharedPoolId":      "1233438346465886",
		"ComponentId":       "3a219eb3",
		"ComponentType":     "TICI_META",
		"ComponentGroup":    "db-3a219eb3-tici-meta",
		"Image":             "385595570414.dkr.ecr.us-west-2.amazonaws.com/tici/tici:master",
		"ImagePullPolicy":   "IfNotPresent",
		"TiCiRole":          "meta",
		"ServerPort":        "8500",
		"StatusPort":        "8501",
		"PDAddress":         "db-3a219eb3-pd.tidb1233438346465886",
		"DfsPrefix":         "/tidb1233438346465886/tici",
		"DfsBucket":         "tidb-bucket",
		"DfsRegion":         "us-west-2",
		"DfsEndpoint":       "s3.us-west-2.amazonaws.com",
		"TiDBWorkerAddress": "db-3a219eb3-tidb-worker.tidb1233438346465886.svc",
		"VerifiedCN":        "59accb4c",
	}

	ticiWorkerTestTemplArgs = map[string]string{
		"ClusterId":       "1233438346465886",
		"SharedPoolId":    "1233438346465886",
		"ComponentId":     "3a219eb3",
		"ComponentType":   "TICI_WORKER",
		"ComponentGroup":  "db-3a219eb3-tici-worker",
		"Image":           "385595570414.dkr.ecr.us-west-2.amazonaws.com/tici/tici:master",
		"ImagePullPolicy": "IfNotPresent",
		"TiCiRole":        "worker",
		"ServerPort":      "8510",
		"StatusPort":      "8511",
		"NeedData":        "true",
		"PDAddress":       "db-3a219eb3-pd.tidb1233438346465886",
		"DfsPrefix":       "/tidb1233438346465886/tici",
		"DfsBucket":       "tidb-bucket",
		"DfsRegion":       "us-west-2",
		"DfsEndpoint":     "s3.us-west-2.amazonaws.com",
		"VerifiedCN":      "59accb4c",
	}
)

func Test_GetSmallestNodeSpec(t *testing.T) {
	tests := []struct {
		name          string
		nodePools     []*dplanemgrv1.NodePool
		expectedCpu   uint32
		expectedMemGi uint32
		expectError   bool
	}{
		{
			name: "single node pool",
			nodePools: []*dplanemgrv1.NodePool{
				{
					CpuCore:               4,
					MemoryGi:              16,
					K8SVersion:            "1.28",
					AvailableCpuMilliCore: 3800,
					AvailableMemoryMi:     15360,
				},
			},
			expectedCpu:   4,
			expectedMemGi: 16,
			expectError:   false,
		},
		{
			name: "multiple node pools - select by CPU",
			nodePools: []*dplanemgrv1.NodePool{
				{
					CpuCore:               8,
					MemoryGi:              32,
					K8SVersion:            "1.28",
					AvailableCpuMilliCore: 8000,
					AvailableMemoryMi:     32000,
				},
				{
					CpuCore:               4,
					MemoryGi:              16,
					K8SVersion:            "1.28",
					AvailableCpuMilliCore: 4000,
					AvailableMemoryMi:     16000,
				},
				{
					CpuCore:               16,
					MemoryGi:              64,
					K8SVersion:            "1.28",
					AvailableCpuMilliCore: 16000,
					AvailableMemoryMi:     64000,
				},
			},
			expectedCpu:   4,
			expectedMemGi: 16,
			expectError:   false,
		},
		{
			name: "multiple node pools - select by memory when CPU same",
			nodePools: []*dplanemgrv1.NodePool{
				{
					CpuCore:               4,
					MemoryGi:              32,
					K8SVersion:            "1.28",
					AvailableCpuMilliCore: 4000,
					AvailableMemoryMi:     32000,
				},
				{
					CpuCore:               4,
					MemoryGi:              16,
					K8SVersion:            "1.28",
					AvailableCpuMilliCore: 4000,
					AvailableMemoryMi:     16000,
				},
				{
					CpuCore:               4,
					MemoryGi:              8,
					K8SVersion:            "1.28",
					AvailableCpuMilliCore: 4000,
					AvailableMemoryMi:     8000,
				},
			},
			expectedCpu:   4,
			expectedMemGi: 8,
			expectError:   false,
		},
		{
			name: "skip node pools with local disk",
			nodePools: []*dplanemgrv1.NodePool{
				{
					CpuCore:               2,
					MemoryGi:              8,
					K8SVersion:            "1.28",
					WithLocalDisk:         true, // should be skipped
					AvailableCpuMilliCore: 2000,
					AvailableMemoryMi:     8000,
				},
				{
					CpuCore:               4,
					MemoryGi:              16,
					K8SVersion:            "1.28",
					WithLocalDisk:         false,
					AvailableCpuMilliCore: 4000,
					AvailableMemoryMi:     16000,
				},
				{
					CpuCore:               8,
					MemoryGi:              32,
					K8SVersion:            "1.28",
					WithLocalDisk:         true, // should be skipped
					AvailableCpuMilliCore: 8000,
					AvailableMemoryMi:     32000,
				},
			},
			expectedCpu:   4,
			expectedMemGi: 16,
			expectError:   false,
		},
		{
			name: "all node pools have local disk",
			nodePools: []*dplanemgrv1.NodePool{
				{
					CpuCore:       4,
					MemoryGi:      16,
					K8SVersion:    "1.28",
					WithLocalDisk: true,
				},
				{
					CpuCore:       8,
					MemoryGi:      32,
					K8SVersion:    "1.28",
					WithLocalDisk: true,
				},
			},
			expectError: true,
		},
		{
			name:        "no node pools",
			nodePools:   []*dplanemgrv1.NodePool{},
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			action := &ApplyTiCiAction{
				dataplaneClient: &mockDataPlaneClient{
					nodePools: tt.nodePools,
				},
			}

			sharedPool := &entity.SharedPool{
				SharedPool: apiv1.SharedPool{
					DataplaneName: "test-dataplane",
				},
			}

			nodeSpec, version, err := action.getSmallestNodeSpec(context.Background(), sharedPool)

			if tt.expectError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)
			assert.Equal(t, tt.expectedCpu, nodeSpec.Cpu)
			assert.Equal(t, tt.expectedMemGi, nodeSpec.MemoryGi)
			assert.NotEmpty(t, version)
		})
	}
}

func Test_CreateDefaultTiCiComponent(t *testing.T) {
	tests := []struct {
		name          string
		componentId   string
		componentType apiv1.ComponentType
		expectError   bool
	}{
		{
			name:          "create TiCI meta component",
			componentId:   "test-component-id",
			componentType: apiv1.ComponentType_TICI_META,
			expectError:   false,
		},
		{
			name:          "create TiCI worker component",
			componentId:   "test-component-id",
			componentType: apiv1.ComponentType_TICI_WORKER,
			expectError:   false,
		},
		{
			name:          "no default component ID",
			componentId:   "",
			componentType: apiv1.ComponentType_TICI_META,
			expectError:   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			action := &ApplyTiCiAction{}

			sharedPool := &entity.SharedPool{
				SharedPool: apiv1.SharedPool{
					ComponentGroups: []*apiv1.Component{
						{
							Id:            tt.componentId,
							ComponentType: apiv1.ComponentType_PD,
						},
					},
				},
			}

			nodeSpec := &entity.NodeSpec{
				Cpu:      4,
				MemoryGi: 16,
			}

			comp, err := action.createDefaultTiCiComponent(sharedPool, tt.componentType, nodeSpec)

			if tt.expectError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)
			assert.Equal(t, tt.componentId, comp.Id)
			assert.Equal(t, tt.componentType, comp.ComponentType)
			assert.Equal(t, apiv1.Component_CREATING, comp.Phase)
			assert.NotNil(t, comp.NodeQuota)
			assert.Equal(t, uint32(0), comp.NodeQuota.NodeCount)
			assert.Equal(t, uint32(0), comp.NodeQuota.MinNodeCount)

			// Meta has MaxNodeCount=1, Worker has MaxNodeCount=10
			expectedMaxNodeCount := uint32(10)
			if tt.componentType == apiv1.ComponentType_TICI_META {
				expectedMaxNodeCount = 1
			}
			assert.Equal(t, expectedMaxNodeCount, comp.NodeQuota.MaxNodeCount)
			assert.Equal(t, nodeSpec, comp.NodeQuota.NodeSpec)
		})
	}
}

func Test_RenderTiCiMetaConfigMap(t *testing.T) {
	tl := initTiCiTemplate(t)
	yamlStr, err := tl.Render("tici_meta_configmap", ticiMetaTestTemplArgs)
	require.NoError(t, err)
	require.NotEmpty(t, yamlStr)

	// Verify that the rendered YAML contains expected fields
	assert.Contains(t, yamlStr, "kind: ConfigMap")
	assert.Contains(t, yamlStr, "name: db-3a219eb3-tici-meta")
	assert.Contains(t, yamlStr, "namespace: tidb1233438346465886")
	assert.Contains(t, yamlStr, "pd_addr")
	assert.Contains(t, yamlStr, "prefix")      // S3 prefix instead of dfs_prefix
	assert.Contains(t, yamlStr, "tidb_server") // TiDB server config

	// Verify Meta uses service address (Deployment with single replica)
	assert.Contains(t, yamlStr, "advertise_addr = \"db-3a219eb3-tici-meta-peer.tidb1233438346465886.svc:8500\"")
	assert.Contains(t, yamlStr, "advertise_status_addr = \"db-3a219eb3-tici-meta-peer.tidb1233438346465886.svc:8501\"")
	assert.Contains(t, yamlStr, "addr = \"0.0.0.0:8500\"")
	assert.Contains(t, yamlStr, "status_addr = \"0.0.0.0:8501\"")
}

func Test_RenderTiCiWorkerConfigMap(t *testing.T) {
	tl := initTiCiTemplate(t)
	yamlStr, err := tl.Render("tici_worker_configmap", ticiWorkerTestTemplArgs)
	require.NoError(t, err)
	require.NotEmpty(t, yamlStr)

	// Verify that the rendered YAML contains expected fields
	assert.Contains(t, yamlStr, "kind: ConfigMap")
	assert.Contains(t, yamlStr, "name: db-3a219eb3-tici-worker")
	assert.Contains(t, yamlStr, "namespace: tidb1233438346465886")
	assert.Contains(t, yamlStr, "pd_addr")
	assert.Contains(t, yamlStr, "prefix") // S3 prefix instead of dfs_prefix

	// Verify StatefulSet-specific advertise address configuration
	assert.Contains(t, yamlStr, "advertise_addr = \"POD_NAME.db-3a219eb3-tici-worker-peer.tidb1233438346465886.svc:8510\"")
	assert.Contains(t, yamlStr, "advertise_status_addr = \"POD_NAME.db-3a219eb3-tici-worker-peer.tidb1233438346465886.svc:8511\"")
	assert.Contains(t, yamlStr, "addr = \"0.0.0.0:8510\"")
	assert.Contains(t, yamlStr, "status_addr = \"0.0.0.0:8511\"")
}

func Test_RenderTiCiMetaDeployment(t *testing.T) {
	tl := initTiCiTemplate(t)
	yamlStr, err := tl.Render("tici_meta", ticiMetaTestTemplArgs)
	require.NoError(t, err)
	require.NotEmpty(t, yamlStr)

	// Verify common fields
	assert.Contains(t, yamlStr, "kind: Deployment")
	assert.Contains(t, yamlStr, "hostNetwork: true")
	assert.Contains(t, yamlStr, ticiMetaTestTemplArgs["ServerPort"])
	assert.Contains(t, yamlStr, ticiMetaTestTemplArgs["StatusPort"])
	assert.Contains(t, yamlStr, "tici-meta")
}

func Test_RenderTiCiWorkerStatefulSet(t *testing.T) {
	// Add required storage parameters for worker
	workerTemplArgs := make(map[string]string)
	for k, v := range ticiWorkerTestTemplArgs {
		workerTemplArgs[k] = v
	}
	workerTemplArgs["StorageSize"] = "300"
	workerTemplArgs["StorageClassName"] = "gp3-encrypted"
	workerTemplArgs["VolumeAttributesClassName"] = "io2-3000iops"

	tl := initTiCiTemplate(t)
	yamlStr, err := tl.Render("tici_worker", workerTemplArgs)
	require.NoError(t, err)
	require.NotEmpty(t, yamlStr)

	// Verify StatefulSet specific fields
	assert.Contains(t, yamlStr, "kind: StatefulSet")
	assert.Contains(t, yamlStr, "hostNetwork: true")
	assert.Contains(t, yamlStr, workerTemplArgs["ServerPort"])
	assert.Contains(t, yamlStr, workerTemplArgs["StatusPort"])
	assert.Contains(t, yamlStr, "tici-worker")
	assert.Contains(t, yamlStr, "volumeClaimTemplates")
	assert.Contains(t, yamlStr, "serviceName:")
}

func Test_RenderTiCiService(t *testing.T) {
	tests := []struct {
		name     string
		templArg map[string]string
	}{
		{
			name:     "TiCI meta service",
			templArg: ticiMetaTestTemplArgs,
		},
		{
			name:     "TiCI worker service",
			templArg: ticiWorkerTestTemplArgs,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tl := initTiCiTemplate(t)
			yamlStr, err := tl.Render("tici_svc", tt.templArg)
			require.NoError(t, err)
			require.NotEmpty(t, yamlStr)

			// Verify service fields
			assert.Contains(t, yamlStr, "kind: Service")
			assert.Contains(t, yamlStr, "type: ClusterIP")
			assert.Contains(t, yamlStr, tt.templArg["ServerPort"])
			assert.Contains(t, yamlStr, tt.templArg["StatusPort"])
		})
	}
}

func initTiCiTemplate(t *testing.T) *render.TemplateLibrary {
	t.Helper()
	tplPath := "../../../deployments/templates"
	templateLib := render.NewTemplateLibrary()

	ticiTemplates := []string{
		"tici_meta",
		"tici_worker",
		"tici_svc",
		"tici_meta_configmap",
		"tici_worker_configmap",
	}

	for _, tpl := range ticiTemplates {
		data, err := os.ReadFile(filepath.Join(tplPath, tpl+".templ"))
		require.NoError(t, err, "Failed to read template file: %s", tpl)
		templateLib.SafeNewTemplate(tpl, string(data))
	}
	return templateLib
}

// Mock implementation for testing
type mockDataPlaneClient struct {
	dplanemgrv1.DataPlaneServiceClient
	nodePools []*dplanemgrv1.NodePool
}

func (m *mockDataPlaneClient) ListNodePools(ctx context.Context, req *dplanemgrv1.ListNodePoolsRequest, opts ...grpc.CallOption) (*dplanemgrv1.ListNodePoolsResponse, error) {
	return &dplanemgrv1.ListNodePoolsResponse{
		NodePools: m.nodePools,
	}, nil
}
