package common

import (
	"context"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/gkeheterogeneous"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	scheme2 "github.com/tidbcloud/infra-provider/pkg/scheme"
)

func recordTestGKEComputeClassCleanupCandidate(t *testing.T, cluster *cloudv1.Cluster) string {
	t.Helper()
	bindings := []cloudv1.GKEComputeClassNodePoolBinding{
		{
			Role:         cloudv1.GKEComputeClassNodePoolRoleBase,
			InstanceType: "n2-standard-8",
			NodePoolName: gkeheterogeneous.NodePoolName(cluster, "n2-standard-8"),
		},
		{
			Role:         cloudv1.GKEComputeClassNodePoolRoleExtra,
			InstanceType: "n2-standard-64",
			NodePoolName: "extra-pool",
		},
	}
	computeClassName, err := cloudv1.GKEComputeClassNameForBindings(bindings)
	require.NoError(t, err)
	require.NoError(t, gkeheterogeneous.RecordComputeClassCleanupCandidate(cluster, computeClassName))
	return computeClassName
}

func testGKEComputeClassCleanupContext(cluster *cloudv1.Cluster, dataplaneObjs ...client.Object) *task.Context {
	return &task.Context{
		Cluster: cluster,
		Local:   fake.NewClientBuilder().WithScheme(scheme2.Scheme).WithObjects(cluster).Build(),
		EnvProvider: &testEnvProvider{
			client: fake.NewClientBuilder().WithObjects(dataplaneObjs...).Build(),
		},
	}
}

func testGKEComputeClassCleanup(ctx *task.Context) *GKEComputeClassCleanup {
	return &GKEComputeClassCleanup{
		BaseTask: &task.BaseTask{Context: ctx},
		sleep: func(context.Context, time.Duration) error {
			return nil
		},
	}
}

func testGatekeeperAssignForComputeClass(t *testing.T, name, computeClassName string) *unstructured.Unstructured {
	t.Helper()
	obj := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": "mutations.gatekeeper.sh/v1",
			"kind":       "Assign",
			"metadata": map[string]interface{}{
				"name": name,
			},
		},
	}
	require.NoError(t, unstructured.SetNestedField(obj.Object, computeClassName, "spec", "parameters", "assign", "value", consts.GKEComputeClassLabelKey))
	return obj
}

func TestGKEComputeClassCleanup_cleanupGKEComputeClassCandidatesSkipsWithoutCleanupAnnotation(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	cluster.UpdateAnnotation(cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, "not-json")

	ctx := testGKEComputeClassCleanupContext(cluster)
	cleanup := testGKEComputeClassCleanup(ctx)

	err := cleanup.cleanupGKEComputeClassCandidates(context.Background())
	assert.NoError(t, err)
}

func TestGKEComputeClassCleanup_CreateRequeuesWithoutTaskErrorWhenCleanupNeedsRetry(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	computeClassName := recordTestGKEComputeClassCleanupCandidate(t, cluster)

	ctx := testGKEComputeClassCleanupContext(cluster)
	cleanup := testGKEComputeClassCleanup(ctx)

	result := cleanup.Create(context.Background())
	assert.True(t, result.IsDoneState())
	assert.NoError(t, result.Error())
	assert.True(t, k8s.RequeueOnly(result.RawError()))
	requeue := k8s.ShouldRequeue(result.RawError())
	assert.True(t, requeue.Requeue)
	assert.Equal(t, gkeComputeClassCleanupRequeueAfter, requeue.RequeueAfter)

	candidates, err := gkeheterogeneous.ComputeClassCleanupCandidates(cluster)
	require.NoError(t, err)
	require.Equal(t, []string{computeClassName}, candidates)
}

func TestGKEComputeClassCleanup_CreateRecoversPanic(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	computeClassName := recordTestGKEComputeClassCleanupCandidate(t, cluster)

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		assert.Equal(t, http.MethodDelete, r.Method)
		assert.Equal(t, "/cluster-agent/tidbcluster-a/api/v1/clusters/cluster-a/gkeHeterogeneous/computeClasses/"+computeClassName, r.URL.Path)
		w.WriteHeader(http.StatusInternalServerError)
		_, _ = w.Write([]byte("temporary failure"))
	}))
	defer server.Close()
	cluster.Status.Gateway = &cloudv1.GatewayStatus{Endpoint: server.URL}

	ctx := testGKEComputeClassCleanupContext(cluster)
	cleanup := &GKEComputeClassCleanup{
		BaseTask: &task.BaseTask{Context: ctx},
		sleep: func(context.Context, time.Duration) error {
			panic("sleep panic")
		},
	}

	var result deploy.Result
	assert.NotPanics(t, func() {
		result = cleanup.Create(context.Background())
	})
	assert.True(t, result.IsDoneState())
	assert.NoError(t, result.Error())
	assert.NoError(t, result.RawError())
}

