package podautoscaler

import (
	"strconv"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/tidbcloud/observability/agent-control-server/pkg/config"
	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
)

func TestSelectSpec(t *testing.T) {
	tests := []struct {
		name               string
		action             string
		currentSpec        string
		minSpec            string
		maxSpec            string
		expectedSpec       string
		expectedSkipReason string
	}{
		{
			name:               "action up: normal upgrade",
			action:             consts.ScaleActionUp,
			currentSpec:        "default",
			minSpec:            "default",
			maxSpec:            "largex",
			expectedSpec:       "medium",
			expectedSkipReason: "",
		},
		{
			name:               "action up: reach max spec",
			action:             consts.ScaleActionUp,
			currentSpec:        "large",
			minSpec:            "default",
			maxSpec:            "largex",
			expectedSpec:       "largex",
			expectedSkipReason: "",
		},
		{
			name:               "action up: already at max spec",
			action:             consts.ScaleActionUp,
			currentSpec:        "largex",
			minSpec:            "default",
			maxSpec:            "largex",
			expectedSpec:       "largex",
			expectedSkipReason: skipReasonMaxSpec,
		},
		{
			name:               "action down: normal downgrade",
			action:             consts.ScaleActionDown,
			currentSpec:        "large",
			minSpec:            "default",
			maxSpec:            "largex",
			expectedSpec:       "medium",
			expectedSkipReason: "",
		},
		{
			name:               "action down: reach min spec",
			action:             consts.ScaleActionDown,
			currentSpec:        "medium",
			minSpec:            "default",
			maxSpec:            "largex",
			expectedSpec:       "default",
			expectedSkipReason: "",
		},
		{
			name:               "action down: already at min spec",
			action:             consts.ScaleActionDown,
			currentSpec:        "default",
			minSpec:            "default",
			maxSpec:            "largex",
			expectedSpec:       "default",
			expectedSkipReason: skipReasonMinSpec,
		},
		{
			name:               "action down: min spec grater than current spec",
			action:             consts.ScaleActionDown,
			currentSpec:        "default",
			minSpec:            "medium",
			maxSpec:            "largex",
			expectedSpec:       "medium",
			expectedSkipReason: "",
		},
		{
			name:               "unknown action",
			action:             "unknown",
			currentSpec:        "small",
			minSpec:            "small",
			maxSpec:            "large",
			expectedSpec:       "small",
			expectedSkipReason: skipReasonOther,
		},
		{
			name:               "current spec not in specs",
			action:             consts.ScaleActionUp,
			currentSpec:        "non-existent",
			minSpec:            "default",
			maxSpec:            "largex",
			expectedSpec:       "non-existent",
			expectedSkipReason: skipReasonOther,
		},
		{
			name:               "empty action",
			action:             "",
			currentSpec:        "default",
			minSpec:            "default",
			maxSpec:            "largex",
			expectedSpec:       "default",
			expectedSkipReason: skipReasonOther,
		},
		{
			name:               "current spec matches min and max",
			action:             consts.ScaleActionUp,
			currentSpec:        "default",
			minSpec:            "default",
			maxSpec:            "default",
			expectedSpec:       "default",
			expectedSkipReason: skipReasonMaxSpec,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			specs := []config.CustomerAgentSpec{
				{Name: "default", MaxNodes: 5},
				{Name: "medium", MaxNodes: 50},
				{Name: "large", MaxNodes: 100},
				{Name: "largex", MaxNodes: 200},
			}
			got := selectSpec(specs, tt.action, tt.currentSpec, tt.minSpec, tt.maxSpec)
			assert.Equal(t, tt.expectedSpec, got.newSpec)
			assert.Equal(t, tt.expectedSkipReason, got.skipReason)
		})
	}
}

