package workflow

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
)

func TestGenStorageClassName(t *testing.T) {
	tests := []struct {
		name     string
		storage  *entity.Storage
		expected string
	}{
		{
			name: "normal",
			storage: &entity.Storage{
				Iops:          1,
				ThroughputMbs: 2,
				Type:          "cloud-ssd",
			},
			expected: "cloud-ssd",
		},
		{
			name: "empty throughput",
			storage: &entity.Storage{
				Iops:          1,
				ThroughputMbs: 0,
				Type:          "cloud-ssd",
			},
			expected: "cloud-ssd",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := GenStorageClassName(nil, tt.storage)
			if result != tt.expected {
				t.Errorf("GenStorageClassName() = %v, want %v", result, tt.expected)
			}
		})
	}
}

func TestIsAllComponent(t *testing.T) {
	tests := []struct {
		name     string
		args     actions.ActionArgs
		expected bool
	}{
		{
			name: "all component types",
			args: actions.ActionArgs{
				ComponentTypesKey: actions.ActionArgString(""),
			},
			expected: true,
		},
		{
			name: "single component type",
			args: actions.ActionArgs{
				ComponentTypesKey: actions.ActionArgString("TIDB"),
			},
			expected: false,
		},
		{
			name: "empty component types",
			args: actions.ActionArgs{
				ComponentTypesKey: actions.ActionArgString(""),
			},
			expected: true,
		},
		{
			name:     "no component types key",
			args:     actions.ActionArgs{},
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := IsAllComponent(tt.args)
			if result != tt.expected {
				t.Errorf("IsAllComponent() = %v, want %v", result, tt.expected)
			}
		})
	}
}

func TestCheckIsTargetComponent(t *testing.T) {
	tests := []struct {
		name     string
		comp     *entity.Component
		args     actions.ActionArgs
		expected bool
	}{
		{
			name: "target component",
			comp: &entity.Component{
				Id:            "tidb-1",
				ComponentType: apiv1.ComponentType_TIDB,
			},
			args: actions.ActionArgs{
				ComponentIdsKey: actions.ActionArgString("tidb-1,tikv-1"),
			},
			expected: true,
		},
		{
			name: "not target component",
			comp: &entity.Component{
				Id:            "pd-1",
				ComponentType: apiv1.ComponentType_PD,
			},
			args: actions.ActionArgs{
				ComponentIdsKey: actions.ActionArgString("tidb-1,tikv-1"),
			},
			expected: false,
		},
		{
			name: "empty component ids",
			comp: &entity.Component{
				Id:            "tidb-1",
				ComponentType: apiv1.ComponentType_TIDB,
			},
			args: actions.ActionArgs{
				ComponentIdsKey: actions.ActionArgString(""),
			},
			expected: true,
		},
		{
			name: "no component ids key",
			comp: &entity.Component{
				Id:            "tidb-1",
				ComponentType: apiv1.ComponentType_TIDB,
			},
			args:     actions.ActionArgs{},
			expected: true,
		},
		{
			name: "nil component",
			comp: nil,
			args: actions.ActionArgs{
				ComponentIdsKey: actions.ActionArgString("tidb-1"),
			},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			result := CheckIsTargetComponent(ctx, tt.comp, tt.args)
			if result != tt.expected {
				t.Errorf("CheckIsTargetComponent() = %v, want %v", result, tt.expected)
			}
		})
	}
}

// TestCheckIsTargetComponent_PerPoolHPAWorkers covers the PR-C.1
// invariant: when ComponentIdsKey is set to the per-pool HPA
// component IDs (TIKV_WORKER + TIDB_WORKER + COPROCESSOR_WORKER,
// all sharing the FnvHash(spID)), every worker passes the filter.
// Pre-fix, create_sharedpool.go passed the PD id which caused
// CheckIsTargetComponent to filter every worker out — the workers
// were silently dropped, no HPA was created. This test pins the
// post-fix behavior so a future regression of the same root cause
// surfaces here.
func TestCheckIsTargetComponent_PerPoolHPAWorkers(t *testing.T) {
	workerFnvHash := "FnvHash-sp-1"
	args := actions.ActionArgs{
		ComponentIdsKey: actions.ActionArgString(workerFnvHash),
	}

	workers := []*entity.Component{
		{Id: workerFnvHash, ComponentType: apiv1.ComponentType_TIKV_WORKER},
		{Id: workerFnvHash, ComponentType: apiv1.ComponentType_TIDB_WORKER},
		{Id: workerFnvHash, ComponentType: apiv1.ComponentType_COPROCESSOR_WORKER},
	}

	for _, w := range workers {
		t.Run(w.GetComponentType().String(), func(t *testing.T) {
			result := CheckIsTargetComponent(context.Background(), w, args)
			if !result {
				t.Errorf("CheckIsTargetComponent(%v) = false, want true (PR-C.1 wiring regression?)", w.GetComponentType())
			}
		})
	}
}

