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"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/template/render"
	"github.com/tidbcloud/cluster-service-ng/pkg/tioperator"
	"google.golang.org/protobuf/encoding/protojson"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/yaml"
)

var (
	tidbPrivilegedTestTemplArgs = map[string]string{
		"ClusterId":       "1233438346465886",
		"SharedPoolId":    "1233438346465886",
		"ComponentId":     "3a219eb3",
		"ComponentType":   "privileged-tidb",
		"ComponentGroup":  "db-3a219eb3-privileged-tidb",
		"AuditLogConfig":  "logDir: /var/log/auditlog\ngracefulStopWaitSeconds: 20",
		"Image":           "385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tidb:master-next-gen",
		"AuditLogImage":   "385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/audit-ng:c91e07382bbf5914ed9f81673bef1c23144b8972",
		"SlowLogImage":    "385595570414.dkr.ecr.us-west-2.amazonaws.com/third-party/busybox:1.34.1",
		"ImagePullPolicy": "IfNotPresent",
		"verifyCN":        "59accb4c",
		"TiKVWorkerURL":   "https://db-3a219eb3-tikv-worker-peer.tidb1233438346465886.svc:19000",
	}
)

func Test_RenderTiDBPrivilegedDeployment(t *testing.T) {
	tl := initTiDBPrivilegedTemplate(t)
	yamlStr, err := tl.Render("tidb_privileged_deployment", tidbPrivilegedTestTemplArgs)
	require.NoError(t, err)
	require.NotEmpty(t, yamlStr)
	assert.YAMLEq(t, `apiVersion: apps/v1
kind: Deployment
metadata:
  name: db-3a219eb3-privileged-tidb
  namespace: tidb1233438346465886
  labels:
    pingcap.com/group: db-3a219eb3-privileged-tidb
    pingcap.com/component: privileged-tidb
    pingcap.com/cluster: db
    tags.tidbcloud.com/shared-pool: '1233438346465886'
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      pingcap.com/group: db-3a219eb3-privileged-tidb
      pingcap.com/component: privileged-tidb
      pingcap.com/cluster: db
      tags.tidbcloud.com/shared-pool: '1233438346465886'
  template:
    metadata:
      labels:
        pingcap.com/group: db-3a219eb3-privileged-tidb
        pingcap.com/component: privileged-tidb
        tags.tidbcloud.com/component: privileged-tidb
        pingcap.com/cluster: db
        tags.tidbcloud.com/shared-pool: '1233438346465886'
        tags.tidbcloud.com/resource-pool: '1233438346465886'
      annotations:
        prometheus.io/path: /metrics
        prometheus.io/port: "10080"
        prometheus.io/scrape: "true"
        tidb.auditlog.prometheus.io/port: "9090"
        tidb.auditlog.config: |
          logDir: /var/log/auditlog
          gracefulStopWaitSeconds: 20
    spec:
      priorityClassName: tidb-ng-component-noncritical
      serviceAccountName: tidb-cluster
      subdomain: db-3a219eb3-privileged-tidb-peer
      containers:
      - name: tidb
        image: 385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tidb:master-next-gen
        imagePullPolicy: IfNotPresent
        command:
        - /tidb-server
        - --config
        - /etc/tidb/config.toml
        env:
        - name: TZ
          value: UTC
        - name: NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        ports:
        - containerPort: 4000
          hostPort: 4000
          name: mysql-client
          protocol: TCP
        - containerPort: 10080
          hostPort: 10080
          name: status
          protocol: TCP
        readinessProbe:
          failureThreshold: 3
          initialDelaySeconds: 10
          periodSeconds: 10
          successThreshold: 1
          exec:
            command:
            - "curl"
            - "--cacert"
            - "/var/lib/tidb-tls/ca.crt"
            - "--cert"
            - "/var/lib/tidb-tls/tls.crt"
            - "--key"
            - "/var/lib/tidb-tls/tls.key"
            - "https://127.0.0.1:10080/status"
          timeoutSeconds: 1
        resources:
          requests:
            cpu: 1
            memory: 2Gi
          limits:
            cpu: 1
            memory: 2Gi
        securityContext:
          readOnlyRootFilesystem: true
          allowPrivilegeEscalation: false
          privileged: false
          capabilities:
            drop:
              - NET_RAW
        volumeMounts:
        - mountPath: /etc/tidb
          name: ti-config
        - mountPath: /var/log/auditlog
          name: ti-auditlog
        - mountPath: /var/lib/tidb-sql-tls
          name: ti-tidb-sql-tls
          readOnly: true
        - mountPath: /var/lib/tidb-tls
          name: ti-tls
          readOnly: true
        - name: tmp-volume
          mountPath: /tmp
      - name: auditlog
        image: 385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/audit-ng:c91e07382bbf5914ed9f81673bef1c23144b8972
        imagePullPolicy: IfNotPresent
        command:
        - /audit-ng
        - server
        - --config
        - /etc/podinfo/audit-config.yaml
        env:
        - name: TZ
          value: UTC
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        ports:
        - containerPort: 9090
          hostPort: 9090
          name: metrics
          protocol: TCP
        resources:
          limits:
            cpu: 100m
            memory: 256Mi
          requests:
            cpu: 100m
            memory: 256Mi
        securityContext:
          readOnlyRootFilesystem: true
          allowPrivilegeEscalation: false
          privileged: false
          capabilities:
            drop:
              - NET_RAW
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /var/log/auditlog
          name: ti-auditlog
        - mountPath: /etc/podinfo
          name: podinfo
          readOnly: true
      initContainers:
      - name: init
        image: 385595570414.dkr.ecr.us-west-2.amazonaws.com/third-party/busybox:1.34.1
        imagePullPolicy: IfNotPresent
        command:
        - sh
        - -c
        - sysctl
        - -w
        - net.ipv4.tcp_keepalive_intvl=75
        - net.ipv4.tcp_keepalive_time=300
        securityContext:
          privileged: true
      tolerations:
      - effect: NoExecute
        key: node.kubernetes.io/not-ready
        operator: Exists
        tolerationSeconds: 300
      - effect: NoExecute
        key: node.kubernetes.io/unreachable
        operator: Exists
        tolerationSeconds: 300
      dnsPolicy: ClusterFirstWithHostNet
      hostNetwork: true
      volumes:
      - name: ti-auditlog
        emptyDir: {}
      - configMap:
          defaultMode: 420
          name: db-3a219eb3-privileged-tidb
        name: ti-config
      - name: ti-tidb-sql-tls
        secret:
          defaultMode: 420
          secretName: db-3a219eb3-privileged-tidb-server-secret
      - name: ti-tls
        secret:
          defaultMode: 420
          secretName: db-3a219eb3-privileged-tidb-cluster-secret
      - downwardAPI:
          defaultMode: 420
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.annotations['tidb.auditlog.config']
            path: audit-config.yaml
        name: podinfo
      - name: tmp-volume
        emptyDir: {}
`, yamlStr)
}