func TestGKEComputeClassCleanup_cleanupGKEComputeClassCandidatesRetriesDeleteFailureBeforeDroppingCandidate(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	computeClassName := recordTestGKEComputeClassCleanupCandidate(t, cluster)

	deleteCalls := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		deleteCalls++
		assert.Equal(t, http.MethodDelete, r.Method)
		assert.Equal(t, "/cluster-agent/tidbcluster-a/api/v1/clusters/cluster-a/gkeHeterogeneous/computeClasses/"+computeClassName, r.URL.Path)
		w.WriteHeader(http.StatusPreconditionFailed)
		_, _ = w.Write([]byte("still referenced"))
	}))
	defer server.Close()
	cluster.Status.Gateway = &cloudv1.GatewayStatus{Endpoint: server.URL}

	ctx := testGKEComputeClassCleanupContext(cluster)
	cleanup := testGKEComputeClassCleanup(ctx)

	err := cleanup.cleanupGKEComputeClassCandidates(context.Background())
	assert.NoError(t, err)
	assert.Equal(t, gkeComputeClassCleanupDeleteMaxAttempts, deleteCalls)

	candidates, err := gkeheterogeneous.ComputeClassCleanupCandidates(cluster)
	require.NoError(t, err)
	require.Empty(t, candidates)
}

func TestGKEComputeClassCleanup_cleanupGKEComputeClassCandidatesStopsAfterRetrySucceeds(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	computeClassName := recordTestGKEComputeClassCleanupCandidate(t, cluster)

	deleteCalls := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		deleteCalls++
		assert.Equal(t, http.MethodDelete, r.Method)
		assert.Equal(t, "/cluster-agent/tidbcluster-a/api/v1/clusters/cluster-a/gkeHeterogeneous/computeClasses/"+computeClassName, r.URL.Path)
		if deleteCalls < gkeComputeClassCleanupDeleteMaxAttempts {
			w.WriteHeader(http.StatusInternalServerError)
			_, _ = w.Write([]byte("temporary failure"))
			return
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()
	cluster.Status.Gateway = &cloudv1.GatewayStatus{Endpoint: server.URL}

	ctx := testGKEComputeClassCleanupContext(cluster)
	sleepCalls := 0
	cleanup := &GKEComputeClassCleanup{
		BaseTask: &task.BaseTask{Context: ctx},
		sleep: func(context.Context, time.Duration) error {
			sleepCalls++
			return nil
		},
	}

	err := cleanup.cleanupGKEComputeClassCandidates(context.Background())
	assert.NoError(t, err)
	assert.Equal(t, gkeComputeClassCleanupDeleteMaxAttempts, deleteCalls)
	assert.Equal(t, gkeComputeClassCleanupDeleteMaxAttempts-1, sleepCalls)

	candidates, err := gkeheterogeneous.ComputeClassCleanupCandidates(cluster)
	require.NoError(t, err)
	require.Empty(t, candidates)
}

func TestGKEComputeClassCleanup_cleanupGKEComputeClassCandidatesDropsCandidateAfterUnmanagedDeleteRejection(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	computeClassName := recordTestGKEComputeClassCleanupCandidate(t, cluster)

	deleteCalls := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		deleteCalls++
		assert.Equal(t, http.MethodDelete, r.Method)
		assert.Equal(t, "/cluster-agent/tidbcluster-a/api/v1/clusters/cluster-a/gkeHeterogeneous/computeClasses/"+computeClassName, r.URL.Path)
		w.WriteHeader(http.StatusForbidden)
		_, _ = w.Write([]byte("compute class " + computeClassName + " is not managed by binding-scoped gke heterogeneous deploy: missing labels"))
	}))
	defer server.Close()
	cluster.Status.Gateway = &cloudv1.GatewayStatus{Endpoint: server.URL}

	ctx := testGKEComputeClassCleanupContext(cluster)
	cleanup := testGKEComputeClassCleanup(ctx)

	err := cleanup.cleanupGKEComputeClassCandidates(context.Background())
	assert.NoError(t, err)
	assert.Equal(t, gkeComputeClassCleanupDeleteMaxAttempts, deleteCalls)

	candidates, err := gkeheterogeneous.ComputeClassCleanupCandidates(cluster)
	require.NoError(t, err)
	require.Empty(t, candidates)
}