func TestBuildTopologies(t *testing.T) {
	tests := []struct {
		name        string
		compType    apiv1.ComponentType
		zones       []string
		templArgs   map[string]string
		pgEnabled   bool
		expected    string
		expectError bool
	}{
		{
			name:      "TIDB component",
			compType:  apiv1.ComponentType_TIDB,
			zones:     []string{"zone1", "zone2", "zone3"},
			templArgs: map[string]string{},
			expected:  "- topology:\n    topology.kubernetes.io/zone: zone1\n- topology:\n    topology.kubernetes.io/zone: zone2\n- topology:\n    topology.kubernetes.io/zone: zone3",
		},
		{
			name:      "TIKV component",
			compType:  apiv1.ComponentType_TIKV,
			zones:     []string{"zone1", "zone2"},
			templArgs: map[string]string{},
			expected:  "- topology:\n    topology.kubernetes.io/zone: zone1\n- topology:\n    topology.kubernetes.io/zone: zone2",
		},
		{
			name:      "PD component",
			compType:  apiv1.ComponentType_PD,
			zones:     []string{"zone1"},
			templArgs: map[string]string{},
			expected:  "- topology:\n    topology.kubernetes.io/zone: zone1",
		},
		{
			name:      "placement group enabled",
			compType:  apiv1.ComponentType_TIKV,
			zones:     []string{"zone1"},
			templArgs: map[string]string{},
			pgEnabled: true,
			expected:  "- topology:\n    tidbcloud.com/pg-partition: \"0\"\n- topology:\n    tidbcloud.com/pg-partition: \"1\"\n- topology:\n    tidbcloud.com/pg-partition: \"2\"",
		},
		{
			name:        "empty zones",
			compType:    apiv1.ComponentType_TIDB,
			zones:       []string{},
			templArgs:   map[string]string{},
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := BuildTopologies(tt.compType, tt.zones, tt.templArgs, tt.pgEnabled)

			if tt.expectError {
				if err == nil {
					t.Error("BuildTopologies() expected error but got none")
				}
				return
			}

			if err != nil {
				t.Errorf("BuildTopologies() unexpected error: %v", err)
			}
			assert.Equal(t, tt.expected, tt.templArgs["Topologies"])
		})
	}
}