func Test_RenderTiDBPrivilegedConfigMap(t *testing.T) {
	tl := initTiDBPrivilegedTemplate(t)
	yamlStr, err := tl.Render("tidb_privileged_configmap", tidbPrivilegedTestTemplArgs)
	require.NoError(t, err)
	require.NotEmpty(t, yamlStr)
	assert.YAMLEq(t, `apiVersion: v1
kind: ConfigMap
metadata:
  name: db-3a219eb3-privileged-tidb
  namespace: tidb1233438346465886
  labels:
    pingcap.com/group: db-3a219eb3-privileged-tidb
    pingcap.com/component: privileged-tidb
    pingcap.com/cluster: db
    tags.tidbcloud.com/shared-pool: '1233438346465886'
data:
  config.toml: |
    advertise-address = 'db-3a219eb3-privileged-tidb.db-3a219eb3-privileged-tidb-peer.tidb1233438346465886.svc'
    disaggregated-tiflash = true
    enable-telemetry = false
    graceful-wait-before-shutdown = 15
    host = '::'
    keyspace-name = ''
    path = 'db-3a219eb3-pd.tidb1233438346465886:2379'
    store = 'tikv'
    tikv-worker-url = 'https://db-3a219eb3-tikv-worker-peer.tidb1233438346465886.svc:19000'
    [log]
    slow-query-file = '/var/log/auditlog/slowlog'

    [log.file]
    max-backups = 3

    [performance]
    tcp-keep-alive = true
    skip-init-stats = true

    [security]
    cluster-ssl-ca = '/var/lib/tidb-tls/ca.crt'
    cluster-ssl-cert = '/var/lib/tidb-tls/tls.crt'
    cluster-ssl-key = '/var/lib/tidb-tls/tls.key'
    ssl-ca = '/var/lib/tidb-sql-tls/ca.crt'
    ssl-cert = '/var/lib/tidb-sql-tls/tls.crt'
    ssl-key = '/var/lib/tidb-sql-tls/tls.key'
    cluster-verify-cn = ['59accb4c']
    enable-sem = true
    skip-grant-table = true

    [status]
    record-db-label = true

    [tikv-client]
    [tikv-client.copr-cache]
    capacity-mb = 0.0

    [instance]
    tidb_enable_stats_owner = false
    tidb_enable_ddl = false

    [standby]
    standby-mode = true
`, yamlStr)
}

