package nodepoolgc

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
)

func testGCConfig() config.NodePoolGCConfig {
	return configWithDefaults(config.NodePoolGCConfig{})
}

func metadataObject(annotations map[string]string, managed bool) *unstructured.Unstructured {
	obj := &unstructured.Unstructured{}
	obj.SetName("np-test")
	obj.SetAnnotations(annotations)
	if managed {
		obj.SetLabels(map[string]string{
			consts.LabelKeyNodePoolGCManaged: consts.AnnoValTrue,
		})
	}
	return obj
}

func TestUpdateGCMetadataStateMachine(t *testing.T) {
	now := time.Date(2026, 7, 10, 8, 0, 0, 0, time.UTC)
	old := now.Add(-15 * 24 * time.Hour).Format(time.RFC3339)
	fresh := now.Add(-time.Hour).Format(time.RFC3339)
	waiting := now.Add(-time.Hour).Format(time.RFC3339)
	ready := now.Add(-13 * time.Hour).Format(time.RFC3339)

	tests := []struct {
		name                string
		managed             bool
		autoApproveDeletion bool
		annotations         map[string]string
		wantStatus          Status
		wantExpiredAt       string
		wantApprovedAt      string
		wantErrKey          string
		wantLastUsed        bool
	}{
		{
			name:         "adopt legacy pool",
			wantStatus:   StatusNotExpired,
			wantLastUsed: true,
		},
		{
			name:         "adopt managed pool without usage metadata",
			managed:      true,
			wantStatus:   StatusNotExpired,
			wantLastUsed: true,
		},
		{
			name:    "fresh pool clears expiration mark",
			managed: true,
			annotations: map[string]string{
				consts.AnnoKeyNodePoolLastUsedTime: fresh,
				consts.AnnoKeyNodePoolGCExpiredAt:  waiting,
			},
			wantStatus: StatusNotExpired,
		},
		{
			name:    "expired pool is marked",
			managed: true,
			annotations: map[string]string{
				consts.AnnoKeyNodePoolLastUsedTime: old,
			},
			wantStatus:    StatusExpired,
			wantExpiredAt: now.Format(time.RFC3339),
		},
		{
			name:    "expired pool waits after mark",
			managed: true,
			annotations: map[string]string{
				consts.AnnoKeyNodePoolLastUsedTime: old,
				consts.AnnoKeyNodePoolGCExpiredAt:  waiting,
			},
			wantStatus:    StatusExpired,
			wantExpiredAt: waiting,
		},
		{
			name:                "expired pool is automatically approved when enabled",
			managed:             true,
			autoApproveDeletion: true,
			annotations: map[string]string{
				consts.AnnoKeyNodePoolLastUsedTime: old,
				consts.AnnoKeyNodePoolGCExpiredAt:  ready,
			},
			wantStatus:     StatusDeletionApproved,
			wantExpiredAt:  ready,
			wantApprovedAt: now.Format(time.RFC3339),
		},
		{
			name:    "expired pool stays unapproved when automatic approval is disabled",
			managed: true,
			annotations: map[string]string{
				consts.AnnoKeyNodePoolLastUsedTime: old,
				consts.AnnoKeyNodePoolGCExpiredAt:  ready,
			},
			wantStatus:    StatusExpired,
			wantExpiredAt: ready,
		},
		{
			name:    "manual approval is honored when automatic deletion is disabled",
			managed: true,
			annotations: map[string]string{
				consts.AnnoKeyNodePoolLastUsedTime: old,
				consts.AnnoKeyNodePoolGCApprovedAt: ready,
			},
			wantStatus:     StatusDeletionApproved,
			wantExpiredAt:  now.Format(time.RFC3339),
			wantApprovedAt: ready,
		},
		{
			name:    "invalid last used timestamp",
			managed: true,
			annotations: map[string]string{
				consts.AnnoKeyNodePoolLastUsedTime: "bad-time",
			},
			wantStatus: StatusFailed,
			wantErrKey: consts.AnnoKeyNodePoolLastUsedTime,
		},
		{
			name:    "invalid expired timestamp",
			managed: true,
			annotations: map[string]string{
				consts.AnnoKeyNodePoolLastUsedTime: old,
				consts.AnnoKeyNodePoolGCExpiredAt:  "bad-time",
			},
			wantStatus:    StatusFailed,
			wantExpiredAt: "bad-time",
			wantErrKey:    consts.AnnoKeyNodePoolGCExpiredAt,
		},
		{
			name:    "invalid approved timestamp",
			managed: true,
			annotations: map[string]string{
				consts.AnnoKeyNodePoolLastUsedTime: old,
				consts.AnnoKeyNodePoolGCApprovedAt: "bad-time",
			},
			wantStatus:     StatusFailed,
			wantExpiredAt:  now.Format(time.RFC3339),
			wantApprovedAt: "bad-time",
			wantErrKey:     consts.AnnoKeyNodePoolGCApprovedAt,
		},
		{
			name:    "last scan is observability only",
			managed: true,
			annotations: map[string]string{
				consts.AnnoKeyNodePoolLastUsedTime: fresh,
				consts.AnnoKeyNodePoolGCLastScanAt: "bad-time",
			},
			wantStatus: StatusNotExpired,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			obj := metadataObject(tt.annotations, tt.managed)

			cfg := testGCConfig()
			cfg.AutoApproveDeletion = tt.autoApproveDeletion
			status, err := updateGCMetadata(now, obj, cfg)

			require.Equal(t, tt.wantStatus, status)
			if tt.wantErrKey == "" {
				require.NoError(t, err)
			} else {
				require.ErrorContains(t, err, tt.wantErrKey)
			}
			require.Equal(t, now.Format(time.RFC3339), obj.GetAnnotations()[consts.AnnoKeyNodePoolGCLastScanAt])
			if tt.wantLastUsed {
				require.Equal(t, now.Format(time.RFC3339), obj.GetAnnotations()[consts.AnnoKeyNodePoolLastUsedTime])
			}
			if !tt.managed {
				require.Equal(t, consts.AnnoValTrue, obj.GetLabels()[consts.LabelKeyNodePoolGCManaged])
			}
			require.Equal(t, tt.wantExpiredAt, obj.GetAnnotations()[consts.AnnoKeyNodePoolGCExpiredAt])
			require.Equal(t, tt.wantApprovedAt, obj.GetAnnotations()[consts.AnnoKeyNodePoolGCApprovedAt])
		})
	}
}

