package discovery

import (
	"context"
	"testing"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/cache/informertest"

	"github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/api/v1alpha1"
	"github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/target"
)

func TestDiscoveryWatchesAndClassifiesAutoscalerTargets(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	scheme := runtime.NewScheme()
	if err := v1alpha1.AddToScheme(scheme); err != nil {
		t.Fatal(err)
	}
	informers := &informertest.FakeInformers{Scheme: scheme}
	discovered, err := New(&Config{Informers: informers})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	if !discovered.NeedLeaderElection() {
		t.Fatal("discovery must require leader election")
	}
	informer, err := informers.FakeInformerFor(ctx, &v1alpha1.Autoscaler{})
	if err != nil {
		t.Fatal(err)
	}
	done := make(chan error, 1)
	go func() { done <- discovered.Start(ctx) }()

	tidb := testAutoscaler("tidb", "dp-x", "TiDBGroup", "tidb")
	tidbDuplicate := testAutoscaler("tidb-copy", "dp-x", "TiDBGroup", "tidb")
	tikv := testAutoscaler("tikv", "dp-x", "TiKVGroup", "tikv")
	waitFor(t, func() bool {
		informer.Add(tidb)
		informer.Add(tidbDuplicate)
		informer.Add(tikv)
		return len(discovered.Targets(AllKinds)) == 2
	}, "initial classified targets")
	waitForTargets(t, discovered, "TiDBGroup", 1)
	waitForTargets(t, discovered, "TiKVGroup", 1)
	waitForTargets(t, discovered, "PDGroup", 0)

	targets := discovered.Targets("TiDBGroup")
	if targets[0].Namespace() != "dp-x" || targets[0].Labels()[target.LabelGroup] != "tidb" {
		t.Fatalf("TiDB targets = %+v", targets)
	}
	updated := tidb.DeepCopy()
	updated.Spec.TargetRef.Name = "tidb-next"
	informer.Update(tidb, updated)
	waitFor(t, func() bool {
		current := discovered.Targets("TiDBGroup")
		return len(current) == 2
	}, "updated target")
	informer.Delete(tidbDuplicate)
	waitForTargets(t, discovered, "TiDBGroup", 1)
	if got := discovered.Targets("TiDBGroup")[0].Labels()[target.LabelGroup]; got != "tidb-next" {
		t.Fatalf("remaining TiDB target group = %q, want tidb-next", got)
	}

	cancel()
	select {
	case err := <-done:
		if err != nil {
			t.Fatalf("Start() error = %v", err)
		}
	case <-time.After(time.Second):
		t.Fatal("Start() did not stop")
	}
}

func TestNewRejectsInvalidConfig(t *testing.T) {
	if _, err := New(nil); err == nil {
		t.Fatal("nil config must be rejected")
	}
	if _, err := New(&Config{}); err == nil {
		t.Fatal("missing informers must be rejected")
	}
}

func waitForTargets(t *testing.T, discovered Discovery, kind string, count int) {
	t.Helper()
	waitFor(t, func() bool { return len(discovered.Targets(kind)) == count }, "target count")
}

func waitFor(t *testing.T, condition func() bool, description string) {
	t.Helper()
	deadline := time.Now().Add(time.Second)
	for time.Now().Before(deadline) {
		if condition() {
			return
		}
		time.Sleep(time.Millisecond)
	}
	t.Fatalf("timed out waiting for %s", description)
}

func testAutoscaler(name, namespace, kind, targetName string) *v1alpha1.Autoscaler {
	return &v1alpha1.Autoscaler{
		ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
		Spec: v1alpha1.AutoscalerSpec{TargetRef: v1alpha1.CrossVersionObjectReference{
			APIVersion: "core.pingcap.com/v1alpha1",
			Kind:       kind,
			Name:       targetName,
		}},
	}
}