func Test_RenderTiDBPrivilegedConfigMap_WithoutTiKVWorkerURL(t *testing.T) {
	tl := initTiDBPrivilegedTemplate(t)
	templArgs := map[string]string{}
	for k, v := range tidbPrivilegedTestTemplArgs {
		templArgs[k] = v
	}
	delete(templArgs, "TiKVWorkerURL")

	yamlStr, err := tl.Render("tidb_privileged_configmap", templArgs)
	require.NoError(t, err)
	require.NotEmpty(t, yamlStr)
	assert.NotContains(t, yamlStr, "tikv-worker-url")
}

func Test_RenderTiDBPrivilegedSVC(t *testing.T) {
	tl := initTiDBPrivilegedTemplate(t)
	yamlStr, err := tl.Render("tidb_privileged_svc", tidbPrivilegedTestTemplArgs)
	require.NoError(t, err)
	require.NotEmpty(t, yamlStr)
	assert.YAMLEq(t, `apiVersion: v1
kind: Service
metadata:
  name: db-3a219eb3-privileged-tidb
  namespace: tidb1233438346465886
  labels:
    pingcap.com/group: db-3a219eb3-privileged-tidb
    pingcap.com/component: privileged-tidb
    pingcap.com/cluster: db
    tags.tidbcloud.com/shared-pool: '1233438346465886'
spec:
  type: ClusterIP
  selector:
    pingcap.com/group: db-3a219eb3-privileged-tidb
    pingcap.com/component: privileged-tidb
    pingcap.com/cluster: db
  ports:
  - name: mysql-client
    port: 4000
    protocol: TCP
    targetPort: mysql-client
  - name: status
    port: 10080
    protocol: TCP
    targetPort: status
`, yamlStr)
}

func TestApplyTiDBPrivilegedActionShouldApply(t *testing.T) {
	tidbWorker := &entity.Component{Id: "worker-1", ComponentType: apiv1.ComponentType_TIDB_WORKER}
	tikvWorker := &entity.Component{Id: "tikv-worker-1", ComponentType: apiv1.ComponentType_TIKV_WORKER}
	sharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			ComponentGroups: []*apiv1.Component{tidbWorker, tikvWorker},
		},
	}
	sharedPoolStr, err := protojson.Marshal(sharedPool)
	require.NoError(t, err)

	tests := []struct {
		name               string
		args               actions.ActionArgs
		wantShouldApply    bool
		wantUpdateExisting bool
	}{
		{
			name:               "create path with no update options applies without updating existing resources",
			args:               actions.ActionArgs{},
			wantShouldApply:    true,
			wantUpdateExisting: false,
		},
		{
			name: "tidb worker image update applies and updates existing deployment",
			args: actions.ActionArgs{
				workflow.SharedPoolKey:                   actions.ActionArgString(string(sharedPoolStr)),
				entity.GetUpdateComponentKey(tidbWorker): actions.ActionArgString(string(tioperator.OpsUpdateImage)),
			},
			wantShouldApply:    true,
			wantUpdateExisting: true,
		},
		{
			name: "tidb worker non image update skips",
			args: actions.ActionArgs{
				workflow.SharedPoolKey:                   actions.ActionArgString(string(sharedPoolStr)),
				entity.GetUpdateComponentKey(tidbWorker): actions.ActionArgString(string(tioperator.OpsUpdateNodeCount)),
			},
			wantShouldApply:    false,
			wantUpdateExisting: false,
		},
		{
			name: "empty update option skips",
			args: actions.ActionArgs{
				workflow.SharedPoolKey:                   actions.ActionArgString(string(sharedPoolStr)),
				entity.GetUpdateComponentKey(tidbWorker): actions.ActionArgString(""),
			},
			wantShouldApply:    false,
			wantUpdateExisting: false,
		},
		{
			name: "other component image update skips",
			args: actions.ActionArgs{
				workflow.SharedPoolKey:                   actions.ActionArgString(string(sharedPoolStr)),
				entity.GetUpdateComponentKey(tikvWorker): actions.ActionArgString(string(tioperator.OpsUpdateImage)),
			},
			wantShouldApply:    false,
			wantUpdateExisting: false,
		},
	}

	action := &applyTiDBPrivilegedAction{}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotShouldApply, gotUpdateExisting := action.shouldApply(tt.args, sharedPool)
			assert.Equal(t, tt.wantShouldApply, gotShouldApply)
			assert.Equal(t, tt.wantUpdateExisting, gotUpdateExisting)
		})
	}
}