func TestDecideSpecByScaleResult(t *testing.T) {
	now := time.Now()
	defaultRange := specRange{minSpec: "default", maxSpec: "largex"}

	tests := []struct {
		name               string
		annotations        map[string]string
		scaleResult        ScaleResult
		specRange          specRange
		expectedCurr       string
		expectedNew        string
		expectedChange     bool
		expectedSkipReason string
	}{
		{
			name: "frozen window active",
			annotations: map[string]string{
				consts.AnnotationKeyO11ySpecUpdatedTimestamp: strconv.Itoa(int(now.Unix())),
				consts.AnnotationKeyO11yDesiredSpec:          "default",
			},
			scaleResult: ScaleResult{
				ActionResult: ActionResult{Action: consts.ScaleActionUp},
			},
			specRange:          defaultRange,
			expectedCurr:       "default",
			expectedNew:        "",
			expectedChange:     false,
			expectedSkipReason: skipReasonFrozen,
		},
		{
			name: "frozen window passed, upgrade",
			annotations: map[string]string{
				consts.AnnotationKeyO11ySpecUpdatedTimestamp: strconv.Itoa(int(now.Add(-6 * time.Minute).Unix())),
				consts.AnnotationKeyO11yDesiredSpec:          "default",
			},
			scaleResult: ScaleResult{
				ActionResult: ActionResult{Action: consts.ScaleActionUp},
			},
			specRange:          defaultRange,
			expectedCurr:       "default",
			expectedNew:        "medium",
			expectedChange:     true,
			expectedSkipReason: "",
		},
		{
			name: "frozen window passed, downgrade",
			annotations: map[string]string{
				consts.AnnotationKeyO11ySpecUpdatedTimestamp: strconv.Itoa(int(now.Add(-6 * time.Minute).Unix())),
				consts.AnnotationKeyO11yDesiredSpec:          "medium",
			},
			scaleResult: ScaleResult{
				ActionResult: ActionResult{Action: consts.ScaleActionDown},
			},
			specRange:          defaultRange,
			expectedCurr:       "medium",
			expectedNew:        "default",
			expectedChange:     true,
			expectedSkipReason: "",
		},
		{
			name: "frozen window passed, no change (max reached)",
			annotations: map[string]string{
				consts.AnnotationKeyO11ySpecUpdatedTimestamp: strconv.Itoa(int(now.Add(-6 * time.Minute).Unix())),
				consts.AnnotationKeyO11yDesiredSpec:          "largex",
			},
			scaleResult: ScaleResult{
				ActionResult: ActionResult{Action: consts.ScaleActionUp},
			},
			specRange:          defaultRange,
			expectedCurr:       "largex",
			expectedNew:        "largex",
			expectedChange:     false,
			expectedSkipReason: skipReasonMaxSpec,
		},
		{
			name: "frozen window passed, no change (min reached)",
			annotations: map[string]string{
				consts.AnnotationKeyO11ySpecUpdatedTimestamp: strconv.Itoa(int(now.Add(-6 * time.Minute).Unix())),
				consts.AnnotationKeyO11yDesiredSpec:          "default",
			},
			scaleResult: ScaleResult{
				ActionResult: ActionResult{Action: consts.ScaleActionDown},
			},
			specRange:          defaultRange,
			expectedCurr:       "default",
			expectedNew:        "default",
			expectedChange:     false,
			expectedSkipReason: skipReasonMinSpec,
		},
		{
			name: "missing timestamp treated as ready",
			annotations: map[string]string{
				consts.AnnotationKeyO11yDesiredSpec: "default",
			},
			scaleResult: ScaleResult{
				ActionResult: ActionResult{Action: consts.ScaleActionUp},
			},
			specRange:          defaultRange,
			expectedCurr:       "default",
			expectedNew:        "medium",
			expectedChange:     true,
			expectedSkipReason: "",
		},
		{
			name: "explicit min/max constraints override specRange",
			annotations: map[string]string{
				consts.AnnotationKeyO11ySpecUpdatedTimestamp: strconv.Itoa(int(now.Add(-6 * time.Minute).Unix())),
				consts.AnnotationKeyO11yDesiredSpec:          "default",
				consts.AnnotationKeyO11yMinSpec:              "default",
				consts.AnnotationKeyO11yMaxSpec:              "default",
			},
			scaleResult: ScaleResult{
				ActionResult: ActionResult{Action: consts.ScaleActionUp},
			},
			specRange:          defaultRange,
			expectedCurr:       "default",
			expectedNew:        "default",
			expectedChange:     false,
			expectedSkipReason: skipReasonMaxSpec,
		},
		{
			name: "use specRange defaults when annotations missing",
			annotations: map[string]string{
				consts.AnnotationKeyO11ySpecUpdatedTimestamp: strconv.Itoa(int(now.Add(-6 * time.Minute).Unix())),
				consts.AnnotationKeyO11yDesiredSpec:          "default",
			},
			scaleResult: ScaleResult{
				ActionResult: ActionResult{Action: consts.ScaleActionUp},
			},
			specRange:          specRange{minSpec: "default", maxSpec: "default"},
			expectedCurr:       "default",
			expectedNew:        "default",
			expectedChange:     false,
			expectedSkipReason: skipReasonMaxSpec,
		},
		{
			name: "current spec missing defaults to default",
			annotations: map[string]string{
				consts.AnnotationKeyO11ySpecUpdatedTimestamp: strconv.Itoa(int(now.Add(-6 * time.Minute).Unix())),
			},
			scaleResult: ScaleResult{
				ActionResult: ActionResult{Action: consts.ScaleActionUp},
			},
			specRange:          defaultRange,
			expectedCurr:       "default",
			expectedNew:        "medium",
			expectedChange:     true,
			expectedSkipReason: "",
		},
	}

	specs := config.CustomerAgentSpecs{
		{Name: "default", MaxNodes: 5},
		{Name: "medium", MaxNodes: 50},
		{Name: "large", MaxNodes: 100},
		{Name: "largex", MaxNodes: 200},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			obj := &metav1.ObjectMeta{
				Annotations: tt.annotations,
			}
			curr, newSpec, changed, reason := decideSpecByScaleResult(obj, tt.scaleResult, specs, 300, tt.specRange, consts.ComponentVector)
			assert.Equal(t, tt.expectedCurr, curr)
			assert.Equal(t, tt.expectedNew, newSpec)
			assert.Equal(t, tt.expectedChange, changed)
			assert.Equal(t, tt.expectedSkipReason, reason)
		})
	}
}

