/*
 * Copyright (c) 2026 Huawei Technologies Co., Ltd.
 * openFuyao is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */

package node

import (
	"context"
	"testing"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	warmupv1alpha1 "github.com/openfuyao/weight-dispatcher/api/v1alpha1"
)

func TestResolverResolveExplicitNodeNamesDeterministically(t *testing.T) {
	t.Parallel()

	resolver := NewResolver(fakeClient(t,
		nodeFixture("node-b", nil, "10.0.0.2"),
		nodeFixture("node-a", nil, "10.0.0.1"),
	))

	nodes, err := resolver.Resolve(context.Background(), warmupv1alpha1.WarmupTargetSpec{
		NodeNames: []string{"node-b", "node-a"},
	})
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}
	if len(nodes) != 2 || nodes[0].Name != "node-a" || nodes[1].Name != "node-b" {
		t.Fatalf("expected sorted explicit nodes, got %+v", nodes)
	}
}

func TestResolverResolveSelectorReturnsSortedNodes(t *testing.T) {
	t.Parallel()

	resolver := NewResolver(fakeClient(t,
		nodeFixture("node-c", map[string]string{"role": "worker"}, "10.0.0.3"),
		nodeFixture("node-a", map[string]string{"role": "worker"}, "10.0.0.1"),
		nodeFixture("node-b", map[string]string{"role": "infra"}, "10.0.0.2"),
	))

	nodes, err := resolver.Resolve(context.Background(), warmupv1alpha1.WarmupTargetSpec{
		NodeSelector: map[string]string{"role": "worker"},
	})
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}
	if len(nodes) != 2 || nodes[0].Name != "node-a" || nodes[1].Name != "node-c" {
		t.Fatalf("expected sorted selector nodes, got %+v", nodes)
	}
}

func TestResolverGetNodeWrapsMissingNodeError(t *testing.T) {
	t.Parallel()

	_, err := NewResolver(fakeClient(t)).GetNode(context.Background(), "missing")
	if err == nil {
		t.Fatalf("expected missing node to fail")
	}
}

func TestExtractNodeInternalIPPrefersInternalThenFallback(t *testing.T) {
	t.Parallel()

	internal := nodeFixture("node-a", nil, "10.0.0.1")
	internal.Status.Addresses = append([]corev1.NodeAddress{{
		Type:    corev1.NodeExternalIP,
		Address: "1.1.1.1",
	}}, internal.Status.Addresses...)

	ip, err := ExtractNodeInternalIP(internal)
	if err != nil {
		t.Fatalf("ExtractNodeInternalIP returned error: %v", err)
	}
	if ip != "10.0.0.1" {
		t.Fatalf("expected InternalIP to win, got %q", ip)
	}

	fallback := &corev1.Node{
		ObjectMeta: metav1.ObjectMeta{Name: "node-b"},
		Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{
			Type:    corev1.NodeHostName,
			Address: "node-b.local",
		}}},
	}
	ip, err = ExtractNodeInternalIP(fallback)
	if err != nil {
		t.Fatalf("ExtractNodeInternalIP fallback returned error: %v", err)
	}
	if ip != "node-b.local" {
		t.Fatalf("expected fallback address, got %q", ip)
	}

	if _, err := ExtractNodeInternalIP(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node-c"}}); err == nil {
		t.Fatalf("expected node without addresses to fail")
	}
}

func fakeClient(t *testing.T, nodes ...*corev1.Node) client.Client {
	t.Helper()

	scheme := runtime.NewScheme()
	if err := corev1.AddToScheme(scheme); err != nil {
		t.Fatalf("AddToScheme returned error: %v", err)
	}
	builder := fake.NewClientBuilder().WithScheme(scheme)
	for _, node := range nodes {
		builder = builder.WithObjects(node)
	}
	return builder.Build()
}

func nodeFixture(name string, labels map[string]string, internalIP string) *corev1.Node {
	return &corev1.Node{
		ObjectMeta: metav1.ObjectMeta{Name: name, Labels: labels},
		Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{
			Type:    corev1.NodeInternalIP,
			Address: internalIP,
		}}},
	}
}