func TestGKEComputeClassCleanup_cleanupGKEComputeClassCandidatesRequeuesWhenAgentUnavailable(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	computeClassName := recordTestGKEComputeClassCleanupCandidate(t, cluster)

	ctx := testGKEComputeClassCleanupContext(cluster)
	cleanup := testGKEComputeClassCleanup(ctx)

	err := cleanup.cleanupGKEComputeClassCandidates(context.Background())
	assert.ErrorContains(t, err, "clusterGateway endpoint is empty")
	requeue := k8s.ShouldRequeue(err)
	assert.True(t, requeue.Requeue)
	assert.Equal(t, gkeComputeClassCleanupRequeueAfter, requeue.RequeueAfter)

	candidates, err := gkeheterogeneous.ComputeClassCleanupCandidates(cluster)
	require.NoError(t, err)
	require.Len(t, candidates, 1)
	assert.Equal(t, computeClassName, candidates[0])
}

func TestGKEComputeClassCleanup_cleanupGKEComputeClassCandidatesDropsCandidateWhenIntentStillReferencesComputeClass(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	computeClassName := updateTestGKEComputeClassBinding(
		t,
		cluster,
		cloudv1.AnnoKeyGKEComputeClassForTiKVBindings,
		"",
		[]string{"n2-standard-8", "n2-standard-64"},
		[]string{gkeheterogeneous.NodePoolName(cluster, "n2-standard-8"), "extra-pool"},
	)
	require.NoError(t, gkeheterogeneous.RecordComputeClassCleanupCandidate(cluster, computeClassName))

	ctx := testGKEComputeClassCleanupContext(cluster)
	ctx.EnvProvider = nil
	cleanup := testGKEComputeClassCleanup(ctx)

	err := cleanup.cleanupGKEComputeClassCandidates(context.Background())
	assert.NoError(t, err)

	candidates, err := gkeheterogeneous.ComputeClassCleanupCandidates(cluster)
	require.NoError(t, err)
	require.Empty(t, candidates)
}

func TestGKEComputeClassCleanup_cleanupGKEComputeClassCandidatesDropsCandidateWhenNodePoolReferencesComputeClass(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	computeClassName := recordTestGKEComputeClassCleanupCandidate(t, cluster)

	deleteCalls := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		deleteCalls++
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()
	cluster.Status.Gateway = &cloudv1.GatewayStatus{Endpoint: server.URL}

	ctx := testGKEComputeClassCleanupContext(cluster)
	existing := existingSharedComputeClassTestNodePool(cluster, "extra-pool", "n2-standard-64", computeClassName)
	markTestAPIManagedGKEHeterogeneousNodePool(existing)
	require.NoError(t, ctx.Local.Create(context.Background(), existing))
	cleanup := testGKEComputeClassCleanup(ctx)

	err := cleanup.cleanupGKEComputeClassCandidates(context.Background())
	assert.NoError(t, err)
	assert.Equal(t, 0, deleteCalls)

	candidates, err := gkeheterogeneous.ComputeClassCleanupCandidates(cluster)
	require.NoError(t, err)
	require.Empty(t, candidates)
}

func TestGKEComputeClassCleanup_cleanupGKEComputeClassCandidatesIgnoresOrdinaryNodePoolComputeClassMetadata(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	computeClassName := recordTestGKEComputeClassCleanupCandidate(t, cluster)

	deleteCalls := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		deleteCalls++
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()
	cluster.Status.Gateway = &cloudv1.GatewayStatus{Endpoint: server.URL}

	ctx := testGKEComputeClassCleanupContext(cluster)
	require.NoError(t, ctx.Local.Create(context.Background(), existingSharedComputeClassTestNodePool(cluster, "ordinary-pool", "n2-standard-64", computeClassName)))
	cleanup := testGKEComputeClassCleanup(ctx)

	err := cleanup.cleanupGKEComputeClassCandidates(context.Background())
	assert.NoError(t, err)
	assert.Equal(t, 1, deleteCalls)

	candidates, err := gkeheterogeneous.ComputeClassCleanupCandidates(cluster)
	require.NoError(t, err)
	require.Empty(t, candidates)
}