func TestCombineSpec(t *testing.T) {
	tests := []struct {
		name        string
		annotations map[string]string
		newSpec     string
		component   string
		expected    string
	}{
		{
			name:        "empty desired spec - vmagent",
			annotations: map[string]string{},
			newSpec:     "new",
			component:   consts.ComponentVMAgent,
			expected:    "new/",
		},
		{
			name:        "empty desired spec - vector",
			annotations: map[string]string{},
			newSpec:     "new",
			component:   consts.ComponentVector,
			expected:    "/new",
		},
		{
			name: "two parts - update vmagent",
			annotations: map[string]string{
				consts.AnnotationKeyO11yDesiredSpec: "old/old",
			},
			newSpec:   "new",
			component: consts.ComponentVMAgent,
			expected:  "new/old",
		},
		{
			name: "two parts - update vector",
			annotations: map[string]string{
				consts.AnnotationKeyO11yDesiredSpec: "old/old",
			},
			newSpec:   "new",
			component: consts.ComponentVector,
			expected:  "old/new",
		},
		{
			name: "one part - update vmagent",
			annotations: map[string]string{
				consts.AnnotationKeyO11yDesiredSpec: "single",
			},
			newSpec:   "new",
			component: consts.ComponentVMAgent,
			expected:  "new/single",
		},
		{
			name: "one part - update vector",
			annotations: map[string]string{
				consts.AnnotationKeyO11yDesiredSpec: "single",
			},
			newSpec:   "new",
			component: consts.ComponentVector,
			expected:  "single/new",
		},
		{
			name: "invalid parts (too many) - update vmagent",
			annotations: map[string]string{
				consts.AnnotationKeyO11yDesiredSpec: "a/b/c",
			},
			newSpec:   "new",
			component: consts.ComponentVMAgent,
			expected:  "new",
		},
		{
			name: "invalid parts (too many) - update vector",
			annotations: map[string]string{
				consts.AnnotationKeyO11yDesiredSpec: "a/b/c",
			},
			newSpec:   "new",
			component: consts.ComponentVector,
			expected:  "new",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			obj := &metav1.ObjectMeta{
				Annotations: tt.annotations,
			}
			got := combineSpec(obj, tt.newSpec, tt.component)
			assert.Equal(t, tt.expected, got)
		})
	}
}