func TestHasComponentType(t *testing.T) {
	components := []*entity.Component{
		{ComponentType: apiv1.ComponentType_TIKV},
		{ComponentType: apiv1.ComponentType_PD},
	}

	assert.True(t, hasComponentType(components, apiv1.ComponentType_TIKV))
	assert.False(t, hasComponentType(components, apiv1.ComponentType_TIDB_WORKER))
}

func TestShouldUseSharedPoolTiDBAsPrivilegedSource(t *testing.T) {
	tests := []struct {
		name     string
		pool     *entity.SharedPool
		expected bool
	}{
		{
			name:     "rcu driven always false",
			pool:     &entity.SharedPool{SharedPool: apiv1.SharedPool{CapacityMode: apiv1.CapacityMode_RCU_DRIVEN}},
			expected: false,
		},
		{
			name:     "topology driven with missing tidb worker true",
			pool:     &entity.SharedPool{SharedPool: apiv1.SharedPool{CapacityMode: apiv1.CapacityMode_TOPOLOGY_DRIVEN}},
			expected: true,
		},
		{
			name: "topology driven with tidb worker node_count zero true",
			pool: &entity.SharedPool{SharedPool: apiv1.SharedPool{
				CapacityMode: apiv1.CapacityMode_TOPOLOGY_DRIVEN,
				ComponentGroups: []*apiv1.Component{{
					ComponentType: apiv1.ComponentType_TIDB_WORKER,
					NodeQuota:     &apiv1.NodeQuota{NodeCount: 0},
				}},
			}},
			expected: true,
		},
		{
			name: "topology driven with tidb worker node_count positive false",
			pool: &entity.SharedPool{SharedPool: apiv1.SharedPool{
				CapacityMode: apiv1.CapacityMode_TOPOLOGY_DRIVEN,
				ComponentGroups: []*apiv1.Component{{
					ComponentType: apiv1.ComponentType_TIDB_WORKER,
					NodeQuota:     &apiv1.NodeQuota{NodeCount: 1},
				}},
			}},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, shouldUseSharedPoolTiDBAsPrivilegedSource(tt.pool))
		})
	}
}

func TestApplyTiDBPrivilegedActionApplyResourceUpdatesDeploymentImage(t *testing.T) {
	tl := initTiDBPrivilegedTemplate(t)
	oldArgs := cloneStringMap(tidbPrivilegedTestTemplArgs)
	oldArgs["Image"] = "example.com/tidb:old"

	oldYAML, err := tl.Render("tidb_privileged_deployment", oldArgs)
	require.NoError(t, err)
	existing, _, err := unstructured.UnstructuredJSONScheme.Decode([]byte(mustYAMLToJSON(t, oldYAML)), nil, nil)
	require.NoError(t, err)

	newYAML, err := tl.Render("tidb_privileged_deployment", tidbPrivilegedTestTemplArgs)
	require.NoError(t, err)
	updated, _, err := unstructured.UnstructuredJSONScheme.Decode([]byte(mustYAMLToJSON(t, newYAML)), nil, nil)
	require.NoError(t, err)

	scheme := runtime.NewScheme()
	scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, &unstructured.Unstructured{})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(existing.(*unstructured.Unstructured)).Build()

	action := &applyTiDBPrivilegedAction{}
	require.NoError(t, action.applyResource(context.Background(), cli, updated.(*unstructured.Unstructured), true))

	got := &unstructured.Unstructured{}
	got.SetGroupVersionKind(updated.GetObjectKind().GroupVersionKind())
	require.NoError(t, cli.Get(context.Background(), types.NamespacedName{
		Name:      updated.(*unstructured.Unstructured).GetName(),
		Namespace: updated.(*unstructured.Unstructured).GetNamespace(),
	}, got))
	assert.Equal(t, tidbPrivilegedTestTemplArgs["Image"], getContainerImage(t, got, "tidb"))
}

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

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

func cloneStringMap(in map[string]string) map[string]string {
	out := make(map[string]string, len(in))
	for k, v := range in {
		out[k] = v
	}
	return out
}

func mustYAMLToJSON(t *testing.T, yamlStr string) string {
	t.Helper()
	jsonBytes, err := yaml.YAMLToJSON([]byte(yamlStr))
	require.NoError(t, err)
	return string(jsonBytes)
}

func getContainerImage(t *testing.T, obj *unstructured.Unstructured, containerName string) string {
	t.Helper()
	containers, ok, err := unstructured.NestedSlice(obj.Object, "spec", "template", "spec", "containers")
	require.NoError(t, err)
	require.True(t, ok)
	for _, container := range containers {
		containerMap, ok := container.(map[string]any)
		require.True(t, ok)
		if containerMap["name"] == containerName {
			image, ok := containerMap["image"].(string)
			require.True(t, ok)
			return image
		}
	}
	t.Fatalf("container %s not found", containerName)
	return ""
}