func TestSetResourceReady(t *testing.T) {
	tests := []struct {
		name                  string
		comp                  *entity.Component
		templArgs             map[string]string
		expectedRequestMemory string
		expectedCPU           string
		expectedLimitCPU      string
		expectedMemory        string
		expectedGoMemLimit    string
		expectError           bool
	}{
		{
			name: "non tidb uses request formula with sidecar 0",
			comp: &entity.Component{
				Id:            "pd-1",
				ComponentType: apiv1.ComponentType_PD,
				NodeQuota: &entity.NodeQuota{
					NodeSpec: &entity.NodeSpec{
						Cpu:               8,
						MemoryGi:          32,
						AvailableCpu:      8,
						AvailableMemoryMi: 32768,
					},
				},
			},
			templArgs: map[string]string{
				"ready": "true",
			},
			expectedRequestMemory: "31744",
			expectedCPU:           "7800m",
			expectedLimitCPU:      "8000m",
			expectedMemory:        "32768",
		},
		{
			name: "empty templ args",
			comp: &entity.Component{
				Id:            "pd-1",
				ComponentType: apiv1.ComponentType_PD,
				NodeQuota: &entity.NodeQuota{
					NodeSpec: &entity.NodeSpec{
						Cpu:               8,
						MemoryGi:          32,
						AvailableCpu:      8,
						AvailableMemoryMi: 32768,
					},
				},
			},
			templArgs: map[string]string{},
		},
		{
			name: "small tidb memory budget uses tidb request formula",
			comp: &entity.Component{
				Id:            "tidb-small",
				ComponentType: apiv1.ComponentType_TIDB,
				NodeQuota: &entity.NodeQuota{
					NodeSpec: &entity.NodeSpec{
						Cpu:               2,
						MemoryGi:          4,
						AvailableCpu:      1,
						AvailableMemoryMi: 2039,
					},
				},
			},
			templArgs:             map[string]string{},
			expectedRequestMemory: "1522",
			expectedCPU:           "860m",
			expectedLimitCPU:      "1000m",
			expectedMemory:        "2039",
			expectedGoMemLimit:    "1631",
		},
		{
			name: "aws 8c32g tidb uses milli cpu max and request reserve",
			comp: &entity.Component{
				Id:            "tidb-8c32g",
				ComponentType: apiv1.ComponentType_TIDB,
				NodeQuota: &entity.NodeQuota{
					NodeSpec: &entity.NodeSpec{
						Cpu:               8,
						MemoryGi:          32,
						AvailableCpu:      7,
						AvailableCpuMilli: 7460,
						AvailableMemoryMi: 29899,
						MaxCpuMilli:       7910,
						MaxMemoryMi:       29899,
					},
				},
			},
			templArgs:             map[string]string{},
			expectedRequestMemory: "28614",
			expectedCPU:           "7220m",
			expectedLimitCPU:      "7910m",
			expectedMemory:        "29899",
			expectedGoMemLimit:    "23919",
		},
		{
			name: "tidb worker subtracts worker sidecar",
			comp: &entity.Component{
				Id:            "tidb-worker-small",
				ComponentType: apiv1.ComponentType_TIDB_WORKER,
				NodeQuota: &entity.NodeQuota{
					NodeSpec: &entity.NodeSpec{
						Cpu:               2,
						MemoryGi:          4,
						AvailableCpu:      1,
						AvailableMemoryMi: 2039,
					},
				},
			},
			templArgs:             map[string]string{},
			expectedRequestMemory: "1733",
			expectedCPU:           "800m",
			expectedLimitCPU:      "1000m",
			expectedMemory:        "2039",
			expectedGoMemLimit:    "1631",
		},
		{
			name: "small non tidb memory budget uses request formula",
			comp: &entity.Component{
				Id:            "pd-small",
				ComponentType: apiv1.ComponentType_PD,
				NodeQuota: &entity.NodeQuota{
					NodeSpec: &entity.NodeSpec{
						Cpu:               2,
						MemoryGi:          4,
						AvailableCpu:      1,
						AvailableMemoryMi: 2039,
					},
				},
			},
			templArgs:             map[string]string{},
			expectedRequestMemory: "1783",
			expectedCPU:           "900m",
			expectedLimitCPU:      "1000m",
			expectedMemory:        "2039",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := SetResourceReady(tt.comp, tt.templArgs)

			if tt.expectError {
				if err == nil {
					t.Error("SetResourceReady() expected error but got none")
				}
				return
			}

			if err != nil {
				t.Errorf("SetResourceReady() unexpected error: %v", err)
			}
			if tt.expectedRequestMemory != "" {
				assert.Equal(t, tt.expectedRequestMemory, tt.templArgs["RequestMemory"])
			}
			if tt.expectedCPU != "" {
				assert.Equal(t, tt.expectedCPU, tt.templArgs["CPU"])
			}
			if tt.expectedLimitCPU != "" {
				assert.Equal(t, tt.expectedLimitCPU, tt.templArgs["LimitCPU"])
			}
			if tt.expectedMemory != "" {
				assert.Equal(t, tt.expectedMemory, tt.templArgs["Memory"])
			}
			if tt.expectedGoMemLimit != "" {
				assert.Equal(t, tt.expectedGoMemLimit, tt.templArgs["GOMEMLIMIT"])
			}
		})
	}
}

func TestCheckIsTargetComponentEdgeCases(t *testing.T) {
	tests := []struct {
		name     string
		comp     *entity.Component
		args     actions.ActionArgs
		expected bool
	}{
		{
			name: "component with empty id",
			comp: &entity.Component{
				Id: "",
			},
			args: actions.ActionArgs{
				ComponentIdsKey: actions.ActionArgString("tidb-1"),
			},
			expected: false,
		},
		{
			name: "component with special characters",
			comp: &entity.Component{
				Id: "tidb@1#2",
			},
			args: actions.ActionArgs{
				ComponentIdsKey: actions.ActionArgString("tidb@1#2,tikv-1"),
			},
			expected: true,
		},
		{
			name: "case sensitive matching",
			comp: &entity.Component{
				Id: "TIDB-1",
			},
			args: actions.ActionArgs{
				ComponentIdsKey: actions.ActionArgString("tidb-1,TIKV-1"),
			},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			result := CheckIsTargetComponent(ctx, tt.comp, tt.args)
			if result != tt.expected {
				t.Errorf("CheckIsTargetComponent() = %v, want %v", result, tt.expected)
			}
		})
	}
}