func TestUpdateGCMetadataSkipsUnmanagedPool(t *testing.T) {
	now := time.Date(2026, 7, 10, 8, 0, 0, 0, time.UTC)
	lastUsed := now.Add(-15 * 24 * time.Hour).Format(time.RFC3339)
	expiredAt := now.Add(-time.Hour).Format(time.RFC3339)
	approvedAt := now.Add(-30 * time.Minute).Format(time.RFC3339)

	tests := []struct {
		name         string
		managedLabel string
	}{
		{
			name:         "false label",
			managedLabel: "false",
		},
		{
			name:         "arbitrary non-true label",
			managedLabel: "legacy-value",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			obj := metadataObject(map[string]string{
				consts.AnnoKeyNodePoolLastUsedTime: lastUsed,
				consts.AnnoKeyNodePoolGCExpiredAt:  expiredAt,
				consts.AnnoKeyNodePoolGCApprovedAt: approvedAt,
			}, false)
			obj.SetLabels(map[string]string{
				consts.LabelKeyNodePoolGCManaged: tt.managedLabel,
			})

			status, err := updateGCMetadata(now, obj, testGCConfig())

			require.NoError(t, err)
			require.Equal(t, StatusNotExpired, status)
			require.Equal(t, tt.managedLabel, obj.GetLabels()[consts.LabelKeyNodePoolGCManaged])
			require.Equal(t, map[string]string{
				consts.AnnoKeyNodePoolLastUsedTime: lastUsed,
				consts.AnnoKeyNodePoolGCExpiredAt:  expiredAt,
				consts.AnnoKeyNodePoolGCApprovedAt: approvedAt,
				consts.AnnoKeyNodePoolGCLastScanAt: now.Format(time.RFC3339),
			}, obj.GetAnnotations())
		})
	}
}