func TestGKEComputeClassCleanup_cleanupGKEComputeClassCandidatesDropsCandidateWhenGatekeeperAssignReferencesComputeClass(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	computeClassName := recordTestGKEComputeClassCleanupCandidate(t, cluster)

	deleteCalls := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		deleteCalls++
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()
	cluster.Status.Gateway = &cloudv1.GatewayStatus{Endpoint: server.URL}

	ctx := testGKEComputeClassCleanupContext(cluster, testGatekeeperAssignForComputeClass(t, "assign-a", computeClassName))
	cleanup := testGKEComputeClassCleanup(ctx)

	err := cleanup.cleanupGKEComputeClassCandidates(context.Background())
	assert.NoError(t, err)
	assert.Equal(t, 0, deleteCalls)

	candidates, err := gkeheterogeneous.ComputeClassCleanupCandidates(cluster)
	require.NoError(t, err)
	require.Empty(t, candidates)
}

func TestGKEComputeClassCleanup_cleanupGKEComputeClassCandidatesDropsCandidateAfterBadRequestStillReferenced(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	computeClassName := recordTestGKEComputeClassCleanupCandidate(t, cluster)

	deleteCalls := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		deleteCalls++
		assert.Equal(t, http.MethodDelete, r.Method)
		assert.Equal(t, "/cluster-agent/tidbcluster-a/api/v1/clusters/cluster-a/gkeHeterogeneous/computeClasses/"+computeClassName, r.URL.Path)
		w.WriteHeader(http.StatusBadRequest)
		_, _ = w.Write([]byte(`{"code":9,"message":"compute class ` + computeClassName + ` is still referenced by gatekeeper assign assign-a"}`))
	}))
	defer server.Close()
	cluster.Status.Gateway = &cloudv1.GatewayStatus{Endpoint: server.URL}

	ctx := testGKEComputeClassCleanupContext(cluster)
	cleanup := testGKEComputeClassCleanup(ctx)

	err := cleanup.cleanupGKEComputeClassCandidates(context.Background())
	assert.NoError(t, err)
	assert.Equal(t, gkeComputeClassCleanupDeleteMaxAttempts, deleteCalls)

	candidates, err := gkeheterogeneous.ComputeClassCleanupCandidates(cluster)
	require.NoError(t, err)
	require.Empty(t, candidates)
}

func TestGKEComputeClassCleanup_cleanupGKEComputeClassCandidatesClearsProcessedCandidatesOnce(t *testing.T) {
	cluster := testGKESharedScopeCluster("cluster-a")
	referencedComputeClass := updateTestGKEComputeClassBinding(
		t,
		cluster,
		cloudv1.AnnoKeyGKEComputeClassForTiKVBindings,
		"",
		[]string{"n2-standard-8", "n2-standard-64"},
		[]string{gkeheterogeneous.NodePoolName(cluster, "n2-standard-8"), "extra-pool"},
	)
	orphanComputeClass := "orphan-priority"
	require.NoError(t, gkeheterogeneous.RecordComputeClassCleanupCandidate(cluster, referencedComputeClass))
	require.NoError(t, gkeheterogeneous.RecordComputeClassCleanupCandidate(cluster, orphanComputeClass))

	deleteCalls := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		deleteCalls++
		assert.Equal(t, http.MethodDelete, r.Method)
		assert.Equal(t, "/cluster-agent/tidbcluster-a/api/v1/clusters/cluster-a/gkeHeterogeneous/computeClasses/"+orphanComputeClass, r.URL.Path)
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()
	cluster.Status.Gateway = &cloudv1.GatewayStatus{Endpoint: server.URL}

	ctx := testGKEComputeClassCleanupContext(cluster)
	cleanup := testGKEComputeClassCleanup(ctx)

	err := cleanup.cleanupGKEComputeClassCandidates(context.Background())
	assert.NoError(t, err)
	assert.Equal(t, 1, deleteCalls)

	candidates, err := gkeheterogeneous.ComputeClassCleanupCandidates(cluster)
	require.NoError(t, err)
	require.Empty(t, candidates)
}