func TestCheckIsAffectedComponent(t *testing.T) {
	tikv := &entity.Component{Id: "tikv-1", ComponentType: apiv1.ComponentType_TIKV}
	coprocessor := &entity.Component{Id: "copr-1", ComponentType: apiv1.ComponentType_COPROCESSOR_WORKER}
	tidb := &entity.Component{Id: "tidb-1", ComponentType: apiv1.ComponentType_TIDB}

	t.Run("affected types ignore mutation component ids", func(t *testing.T) {
		args := actions.ActionArgs{
			ComponentIdsKey:           actions.ActionArgString("copr-1"),
			ComponentTypesKey:         actions.ActionArgString("coprocessor-tikv-worker"),
			AffectedComponentTypesKey: actions.ActionArgString("tikv,coprocessor-tikv-worker"),
		}

		assert.True(t, CheckIsAffectedComponent(context.Background(), tikv, args))
		assert.True(t, CheckIsAffectedComponent(context.Background(), coprocessor, args))
		assert.False(t, CheckIsAffectedComponent(context.Background(), tidb, args))
	})

	t.Run("missing affected types preserves target id and type filtering", func(t *testing.T) {
		args := actions.ActionArgs{
			ComponentIdsKey:   actions.ActionArgString("tidb-1"),
			ComponentTypesKey: actions.ActionArgString("tidb"),
		}

		assert.True(t, CheckIsAffectedComponent(context.Background(), tidb, args))
		assert.False(t, CheckIsAffectedComponent(context.Background(), tikv, args))
	})
}

func TestBuildServerLabelsYAML(t *testing.T) {
	tests := []struct {
		name     string
		labels   map[string]string
		expected string
	}{
		{
			name:     "empty labels",
			labels:   map[string]string{},
			expected: "",
		},
		{
			name:     "nil labels",
			labels:   nil,
			expected: "",
		},
		{
			name: "single label",
			labels: map[string]string{
				"keyspace": "cluster-123",
			},
			expected: "labels:\n  \"keyspace\": \"cluster-123\"",
		},
		{
			name: "multiple labels sorted",
			labels: map[string]string{
				"keyspace": "cluster-123",
				"cidr":     "10.0.0.0/8",
				"custom":   "value",
			},
			expected: "labels:\n  \"cidr\": \"10.0.0.0/8\"\n  \"custom\": \"value\"\n  \"keyspace\": \"cluster-123\"",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := BuildServerLabelsYAML(tt.labels)
			if result != tt.expected {
				t.Errorf("BuildServerLabelsYAML() = %q, want %q", result, tt.expected)
			}
		})
	}
}

func TestAddAzureDfsArgs(t *testing.T) {
	tests := []struct {
		name        string
		sharedPool  *entity.SharedPool
		tplArgs     map[string]string
		expectErr   bool
		errContains string
		expected    map[string]string
	}{
		{
			name: "non-azure provider",
			sharedPool: &entity.SharedPool{
				SharedPool: apiv1.SharedPool{
					Provider: &apiv1.Provider{
						Type: apiv1.ProviderType_AWS,
					},
					ObjectStorage: &apiv1.SharedPool_ObjectStorage{
						Bucket: "ignored/bucket",
					},
				},
			},
			tplArgs: map[string]string{
				"keep": "value",
			},
			expected: map[string]string{
				"keep": "value",
			},
		},
		{
			name: "azure provider with valid bucket",
			sharedPool: &entity.SharedPool{
				SharedPool: apiv1.SharedPool{
					Provider: &apiv1.Provider{
						Type:         apiv1.ProviderType_AZURE,
						NodeClientId: "node-client-123",
					},
					ObjectStorage: &apiv1.SharedPool_ObjectStorage{
						Bucket: "account-1/container-1",
					},
				},
			},
			tplArgs: map[string]string{},
			expected: map[string]string{
				"AZURE_CLIENT_ID":     "node-client-123",
				"DfsBackend":          "azure",
				"DfsAzureAccountName": "account-1",
				"DfsAzureContainer":   "container-1",
			},
		},
		{
			name: "azure provider with invalid bucket",
			sharedPool: &entity.SharedPool{
				SharedPool: apiv1.SharedPool{
					Provider: &apiv1.Provider{
						Type: apiv1.ProviderType_AZURE,
					},
					ObjectStorage: &apiv1.SharedPool_ObjectStorage{
						Bucket: "invalid-bucket",
					},
				},
			},
			tplArgs: map[string]string{
				"keep": "value",
			},
			expectErr:   true,
			errContains: "invalid azure bucket format",
			expected: map[string]string{
				"keep": "value",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := AddAzureDfsArgs(tt.sharedPool, tt.tplArgs)
			if tt.expectErr {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.errContains)
			} else {
				require.NoError(t, err)
				assert.Equal(t, tt.expected, tt.tplArgs)
			}
		})
	}
}
