// Copyright 2025 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controllers

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/intstr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	mcpv1beta1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1beta1"
	ctrlutil "github.com/stacklok/toolhive/cmd/thv-operator/pkg/controllerutil"
	"github.com/stacklok/toolhive/cmd/thv-operator/pkg/runconfig/configmap/checksum"
	"github.com/stacklok/toolhive/cmd/thv-operator/pkg/virtualmcpserverstatus"
	vmcpconfig "github.com/stacklok/toolhive/pkg/vmcp/config"
	"github.com/stacklok/toolhive/pkg/vmcp/workloads"
)

const (
	testChecksumValue = "test-checksum-123"
	testVmcpName      = "test-vmcp"
)

// TestVirtualMCPServerValidateGroupRef tests the GroupRef validation
func TestVirtualMCPServerValidateGroupRef(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name           string
		vmcp           *mcpv1beta1.VirtualMCPServer
		mcpGroup       *mcpv1beta1.MCPGroup
		mcpServers     []mcpv1beta1.MCPServer
		expectError    bool
		expectedPhase  mcpv1beta1.VirtualMCPServerPhase
		expectedReason string
	}{
		{
			name: "valid group ref with ready group",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
				},
			},
			mcpGroup: &mcpv1beta1.MCPGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testGroupName,
					Namespace: "default",
				},
				Status: mcpv1beta1.MCPGroupStatus{
					Phase:   mcpv1beta1.MCPGroupPhaseReady,
					Servers: []string{"backend-1", "backend-2"},
				},
			},
			mcpServers: []mcpv1beta1.MCPServer{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "backend-1",
						Namespace: "default",
					},
					Status: mcpv1beta1.MCPServerStatus{
						Phase: mcpv1beta1.MCPServerPhaseReady,
						URL:   "http://backend-1.default.svc.cluster.local:8080",
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "backend-2",
						Namespace: "default",
					},
					Status: mcpv1beta1.MCPServerStatus{
						Phase: mcpv1beta1.MCPServerPhaseReady,
						URL:   "http://backend-2.default.svc.cluster.local:8080",
					},
				},
			},
			expectError:    false,
			expectedReason: mcpv1beta1.ConditionReasonVirtualMCPServerGroupRefValid,
		},
		{
			name: "group ref not found",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: "missing-group"},
				},
			},
			expectError:    true,
			expectedPhase:  mcpv1beta1.VirtualMCPServerPhaseFailed,
			expectedReason: mcpv1beta1.ConditionReasonVirtualMCPServerGroupRefNotFound,
		},
		{
			name: "group ref not ready",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: "pending-group"},
				},
			},
			mcpGroup: &mcpv1beta1.MCPGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "pending-group",
					Namespace: "default",
				},
				Status: mcpv1beta1.MCPGroupStatus{
					Phase: mcpv1beta1.MCPGroupPhasePending,
				},
			},
			expectError:    true,
			expectedPhase:  mcpv1beta1.VirtualMCPServerPhasePending,
			expectedReason: mcpv1beta1.ConditionReasonVirtualMCPServerGroupRefNotReady,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			// Setup fake client with resources
			scheme := runtime.NewScheme()
			_ = mcpv1beta1.AddToScheme(scheme)
			_ = corev1.AddToScheme(scheme)
			_ = appsv1.AddToScheme(scheme)
			_ = rbacv1.AddToScheme(scheme)

			objs := []client.Object{tt.vmcp}
			if tt.mcpGroup != nil {
				objs = append(objs, tt.mcpGroup)
			}
			for i := range tt.mcpServers {
				objs = append(objs, &tt.mcpServers[i])
			}

			fakeClient := fake.NewClientBuilder().
				WithScheme(scheme).
				WithObjects(objs...).
				WithStatusSubresource(&mcpv1beta1.VirtualMCPServer{}).
				Build()

			r := &VirtualMCPServerReconciler{
				Client:           fakeClient,
				Scheme:           scheme,
				PlatformDetector: ctrlutil.NewSharedPlatformDetector(),
			}

			statusManager := virtualmcpserverstatus.NewStatusManager(tt.vmcp)
			err := r.validateGroupRef(context.Background(), tt.vmcp, statusManager)
			// Apply status updates for test assertions
			_ = statusManager.UpdateStatus(context.Background(), &tt.vmcp.Status)

			if tt.expectError {
				assert.Error(t, err)
				assert.Equal(t, tt.expectedPhase, tt.vmcp.Status.Phase)

				// Check condition reason
				for _, cond := range tt.vmcp.Status.Conditions {
					if cond.Type == mcpv1beta1.ConditionTypeVirtualMCPServerGroupRefValidated {
						assert.Equal(t, tt.expectedReason, cond.Reason)
					}
				}
			} else {
				assert.NoError(t, err)

				// Check condition is set to true
				foundCondition := false
				for _, cond := range tt.vmcp.Status.Conditions {
					if cond.Type == mcpv1beta1.ConditionTypeVirtualMCPServerGroupRefValidated {
						foundCondition = true
						assert.Equal(t, metav1.ConditionTrue, cond.Status)
						assert.Equal(t, tt.expectedReason, cond.Reason)
					}
				}
				assert.True(t, foundCondition, "GroupRefValidated condition should be set")
			}
		})
	}
}

// TestVirtualMCPServerEnsureRBACResources tests RBAC resource creation
func TestVirtualMCPServerEnsureRBACResources(t *testing.T) {
	t.Parallel()

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = rbacv1.AddToScheme(scheme)

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp).
		Build()

	r := &VirtualMCPServerReconciler{
		Client: fakeClient,
		Scheme: scheme,
	}

	err := r.ensureRBACResources(context.Background(), vmcp)
	require.NoError(t, err)

	// Verify ServiceAccount was created
	sa := &corev1.ServiceAccount{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      vmcpServiceAccountName(vmcp.Name),
		Namespace: vmcp.Namespace,
	}, sa)
	require.NoError(t, err)
	assert.Equal(t, vmcpServiceAccountName(vmcp.Name), sa.Name)

	// Verify Role was created
	role := &rbacv1.Role{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      vmcpServiceAccountName(vmcp.Name),
		Namespace: vmcp.Namespace,
	}, role)
	require.NoError(t, err)
	assert.Equal(t, vmcpServiceAccountName(vmcp.Name), role.Name)
	assert.NotEmpty(t, role.Rules)

	// Verify Role includes required ToolHive resources (mcpgroups, mcpservers, mcpremoteproxies, mcpexternalauthconfigs)
	var toolhiveRule *rbacv1.PolicyRule
	for i := range role.Rules {
		if len(role.Rules[i].APIGroups) > 0 && role.Rules[i].APIGroups[0] == "toolhive.stacklok.dev" {
			toolhiveRule = &role.Rules[i]
			break
		}
	}
	require.NotNil(t, toolhiveRule, "Role should have a rule for toolhive.stacklok.dev API group")
	assert.Contains(t, toolhiveRule.Resources, "mcpgroups", "Role should allow listing mcpgroups")
	assert.Contains(t, toolhiveRule.Resources, "mcpservers", "Role should allow listing mcpservers")
	assert.Contains(t, toolhiveRule.Resources, "mcpremoteproxies", "Role should allow listing mcpremoteproxies")
	assert.Contains(t, toolhiveRule.Resources, "mcpserverentries", "Role should allow listing mcpserverentries")
	assert.Contains(t, toolhiveRule.Resources, "mcpexternalauthconfigs", "Role should allow listing mcpexternalauthconfigs")

	// Verify RoleBinding was created
	rb := &rbacv1.RoleBinding{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      vmcpServiceAccountName(vmcp.Name),
		Namespace: vmcp.Namespace,
	}, rb)
	require.NoError(t, err)
	assert.Equal(t, vmcpServiceAccountName(vmcp.Name), rb.Name)
	assert.Equal(t, vmcpServiceAccountName(vmcp.Name), rb.RoleRef.Name)
	assert.Len(t, rb.Subjects, 1)
	assert.Equal(t, vmcpServiceAccountName(vmcp.Name), rb.Subjects[0].Name)
}

// TestVirtualMCPServerEnsureRBACResources_ImagePullSecrets verifies that
// spec.imagePullSecrets propagates to the operator-managed ServiceAccount.
func TestVirtualMCPServerEnsureRBACResources_ImagePullSecrets(t *testing.T) {
	t.Parallel()

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
			ImagePullSecrets: []corev1.LocalObjectReference{
				{Name: "vmcp-creds"},
				{Name: "extra-creds"},
			},
		},
	}

	scheme := runtime.NewScheme()
	require.NoError(t, mcpv1beta1.AddToScheme(scheme))
	require.NoError(t, corev1.AddToScheme(scheme))
	require.NoError(t, rbacv1.AddToScheme(scheme))

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp).
		Build()

	r := &VirtualMCPServerReconciler{
		Client: fakeClient,
		Scheme: scheme,
	}

	require.NoError(t, r.ensureRBACResources(t.Context(), vmcp))

	sa := &corev1.ServiceAccount{}
	require.NoError(t, fakeClient.Get(t.Context(), types.NamespacedName{
		Name:      vmcpServiceAccountName(vmcp.Name),
		Namespace: vmcp.Namespace,
	}, sa))

	expected := []corev1.LocalObjectReference{
		{Name: "vmcp-creds"},
		{Name: "extra-creds"},
	}
	assert.Equal(t, expected, sa.ImagePullSecrets)
}

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

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "update-vmcp",
			Namespace: "default",
			UID:       "test-uid",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: "test-group"},
		},
	}

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = rbacv1.AddToScheme(scheme)

	saName := vmcpServiceAccountName(vmcp.Name)

	// Pre-create RBAC resources with outdated rules
	existingSA := &corev1.ServiceAccount{
		ObjectMeta: metav1.ObjectMeta{
			Name:      saName,
			Namespace: vmcp.Namespace,
		},
	}
	existingRole := &rbacv1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name:      saName,
			Namespace: vmcp.Namespace,
		},
		Rules: []rbacv1.PolicyRule{
			{
				APIGroups: []string{""},
				Resources: []string{"pods"},
				Verbs:     []string{"get"},
			},
		},
	}
	existingRB := &rbacv1.RoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name:      saName,
			Namespace: vmcp.Namespace,
		},
		RoleRef: rbacv1.RoleRef{
			APIGroup: "rbac.authorization.k8s.io",
			Kind:     "Role",
			Name:     saName,
		},
		Subjects: []rbacv1.Subject{
			{
				Kind:      "ServiceAccount",
				Name:      saName,
				Namespace: vmcp.Namespace,
			},
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp, existingSA, existingRole, existingRB).
		Build()

	r := &VirtualMCPServerReconciler{
		Client: fakeClient,
		Scheme: scheme,
	}

	// Call ensureRBACResources - should update the Role with correct rules
	err := r.ensureRBACResources(context.Background(), vmcp)
	require.NoError(t, err)

	// Verify Role was updated with correct rules
	role := &rbacv1.Role{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      saName,
		Namespace: vmcp.Namespace,
	}, role)
	assert.NoError(t, err)
	assert.Equal(t, vmcpDiscoveredRBACRules, role.Rules, "Role should be updated with correct rules")
}

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

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "idempotent-vmcp",
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: "test-group"},
		},
	}

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = rbacv1.AddToScheme(scheme)

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp).
		Build()

	r := &VirtualMCPServerReconciler{
		Client: fakeClient,
		Scheme: scheme,
	}

	// Call ensureRBACResources multiple times
	for i := range 3 {
		err := r.ensureRBACResources(context.Background(), vmcp)
		require.NoError(t, err, "iteration %d should succeed", i)
	}

	saName := vmcpServiceAccountName(vmcp.Name)

	// Verify resources still exist with correct configuration
	sa := &corev1.ServiceAccount{}
	err := fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      saName,
		Namespace: vmcp.Namespace,
	}, sa)
	assert.NoError(t, err)

	role := &rbacv1.Role{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      saName,
		Namespace: vmcp.Namespace,
	}, role)
	assert.NoError(t, err)
	assert.Equal(t, vmcpDiscoveredRBACRules, role.Rules)

	rb := &rbacv1.RoleBinding{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      saName,
		Namespace: vmcp.Namespace,
	}, rb)
	assert.NoError(t, err)
}

// TestVirtualMCPServerEnsureRBACResources_InlineMode tests that inline mode uses
// minimal RBAC permissions (no secret/configmap access) for security
func TestVirtualMCPServerEnsureRBACResources_InlineMode(t *testing.T) {
	t.Parallel()

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "inline-mode-vmcp",
			Namespace: "default",
			UID:       "test-uid",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: "test-group"},
			OutgoingAuth: &mcpv1beta1.OutgoingAuthConfig{
				Source: "inline",
			},
		},
	}

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = rbacv1.AddToScheme(scheme)

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp).
		Build()

	r := &VirtualMCPServerReconciler{
		Client: fakeClient,
		Scheme: scheme,
	}

	// Call ensureRBACResources in inline mode
	err := r.ensureRBACResources(context.Background(), vmcp)
	require.NoError(t, err)

	// Verify Role was created with minimal permissions (inline mode)
	saName := vmcpServiceAccountName(vmcp.Name)
	role := &rbacv1.Role{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      saName,
		Namespace: vmcp.Namespace,
	}, role)
	assert.NoError(t, err, "Role should be created in inline mode")
	assert.Equal(t, vmcpInlineRBACRules, role.Rules, "Role should use minimal rules in inline mode")

	// Verify inline mode doesn't have secret/configmap access
	for _, rule := range role.Rules {
		for _, resource := range rule.Resources {
			assert.NotContains(t, resource, "secrets", "Inline mode should not have secret access")
			assert.NotContains(t, resource, "configmaps", "Inline mode should not have configmap access")
		}
	}

	// Verify inline mode still has status update permissions
	hasStatusPermission := false
	for _, rule := range role.Rules {
		for _, resource := range rule.Resources {
			if resource == "virtualmcpservers/status" {
				hasStatusPermission = true
				assert.Contains(t, rule.Verbs, "update", "Should have update permission for status")
				assert.Contains(t, rule.Verbs, "patch", "Should have patch permission for status")
			}
		}
	}
	assert.True(t, hasStatusPermission, "Inline mode should have status update permissions")
}

// TestVirtualMCPServerEnsureRBACResources_DiscoveredMode tests that discovered mode uses
// full RBAC permissions (including secret/configmap access) for backend discovery
func TestVirtualMCPServerEnsureRBACResources_DiscoveredMode(t *testing.T) {
	t.Parallel()

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "discovered-mode-vmcp",
			Namespace: "default",
			UID:       "test-uid",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: "test-group"},
			OutgoingAuth: &mcpv1beta1.OutgoingAuthConfig{
				Source: "discovered",
			},
		},
	}

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = rbacv1.AddToScheme(scheme)

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp).
		Build()

	r := &VirtualMCPServerReconciler{
		Client: fakeClient,
		Scheme: scheme,
	}

	// Call ensureRBACResources in discovered mode
	err := r.ensureRBACResources(context.Background(), vmcp)
	require.NoError(t, err)

	// Verify Role was created with full permissions (discovered mode)
	saName := vmcpServiceAccountName(vmcp.Name)
	role := &rbacv1.Role{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      saName,
		Namespace: vmcp.Namespace,
	}, role)
	assert.NoError(t, err, "Role should be created in discovered mode")
	assert.Equal(t, vmcpDiscoveredRBACRules, role.Rules, "Role should use full rules in discovered mode")

	// Verify discovered mode has secret/configmap access
	hasSecretAccess := false
	hasConfigMapAccess := false
	for _, rule := range role.Rules {
		for _, resource := range rule.Resources {
			if resource == "secrets" {
				hasSecretAccess = true
				assert.Contains(t, rule.Verbs, "get", "Should have get permission for secrets")
			}
			if resource == "configmaps" {
				hasConfigMapAccess = true
				assert.Contains(t, rule.Verbs, "get", "Should have get permission for configmaps")
			}
		}
	}
	assert.True(t, hasSecretAccess, "Discovered mode should have secret access")
	assert.True(t, hasConfigMapAccess, "Discovered mode should have configmap access")
}

// TestVirtualMCPServerEnsureRBACResources_CustomServiceAccount tests that RBAC resources
// are NOT created when a custom ServiceAccount is provided
func TestVirtualMCPServerEnsureRBACResources_CustomServiceAccount(t *testing.T) {
	t.Parallel()

	customSA := "custom-vmcp-sa"
	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "custom-sa-vmcp",
			Namespace: "default",
			UID:       "test-uid",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef:       &mcpv1beta1.MCPGroupRef{Name: "test-group"},
			ServiceAccount: &customSA,
		},
	}

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = rbacv1.AddToScheme(scheme)

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp).
		Build()

	r := &VirtualMCPServerReconciler{
		Client: fakeClient,
		Scheme: scheme,
	}

	// Call ensureRBACResources - should return nil without creating resources
	err := r.ensureRBACResources(context.Background(), vmcp)
	require.NoError(t, err)

	// Verify NO RBAC resources were created
	generatedSAName := vmcpServiceAccountName(vmcp.Name)

	sa := &corev1.ServiceAccount{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      generatedSAName,
		Namespace: vmcp.Namespace,
	}, sa)
	assert.Error(t, err, "ServiceAccount should not be created when custom ServiceAccount is provided")

	role := &rbacv1.Role{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      generatedSAName,
		Namespace: vmcp.Namespace,
	}, role)
	assert.Error(t, err, "Role should not be created when custom ServiceAccount is provided")

	rb := &rbacv1.RoleBinding{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      generatedSAName,
		Namespace: vmcp.Namespace,
	}, rb)
	assert.Error(t, err, "RoleBinding should not be created when custom ServiceAccount is provided")
}

// TestVirtualMCPServerEnsureDeployment tests Deployment creation
func TestVirtualMCPServerEnsureDeployment(t *testing.T) {
	t.Parallel()

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	// Create MCPGroup that the VirtualMCPServer references
	mcpGroup := &mcpv1beta1.MCPGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testGroupName,
			Namespace: "default",
		},
		Status: mcpv1beta1.MCPGroupStatus{
			Phase: mcpv1beta1.MCPGroupPhaseReady,
		},
	}

	// Create ConfigMap with checksum
	configMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmcpConfigMapName(vmcp.Name),
			Namespace: "default",
			Annotations: map[string]string{
				"toolhive.stacklok.dev/content-checksum": "test-checksum-123",
			},
		},
		Data: map[string]string{
			"config.yaml": "{}",
		},
	}

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp, mcpGroup, configMap).
		Build()

	r := &VirtualMCPServerReconciler{
		Client:           fakeClient,
		Scheme:           scheme,
		PlatformDetector: ctrlutil.NewSharedPlatformDetector(),
	}

	result, err := r.ensureDeployment(context.Background(), vmcp, nil, []workloads.TypedWorkload{})
	require.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)

	// Verify Deployment was created
	deployment := &appsv1.Deployment{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      vmcp.Name,
		Namespace: vmcp.Namespace,
	}, deployment)
	require.NoError(t, err)
	assert.Equal(t, vmcp.Name, deployment.Name)
	// spec.replicas is nil — nil-passthrough for HPA compatibility
	assert.Nil(t, deployment.Spec.Replicas)

	// Verify container configuration
	require.Len(t, deployment.Spec.Template.Spec.Containers, 1)
	container := deployment.Spec.Template.Spec.Containers[0]
	assert.Equal(t, "vmcp", container.Name)
	assert.NotEmpty(t, container.Image)
	assert.Contains(t, container.Args, "serve")
	assert.Contains(t, container.Args, "--config=/etc/vmcp-config/config.yaml")

	// Verify checksum annotation is set using standard annotation key
	assert.Equal(t, "test-checksum-123",
		deployment.Spec.Template.Annotations[checksum.RunConfigChecksumAnnotation])
}

// TestVirtualMCPServerEnsureService tests Service creation
func TestVirtualMCPServerEnsureService(t *testing.T) {
	t.Parallel()

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp).
		Build()

	r := &VirtualMCPServerReconciler{
		Client: fakeClient,
		Scheme: scheme,
	}

	result, err := r.ensureService(context.Background(), vmcp)
	require.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)

	// Verify Service was created
	service := &corev1.Service{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name:      vmcpServiceName(vmcp.Name),
		Namespace: vmcp.Namespace,
	}, service)
	require.NoError(t, err)
	assert.Equal(t, vmcpServiceName(vmcp.Name), service.Name)
	assert.Equal(t, corev1.ServiceTypeClusterIP, service.Spec.Type)

	// Verify port configuration
	require.Len(t, service.Spec.Ports, 1)
	assert.Equal(t, vmcpDefaultPort, service.Spec.Ports[0].Port)
	assert.Equal(t, "http", service.Spec.Ports[0].Name)
}

// TestVirtualMCPServerServiceType tests Service creation with different service types
func TestVirtualMCPServerServiceType(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name                string
		serviceType         string
		expectedServiceType corev1.ServiceType
	}{
		{
			name:                "default to ClusterIP",
			serviceType:         "",
			expectedServiceType: corev1.ServiceTypeClusterIP,
		},
		{
			name:                "explicit ClusterIP",
			serviceType:         "ClusterIP",
			expectedServiceType: corev1.ServiceTypeClusterIP,
		},
		{
			name:                "LoadBalancer",
			serviceType:         "LoadBalancer",
			expectedServiceType: corev1.ServiceTypeLoadBalancer,
		},
		{
			name:                "NodePort",
			serviceType:         "NodePort",
			expectedServiceType: corev1.ServiceTypeNodePort,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			vmcp := &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef:    &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					ServiceType: tt.serviceType,
				},
			}

			scheme := runtime.NewScheme()
			_ = mcpv1beta1.AddToScheme(scheme)
			_ = corev1.AddToScheme(scheme)

			r := &VirtualMCPServerReconciler{
				Scheme: scheme,
			}

			// Test serviceForVirtualMCPServer
			service := r.serviceForVirtualMCPServer(context.Background(), vmcp)
			require.NotNil(t, service)
			assert.Equal(t, tt.expectedServiceType, service.Spec.Type)
		})
	}
}

// TestVirtualMCPServerServiceNeedsUpdate tests service update detection
func TestVirtualMCPServerServiceNeedsUpdate(t *testing.T) {
	t.Parallel()

	baseVmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef:    &mcpv1beta1.MCPGroupRef{Name: testGroupName},
			ServiceType: "ClusterIP",
		},
	}

	baseService := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmcpServiceName(baseVmcp.Name),
			Namespace: baseVmcp.Namespace,
			Labels:    labelsForVirtualMCPServer(baseVmcp.Name),
		},
		Spec: corev1.ServiceSpec{
			Type:            corev1.ServiceTypeClusterIP,
			SessionAffinity: corev1.ServiceAffinityClientIP,
			Ports: []corev1.ServicePort{{
				Port: vmcpDefaultPort,
			}},
		},
	}

	tests := []struct {
		name        string
		service     *corev1.Service
		vmcp        *mcpv1beta1.VirtualMCPServer
		needsUpdate bool
	}{
		{
			name:        "no update needed",
			service:     baseService.DeepCopy(),
			vmcp:        baseVmcp.DeepCopy(),
			needsUpdate: false,
		},
		{
			name:    "service type changed to LoadBalancer",
			service: baseService.DeepCopy(),
			vmcp: func() *mcpv1beta1.VirtualMCPServer {
				v := baseVmcp.DeepCopy()
				v.Spec.ServiceType = "LoadBalancer"
				return v
			}(),
			needsUpdate: true,
		},
		{
			name:    "service type changed to NodePort",
			service: baseService.DeepCopy(),
			vmcp: func() *mcpv1beta1.VirtualMCPServer {
				v := baseVmcp.DeepCopy()
				v.Spec.ServiceType = "NodePort"
				return v
			}(),
			needsUpdate: true,
		},
		{
			name: "port changed",
			service: func() *corev1.Service {
				s := baseService.DeepCopy()
				s.Spec.Ports[0].Port = 9999
				return s
			}(),
			vmcp:        baseVmcp.DeepCopy(),
			needsUpdate: true,
		},
		{
			name: "session affinity missing",
			service: func() *corev1.Service {
				s := baseService.DeepCopy()
				s.Spec.SessionAffinity = ""
				return s
			}(),
			vmcp:        baseVmcp.DeepCopy(),
			needsUpdate: true,
		},
		{
			name: "session affinity spec changed to None",
			service: func() *corev1.Service {
				s := baseService.DeepCopy()
				s.Spec.SessionAffinity = corev1.ServiceAffinityClientIP
				return s
			}(),
			vmcp: func() *mcpv1beta1.VirtualMCPServer {
				v := baseVmcp.DeepCopy()
				v.Spec.SessionAffinity = string(corev1.ServiceAffinityNone)
				return v
			}(),
			needsUpdate: true,
		},
		{
			name: "session affinity matches spec None",
			service: func() *corev1.Service {
				s := baseService.DeepCopy()
				s.Spec.SessionAffinity = corev1.ServiceAffinityNone
				return s
			}(),
			vmcp: func() *mcpv1beta1.VirtualMCPServer {
				v := baseVmcp.DeepCopy()
				v.Spec.SessionAffinity = string(corev1.ServiceAffinityNone)
				return v
			}(),
			needsUpdate: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			r := &VirtualMCPServerReconciler{}
			result := r.serviceNeedsUpdate(tt.service, tt.vmcp)
			assert.Equal(t, tt.needsUpdate, result)
		})
	}
}

// TestVirtualMCPServerUpdateStatus tests status update logic
func TestVirtualMCPServerUpdateStatus(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name          string
		vmcp          *mcpv1beta1.VirtualMCPServer
		pods          []corev1.Pod
		expectedPhase mcpv1beta1.VirtualMCPServerPhase
	}{
		{
			name: "ready pods",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
			},
			pods: []corev1.Pod{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      testVmcpName + "-pod-1",
						Namespace: "default",
						Labels:    labelsForVirtualMCPServer(testVmcpName),
					},
					Status: corev1.PodStatus{
						Phase: corev1.PodRunning,
						Conditions: []corev1.PodCondition{
							{
								Type:   corev1.PodReady,
								Status: corev1.ConditionTrue,
							},
						},
					},
				},
			},
			expectedPhase: mcpv1beta1.VirtualMCPServerPhaseReady,
		},
		{
			name: "running but not ready pods",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
			},
			pods: []corev1.Pod{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      testVmcpName + "-pod-1",
						Namespace: "default",
						Labels:    labelsForVirtualMCPServer(testVmcpName),
					},
					Status: corev1.PodStatus{
						Phase: corev1.PodRunning,
						// No PodReady condition or PodReady=False means pod isn't ready yet
						Conditions: []corev1.PodCondition{
							{
								Type:   corev1.PodReady,
								Status: corev1.ConditionFalse,
							},
						},
					},
				},
			},
			expectedPhase: mcpv1beta1.VirtualMCPServerPhasePending,
		},
		{
			name: "pending pods",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
			},
			pods: []corev1.Pod{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      testVmcpName + "-pod-1",
						Namespace: "default",
						Labels:    labelsForVirtualMCPServer(testVmcpName),
					},
					Status: corev1.PodStatus{
						Phase: corev1.PodPending,
					},
				},
			},
			expectedPhase: mcpv1beta1.VirtualMCPServerPhasePending,
		},
		{
			name: "failed pods",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
			},
			pods: []corev1.Pod{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      testVmcpName + "-pod-1",
						Namespace: "default",
						Labels:    labelsForVirtualMCPServer(testVmcpName),
					},
					Status: corev1.PodStatus{
						Phase: corev1.PodFailed,
					},
				},
			},
			expectedPhase: mcpv1beta1.VirtualMCPServerPhaseFailed,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			scheme := runtime.NewScheme()
			_ = mcpv1beta1.AddToScheme(scheme)
			_ = corev1.AddToScheme(scheme)

			objs := []client.Object{tt.vmcp}
			for i := range tt.pods {
				objs = append(objs, &tt.pods[i])
			}

			fakeClient := fake.NewClientBuilder().
				WithScheme(scheme).
				WithObjects(objs...).
				WithStatusSubresource(&mcpv1beta1.VirtualMCPServer{}).
				Build()

			r := &VirtualMCPServerReconciler{
				Client: fakeClient,
				Scheme: scheme,
			}

			statusManager := virtualmcpserverstatus.NewStatusManager(tt.vmcp)
			err := r.updateVirtualMCPServerStatus(context.Background(), tt.vmcp, statusManager)
			require.NoError(t, err)
			// Apply status updates for test assertions
			_ = statusManager.UpdateStatus(context.Background(), &tt.vmcp.Status)
			assert.Equal(t, tt.expectedPhase, tt.vmcp.Status.Phase)
		})
	}
}

// TestVirtualMCPServerLabels tests label generation
func TestVirtualMCPServerLabels(t *testing.T) {
	t.Parallel()

	name := testVmcpName
	labels := labelsForVirtualMCPServer(name)

	assert.Equal(t, "virtualmcpserver", labels["app"])
	assert.Equal(t, "virtualmcpserver", labels["app.kubernetes.io/name"])
	assert.Equal(t, name, labels["app.kubernetes.io/instance"])
	assert.Equal(t, "true", labels["toolhive"])
	assert.Equal(t, name, labels["toolhive-name"])
}

// TestVirtualMCPServerNaming tests naming functions
func TestVirtualMCPServerNaming(t *testing.T) {
	t.Parallel()

	vmcpName := "my-vmcp"

	// Test service account name
	saName := vmcpServiceAccountName(vmcpName)
	assert.Equal(t, "my-vmcp-vmcp", saName)

	// Test service name
	svcName := vmcpServiceName(vmcpName)
	assert.Equal(t, "vmcp-my-vmcp", svcName)

	// Test ConfigMap name
	cmName := vmcpConfigMapName(vmcpName)
	assert.Equal(t, "my-vmcp-vmcp-config", cmName)

	// Test service URL
	url := createVmcpServiceURL(vmcpName, "default", 8080)
	assert.Equal(t, "http://vmcp-my-vmcp.default.svc.cluster.local:8080", url)
}

// TestVirtualMCPServerAuthConfiguredCondition tests AuthConfigured condition setting
// with various secret validation scenarios
func TestVirtualMCPServerAuthConfiguredCondition(t *testing.T) {
	t.Parallel()

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)

	tests := []struct {
		name                string
		vmcp                *mcpv1beta1.VirtualMCPServer
		secrets             []client.Object
		expectAuthCondition bool
		expectedAuthStatus  metav1.ConditionStatus
		expectedAuthReason  string
		expectError         bool
	}{
		{
			name: "valid auth with no secrets required (anonymous)",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					IncomingAuth: &mcpv1beta1.IncomingAuthConfig{
						Type: "anonymous",
					},
				},
			},
			secrets:             []client.Object{},
			expectAuthCondition: true,
			expectedAuthStatus:  metav1.ConditionTrue,
			expectedAuthReason:  mcpv1beta1.ConditionReasonAuthValid,
			expectError:         false,
		},
		{
			name: "OIDC with missing client secret via MCPOIDCConfig",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					IncomingAuth: &mcpv1beta1.IncomingAuthConfig{
						Type:          "oidc",
						OIDCConfigRef: &mcpv1beta1.MCPOIDCConfigReference{Name: "test-oidc", Audience: "test-audience"},
					},
				},
			},
			secrets: []client.Object{
				&mcpv1beta1.MCPOIDCConfig{
					ObjectMeta: metav1.ObjectMeta{Name: "test-oidc", Namespace: "default"},
					Spec: mcpv1beta1.MCPOIDCConfigSpec{
						Type: mcpv1beta1.MCPOIDCConfigTypeInline,
						Inline: &mcpv1beta1.InlineOIDCSharedConfig{
							Issuer: "https://issuer.example.com",
							ClientSecretRef: &mcpv1beta1.SecretKeyRef{
								Name: "missing-secret",
								Key:  "client-secret",
							},
						},
					},
				},
			},
			expectAuthCondition: true,
			expectedAuthStatus:  metav1.ConditionFalse,
			expectedAuthReason:  mcpv1beta1.ConditionReasonAuthInvalid,
			expectError:         true,
		},
		{
			name: "OIDC with valid client secret via MCPOIDCConfig",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					IncomingAuth: &mcpv1beta1.IncomingAuthConfig{
						Type:          "oidc",
						OIDCConfigRef: &mcpv1beta1.MCPOIDCConfigReference{Name: "test-oidc", Audience: "test-audience"},
					},
				},
			},
			secrets: []client.Object{
				&mcpv1beta1.MCPOIDCConfig{
					ObjectMeta: metav1.ObjectMeta{Name: "test-oidc", Namespace: "default"},
					Spec: mcpv1beta1.MCPOIDCConfigSpec{
						Type: mcpv1beta1.MCPOIDCConfigTypeInline,
						Inline: &mcpv1beta1.InlineOIDCSharedConfig{
							Issuer: "https://issuer.example.com",
							ClientSecretRef: &mcpv1beta1.SecretKeyRef{
								Name: "oidc-secret",
								Key:  "client-secret",
							},
						},
					},
				},
				&corev1.Secret{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "oidc-secret",
						Namespace: "default",
					},
					Data: map[string][]byte{
						"client-secret": []byte("supersecret"),
					},
				},
			},
			expectAuthCondition: true,
			expectedAuthStatus:  metav1.ConditionTrue,
			expectedAuthReason:  mcpv1beta1.ConditionReasonAuthValid,
			expectError:         false,
		},
		{
			name: "OIDC secret exists but missing required key via MCPOIDCConfig",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					IncomingAuth: &mcpv1beta1.IncomingAuthConfig{
						Type:          "oidc",
						OIDCConfigRef: &mcpv1beta1.MCPOIDCConfigReference{Name: "test-oidc", Audience: "test-audience"},
					},
				},
			},
			secrets: []client.Object{
				&mcpv1beta1.MCPOIDCConfig{
					ObjectMeta: metav1.ObjectMeta{Name: "test-oidc", Namespace: "default"},
					Spec: mcpv1beta1.MCPOIDCConfigSpec{
						Type: mcpv1beta1.MCPOIDCConfigTypeInline,
						Inline: &mcpv1beta1.InlineOIDCSharedConfig{
							Issuer: "https://issuer.example.com",
							ClientSecretRef: &mcpv1beta1.SecretKeyRef{
								Name: "oidc-secret",
								Key:  "client-secret",
							},
						},
					},
				},
				&corev1.Secret{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "oidc-secret",
						Namespace: "default",
					},
					Data: map[string][]byte{
						"wrong-key": []byte("supersecret"),
					},
				},
			},
			expectAuthCondition: true,
			expectedAuthStatus:  metav1.ConditionFalse,
			expectedAuthReason:  mcpv1beta1.ConditionReasonAuthInvalid,
			expectError:         true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			objs := append([]client.Object{tt.vmcp}, tt.secrets...)

			fakeClient := fake.NewClientBuilder().
				WithScheme(scheme).
				WithObjects(objs...).
				WithStatusSubresource(&mcpv1beta1.VirtualMCPServer{}).
				Build()

			r := &VirtualMCPServerReconciler{
				Client:           fakeClient,
				Scheme:           scheme,
				PlatformDetector: ctrlutil.NewSharedPlatformDetector(),
			}

			statusManager := virtualmcpserverstatus.NewStatusManager(tt.vmcp)
			_, err := r.ensureAllResources(context.Background(), tt.vmcp, nil, statusManager)

			if tt.expectError {
				assert.Error(t, err)
			}
			// ensureAllResources may return errors for missing resources like MCPGroup
			// We're only testing the auth condition setting

			// Apply status updates to check condition
			_ = statusManager.UpdateStatus(context.Background(), &tt.vmcp.Status)

			if tt.expectAuthCondition {
				// Find AuthConfigured condition
				var authCondition *metav1.Condition
				for i := range tt.vmcp.Status.Conditions {
					if tt.vmcp.Status.Conditions[i].Type == mcpv1beta1.ConditionTypeAuthConfigured {
						authCondition = &tt.vmcp.Status.Conditions[i]
						break
					}
				}

				require.NotNil(t, authCondition, "AuthConfigured condition should be set")
				assert.Equal(t, tt.expectedAuthStatus, authCondition.Status)
				assert.Equal(t, tt.expectedAuthReason, authCondition.Reason)
			}
		})
	}
}

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

	// Setup
	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)
	_ = rbacv1.AddToScheme(scheme)

	k8sClient := fake.NewClientBuilder().WithScheme(scheme).Build()

	reconciler := &VirtualMCPServerReconciler{
		Client: k8sClient,
		Scheme: scheme,
	}

	// Test reconciling a resource that doesn't exist
	req := ctrl.Request{
		NamespacedName: types.NamespacedName{
			Name:      "nonexistent",
			Namespace: "default",
		},
	}

	result, err := reconciler.Reconcile(context.Background(), req)

	// Should not error and should not requeue
	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)
}

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

	tests := []struct {
		name           string
		setupVMCP      func() *mcpv1beta1.VirtualMCPServer
		setupCollector func(vmcp *mcpv1beta1.VirtualMCPServer) virtualmcpserverstatus.StatusManager
		expectUpdate   bool
		expectError    bool
	}{
		{
			name: "successful status update",
			setupVMCP: func() *mcpv1beta1.VirtualMCPServer {
				return &mcpv1beta1.VirtualMCPServer{
					ObjectMeta: metav1.ObjectMeta{
						Name:       testVmcpName,
						Namespace:  "default",
						Generation: 1,
					},
					Spec: mcpv1beta1.VirtualMCPServerSpec{
						GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					},
				}
			},
			setupCollector: func(vmcp *mcpv1beta1.VirtualMCPServer) virtualmcpserverstatus.StatusManager {
				collector := virtualmcpserverstatus.NewStatusManager(vmcp)
				collector.SetPhase(mcpv1beta1.VirtualMCPServerPhaseReady)
				collector.SetMessage("All resources ready")
				return collector
			},
			expectUpdate: true,
			expectError:  false,
		},
		{
			name: "no changes to apply",
			setupVMCP: func() *mcpv1beta1.VirtualMCPServer {
				return &mcpv1beta1.VirtualMCPServer{
					ObjectMeta: metav1.ObjectMeta{
						Name:       testVmcpName,
						Namespace:  "default",
						Generation: 1,
					},
					Spec: mcpv1beta1.VirtualMCPServerSpec{
						GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					},
				}
			},
			setupCollector: func(vmcp *mcpv1beta1.VirtualMCPServer) virtualmcpserverstatus.StatusManager {
				return virtualmcpserverstatus.NewStatusManager(vmcp)
			},
			expectUpdate: false,
			expectError:  false,
		},
		{
			name: "batch update with multiple changes",
			setupVMCP: func() *mcpv1beta1.VirtualMCPServer {
				return &mcpv1beta1.VirtualMCPServer{
					ObjectMeta: metav1.ObjectMeta{
						Name:       testVmcpName,
						Namespace:  "default",
						Generation: 1,
					},
					Spec: mcpv1beta1.VirtualMCPServerSpec{
						GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					},
				}
			},
			setupCollector: func(vmcp *mcpv1beta1.VirtualMCPServer) virtualmcpserverstatus.StatusManager {
				collector := virtualmcpserverstatus.NewStatusManager(vmcp)
				collector.SetPhase(mcpv1beta1.VirtualMCPServerPhaseReady)
				collector.SetMessage("All resources ready")
				collector.SetURL("http://test.example.com")
				collector.SetObservedGeneration(1)
				collector.SetGroupRefValidatedCondition("GroupValid", "group is valid", metav1.ConditionTrue)
				collector.SetAuthConfiguredCondition("AuthValid", "auth is configured", metav1.ConditionTrue)
				collector.SetReadyCondition("DeploymentReady", "deployment is ready", metav1.ConditionTrue)
				return collector
			},
			expectUpdate: true,
			expectError:  false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			scheme := runtime.NewScheme()
			_ = mcpv1beta1.AddToScheme(scheme)
			_ = corev1.AddToScheme(scheme)
			_ = appsv1.AddToScheme(scheme)
			_ = rbacv1.AddToScheme(scheme)

			vmcp := tt.setupVMCP()
			k8sClient := fake.NewClientBuilder().
				WithScheme(scheme).
				WithObjects(vmcp).
				WithStatusSubresource(vmcp).
				Build()

			reconciler := &VirtualMCPServerReconciler{
				Client: k8sClient,
				Scheme: scheme,
			}

			collector := tt.setupCollector(vmcp)

			err := reconciler.applyStatusUpdates(context.Background(), vmcp, collector)

			if tt.expectError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)

				// Verify the status was updated
				updatedVMCP := &mcpv1beta1.VirtualMCPServer{}
				err := k8sClient.Get(context.Background(), types.NamespacedName{
					Name:      vmcp.Name,
					Namespace: vmcp.Namespace,
				}, updatedVMCP)
				require.NoError(t, err)

				if tt.expectUpdate {
					// Verify updates were applied
					assert.NotEqual(t, mcpv1beta1.VirtualMCPServerPhase(""), updatedVMCP.Status.Phase)
				}
			}
		})
	}
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)
	_ = rbacv1.AddToScheme(scheme)

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:       testVmcpName,
			Namespace:  "default",
			Generation: 1,
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	// Create client WITHOUT the resource
	k8sClient := fake.NewClientBuilder().
		WithScheme(scheme).
		Build()

	reconciler := &VirtualMCPServerReconciler{
		Client: k8sClient,
		Scheme: scheme,
	}

	collector := virtualmcpserverstatus.NewStatusManager(vmcp)
	collector.SetPhase(mcpv1beta1.VirtualMCPServerPhaseReady)

	err := reconciler.applyStatusUpdates(context.Background(), vmcp, collector)

	// Should return error when resource doesn't exist
	assert.Error(t, err)
}

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

	tests := []struct {
		name        string
		setupVMCP   func() *mcpv1beta1.VirtualMCPServer
		setupClient func(t *testing.T, vmcp *mcpv1beta1.VirtualMCPServer) client.Client
		expectError bool
	}{
		{
			name: "no auth configured - valid",
			setupVMCP: func() *mcpv1beta1.VirtualMCPServer {
				return &mcpv1beta1.VirtualMCPServer{
					ObjectMeta: metav1.ObjectMeta{
						Name:       testVmcpName,
						Namespace:  "default",
						Generation: 1,
					},
					Spec: mcpv1beta1.VirtualMCPServerSpec{
						GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					},
				}
			},
			setupClient: func(_ *testing.T, vmcp *mcpv1beta1.VirtualMCPServer) client.Client {
				scheme := runtime.NewScheme()
				_ = mcpv1beta1.AddToScheme(scheme)
				_ = corev1.AddToScheme(scheme)
				_ = appsv1.AddToScheme(scheme)
				_ = rbacv1.AddToScheme(scheme)

				mcpGroup := &mcpv1beta1.MCPGroup{
					ObjectMeta: metav1.ObjectMeta{
						Name:      testGroupName,
						Namespace: "default",
					},
					Status: mcpv1beta1.MCPGroupStatus{
						Phase: mcpv1beta1.MCPGroupPhaseReady,
					},
				}
				return fake.NewClientBuilder().
					WithScheme(scheme).
					WithObjects(vmcp, mcpGroup).
					WithStatusSubresource(vmcp).
					Build()
			},
			expectError: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			vmcp := tt.setupVMCP()
			k8sClient := tt.setupClient(t, vmcp)

			reconciler := &VirtualMCPServerReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			collector := virtualmcpserverstatus.NewStatusManager(vmcp)

			_, err := reconciler.ensureAllResources(context.Background(), vmcp, nil, collector)

			if tt.expectError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)

	reconciler := &VirtualMCPServerReconciler{
		Scheme: scheme,
	}

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	tests := []struct {
		name           string
		deployment     *appsv1.Deployment
		vmcp           *mcpv1beta1.VirtualMCPServer
		expectedUpdate bool
	}{
		{
			name:           "nil deployment needs update",
			deployment:     nil,
			vmcp:           vmcp,
			expectedUpdate: true,
		},
		{
			name: "nil vmcp needs update",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "vmcp",
									Image: "test-image:latest",
								},
							},
						},
					},
				},
			},
			vmcp:           nil,
			expectedUpdate: true,
		},
		{
			name: "empty containers needs update",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{},
						},
					},
				},
			},
			vmcp:           vmcp,
			expectedUpdate: true,
		},
		{
			name: "image change needs update",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "vmcp",
									Image: "old-image:v1",
									Ports: []corev1.ContainerPort{
										{ContainerPort: 4483},
									},
									Env: mustBuildEnvVarsForVmcp(reconciler, vmcp),
								},
							},
							ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
						},
					},
				},
			},
			vmcp:           vmcp,
			expectedUpdate: true,
		},
		{
			name: "port change needs update",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "vmcp",
									Image: getVmcpImage(),
									Ports: []corev1.ContainerPort{
										{ContainerPort: 8080},
									},
									Env: mustBuildEnvVarsForVmcp(reconciler, vmcp),
								},
							},
							ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
						},
					},
				},
			},
			vmcp:           vmcp,
			expectedUpdate: true,
		},
		{
			name: "env var change needs update",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "vmcp",
									Image: getVmcpImage(),
									Ports: []corev1.ContainerPort{
										{ContainerPort: 4483},
									},
									Env: []corev1.EnvVar{
										{Name: "OLD_VAR", Value: "old-value"},
									},
								},
							},
							ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
						},
					},
				},
			},
			vmcp:           vmcp,
			expectedUpdate: true,
		},
		{
			name: "service account change needs update",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "vmcp",
									Image: getVmcpImage(),
									Ports: []corev1.ContainerPort{
										{ContainerPort: 4483},
									},
									Args: reconciler.buildContainerArgsForVmcp(vmcp),
									Env:  mustBuildEnvVarsForVmcp(reconciler, vmcp),
								},
							},
							ServiceAccountName: "wrong-service-account",
						},
					},
				},
			},
			vmcp:           vmcp,
			expectedUpdate: true,
		},
		{
			name: "log level change to debug needs update",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "vmcp",
									Image: getVmcpImage(),
									Ports: []corev1.ContainerPort{
										{ContainerPort: 4483},
									},
									Args: []string{"serve", "--config=/etc/vmcp-config/config.yaml", "--host=0.0.0.0", "--port=4483"},
									Env:  mustBuildEnvVarsForVmcp(reconciler, vmcp),
								},
							},
							ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
						},
					},
				},
			},
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					Config: vmcpconfig.Config{
						Group: testGroupName,
						Operational: &vmcpconfig.OperationalConfig{
							LogLevel: "debug",
						},
					},
				},
			},
			expectedUpdate: true,
		},
		{
			name: "log level removed from debug needs update",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "vmcp",
									Image: getVmcpImage(),
									Ports: []corev1.ContainerPort{
										{ContainerPort: 4483},
									},
									Args: []string{"serve", "--config=/etc/vmcp-config/config.yaml", "--host=0.0.0.0", "--port=4483", "--debug"},
									Env:  mustBuildEnvVarsForVmcp(reconciler, vmcp),
								},
							},
							ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
						},
					},
				},
			},
			vmcp:           vmcp,
			expectedUpdate: true,
		},
		{
			name: "no changes - no update needed",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "vmcp",
									Image: getVmcpImage(),
									Ports: []corev1.ContainerPort{
										{ContainerPort: 4483},
									},
									Args: reconciler.buildContainerArgsForVmcp(vmcp),
									Env:  mustBuildEnvVarsForVmcp(reconciler, vmcp),
								},
							},
							ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
						},
					},
				},
			},
			vmcp:           vmcp,
			expectedUpdate: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			needsUpdate := reconciler.containerNeedsUpdate(context.Background(), tt.deployment, tt.vmcp, nil, []workloads.TypedWorkload{})
			assert.Equal(t, tt.expectedUpdate, needsUpdate)
		})
	}
}

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

	reconciler := &VirtualMCPServerReconciler{}

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
	}

	tests := []struct {
		name           string
		deployment     *appsv1.Deployment
		vmcp           *mcpv1beta1.VirtualMCPServer
		expectedUpdate bool
	}{
		{
			name:           "nil deployment needs update",
			deployment:     nil,
			vmcp:           vmcp,
			expectedUpdate: true,
		},
		{
			name: "nil vmcp needs update",
			deployment: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Labels: labelsForVirtualMCPServer(testVmcpName),
				},
			},
			vmcp:           nil,
			expectedUpdate: true,
		},
		{
			name: "label change needs update",
			deployment: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						"wrong-label": "wrong-value",
					},
					Annotations: make(map[string]string),
				},
			},
			vmcp:           vmcp,
			expectedUpdate: true,
		},
		{
			name: "extra annotations allowed - no update needed",
			deployment: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Labels: labelsForVirtualMCPServer(vmcp.Name),
					Annotations: map[string]string{
						"extra-annotation": "extra-value",
					},
				},
			},
			vmcp:           vmcp,
			expectedUpdate: false,
		},
		{
			name: "no changes - no update needed",
			deployment: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Labels:      labelsForVirtualMCPServer(vmcp.Name),
					Annotations: make(map[string]string),
				},
			},
			vmcp:           vmcp,
			expectedUpdate: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			needsUpdate := reconciler.deploymentMetadataNeedsUpdate(tt.deployment, tt.vmcp)
			assert.Equal(t, tt.expectedUpdate, needsUpdate)
		})
	}
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)

	reconciler := &VirtualMCPServerReconciler{
		Scheme: scheme,
	}

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
	}

	vmcpConfigChecksum := testChecksumValue
	expectedLabels, expectedAnnotations := reconciler.buildPodTemplateMetadata(
		labelsForVirtualMCPServer(vmcp.Name), vmcp, vmcpConfigChecksum,
	)

	tests := []struct {
		name           string
		deployment     *appsv1.Deployment
		vmcp           *mcpv1beta1.VirtualMCPServer
		checksum       string
		expectedUpdate bool
	}{
		{
			name:           "nil deployment needs update",
			deployment:     nil,
			vmcp:           vmcp,
			checksum:       vmcpConfigChecksum,
			expectedUpdate: true,
		},
		{
			name: "nil vmcp needs update",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						ObjectMeta: metav1.ObjectMeta{
							Labels:      expectedLabels,
							Annotations: expectedAnnotations,
						},
					},
				},
			},
			vmcp:           nil,
			checksum:       vmcpConfigChecksum,
			expectedUpdate: true,
		},
		{
			name: "pod template label change needs update",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						ObjectMeta: metav1.ObjectMeta{
							Labels: map[string]string{
								"wrong-label": "wrong-value",
							},
							Annotations: expectedAnnotations,
						},
					},
				},
			},
			vmcp:           vmcp,
			checksum:       vmcpConfigChecksum,
			expectedUpdate: true,
		},
		{
			name: "pod template annotation change needs update",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						ObjectMeta: metav1.ObjectMeta{
							Labels: expectedLabels,
							Annotations: map[string]string{
								"wrong-annotation": "wrong-value",
							},
						},
					},
				},
			},
			vmcp:           vmcp,
			checksum:       vmcpConfigChecksum,
			expectedUpdate: true,
		},
		{
			name: "checksum change needs update",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						ObjectMeta: metav1.ObjectMeta{
							Labels: expectedLabels,
							Annotations: map[string]string{
								checksum.RunConfigChecksumAnnotation: "old-checksum",
							},
						},
					},
				},
			},
			vmcp:           vmcp,
			checksum:       vmcpConfigChecksum,
			expectedUpdate: true,
		},
		{
			name: "no changes - no update needed",
			deployment: &appsv1.Deployment{
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						ObjectMeta: metav1.ObjectMeta{
							Labels:      expectedLabels,
							Annotations: expectedAnnotations,
						},
					},
				},
			},
			vmcp:           vmcp,
			checksum:       vmcpConfigChecksum,
			expectedUpdate: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			needsUpdate := reconciler.podTemplateMetadataNeedsUpdate(tt.deployment, tt.vmcp, tt.checksum)
			assert.Equal(t, tt.expectedUpdate, needsUpdate)
		})
	}
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)

	reconciler := &VirtualMCPServerReconciler{
		Scheme: scheme,
	}

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	vmcpConfigChecksum := testChecksumValue
	expectedLabels, expectedAnnotations := reconciler.buildPodTemplateMetadata(
		labelsForVirtualMCPServer(vmcp.Name), vmcp, vmcpConfigChecksum,
	)

	tests := []struct {
		name           string
		deployment     *appsv1.Deployment
		expectedUpdate bool
	}{
		{
			name: "deployment metadata changed",
			deployment: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						"wrong-label": "wrong-value",
					},
					Annotations: make(map[string]string),
				},
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						ObjectMeta: metav1.ObjectMeta{
							Labels:      expectedLabels,
							Annotations: expectedAnnotations,
						},
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "vmcp",
									Image: getVmcpImage(),
									Ports: []corev1.ContainerPort{
										{ContainerPort: 4483},
									},
									Env: mustBuildEnvVarsForVmcp(reconciler, vmcp),
								},
							},
							ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
						},
					},
				},
			},
			expectedUpdate: true,
		},
		{
			name: "pod template metadata changed",
			deployment: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Labels:      labelsForVirtualMCPServer(vmcp.Name),
					Annotations: make(map[string]string),
				},
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						ObjectMeta: metav1.ObjectMeta{
							Labels: map[string]string{
								"wrong-label": "wrong-value",
							},
							Annotations: expectedAnnotations,
						},
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "vmcp",
									Image: getVmcpImage(),
									Ports: []corev1.ContainerPort{
										{ContainerPort: 4483},
									},
									Env: mustBuildEnvVarsForVmcp(reconciler, vmcp),
								},
							},
							ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
						},
					},
				},
			},
			expectedUpdate: true,
		},
		{
			name: "container changed",
			deployment: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Labels:      labelsForVirtualMCPServer(vmcp.Name),
					Annotations: make(map[string]string),
				},
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						ObjectMeta: metav1.ObjectMeta{
							Labels:      expectedLabels,
							Annotations: expectedAnnotations,
						},
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "vmcp",
									Image: "old-image:v1",
									Ports: []corev1.ContainerPort{
										{ContainerPort: 4483},
									},
									Args: reconciler.buildContainerArgsForVmcp(vmcp),
									Env:  mustBuildEnvVarsForVmcp(reconciler, vmcp),
								},
							},
							ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
						},
					},
				},
			},
			expectedUpdate: true,
		},
		{
			name: "no changes - no update needed",
			deployment: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Labels:      labelsForVirtualMCPServer(vmcp.Name),
					Annotations: make(map[string]string),
				},
				Spec: appsv1.DeploymentSpec{
					Template: corev1.PodTemplateSpec{
						ObjectMeta: metav1.ObjectMeta{
							Labels:      expectedLabels,
							Annotations: expectedAnnotations,
						},
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "vmcp",
									Image: getVmcpImage(),
									Ports: []corev1.ContainerPort{
										{ContainerPort: 4483},
									},
									Args: reconciler.buildContainerArgsForVmcp(vmcp),
									Env:  mustBuildEnvVarsForVmcp(reconciler, vmcp),
								},
							},
							ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
						},
					},
				},
			},
			expectedUpdate: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			needsUpdate := reconciler.deploymentNeedsUpdate(context.Background(), tt.deployment, vmcp, vmcpConfigChecksum, nil, []workloads.TypedWorkload{})
			assert.Equal(t, tt.expectedUpdate, needsUpdate)
		})
	}
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)
	_ = rbacv1.AddToScheme(scheme)

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:       testVmcpName,
			Namespace:  "default",
			Generation: 1,
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	mcpGroup := &mcpv1beta1.MCPGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testGroupName,
			Namespace: "default",
		},
		Status: mcpv1beta1.MCPGroupStatus{
			Phase: mcpv1beta1.MCPGroupPhaseReady,
		},
	}

	// Create deployment that will be found by ensureDeployment
	replicas := int32(1)
	deployment := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
			Labels:    labelsForVirtualMCPServer(vmcp.Name),
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &replicas,
			Selector: &metav1.LabelSelector{
				MatchLabels: labelsForVirtualMCPServer(vmcp.Name),
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: labelsForVirtualMCPServer(vmcp.Name),
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:  "vmcp",
							Image: "test-image:latest",
						},
					},
				},
			},
		},
		Status: appsv1.DeploymentStatus{
			ReadyReplicas: 1,
		},
	}

	// Create service that will be found by ensureService
	service := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmcpServiceName(vmcp.Name),
			Namespace: "default",
			Labels:    labelsForVirtualMCPServer(vmcp.Name),
		},
		Spec: corev1.ServiceSpec{
			Selector: labelsForVirtualMCPServer(vmcp.Name),
			Ports: []corev1.ServicePort{
				{
					Port:       4483,
					TargetPort: intstr.FromInt(4483),
				},
			},
		},
	}

	// Create pod for status update
	pod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmcp.Name + "-pod",
			Namespace: "default",
			Labels:    labelsForVirtualMCPServer(vmcp.Name),
		},
		Status: corev1.PodStatus{
			Phase: corev1.PodRunning,
			Conditions: []corev1.PodCondition{
				{
					Type:   corev1.PodReady,
					Status: corev1.ConditionTrue,
				},
			},
		},
	}

	k8sClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp, mcpGroup, deployment, service, pod).
		WithStatusSubresource(vmcp).
		Build()

	reconciler := &VirtualMCPServerReconciler{
		Client: k8sClient,
		Scheme: scheme,
	}

	req := ctrl.Request{
		NamespacedName: types.NamespacedName{
			Name:      vmcp.Name,
			Namespace: vmcp.Namespace,
		},
	}

	result, err := reconciler.Reconcile(context.Background(), req)

	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)

	// Verify status was updated
	updatedVMCP := &mcpv1beta1.VirtualMCPServer{}
	err = k8sClient.Get(context.Background(), types.NamespacedName{
		Name:      vmcp.Name,
		Namespace: vmcp.Namespace,
	}, updatedVMCP)
	require.NoError(t, err)

	// Verify conditions were set
	assert.NotEmpty(t, updatedVMCP.Status.Conditions)
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)
	_ = rbacv1.AddToScheme(scheme)

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:       testVmcpName,
			Namespace:  "default",
			Generation: 1,
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: "nonexistent-group"},
		},
	}

	// Don't create the MCPGroup so validation fails
	k8sClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp).
		WithStatusSubresource(vmcp).
		Build()

	reconciler := &VirtualMCPServerReconciler{
		Client: k8sClient,
		Scheme: scheme,
	}

	req := ctrl.Request{
		NamespacedName: types.NamespacedName{
			Name:      vmcp.Name,
			Namespace: vmcp.Namespace,
		},
	}

	result, err := reconciler.Reconcile(context.Background(), req)

	assert.Error(t, err)
	assert.Equal(t, ctrl.Result{}, result)

	// Verify status was updated with error condition
	updatedVMCP := &mcpv1beta1.VirtualMCPServer{}
	err = k8sClient.Get(context.Background(), types.NamespacedName{
		Name:      vmcp.Name,
		Namespace: vmcp.Namespace,
	}, updatedVMCP)
	require.NoError(t, err)

	assert.Equal(t, mcpv1beta1.VirtualMCPServerPhaseFailed, updatedVMCP.Status.Phase)
	assert.NotEmpty(t, updatedVMCP.Status.Message)
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)
	_ = rbacv1.AddToScheme(scheme)

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:       testVmcpName,
			Namespace:  "default",
			Generation: 1,
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	mcpGroup := &mcpv1beta1.MCPGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testGroupName,
			Namespace: "default",
		},
		Status: mcpv1beta1.MCPGroupStatus{
			Phase: mcpv1beta1.MCPGroupPhasePending, // Not ready
		},
	}

	k8sClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp, mcpGroup).
		WithStatusSubresource(vmcp).
		Build()

	reconciler := &VirtualMCPServerReconciler{
		Client: k8sClient,
		Scheme: scheme,
	}

	req := ctrl.Request{
		NamespacedName: types.NamespacedName{
			Name:      vmcp.Name,
			Namespace: vmcp.Namespace,
		},
	}

	result, err := reconciler.Reconcile(context.Background(), req)

	assert.Error(t, err)
	assert.Contains(t, err.Error(), "is not ready")
	assert.Equal(t, ctrl.Result{}, result)

	// Verify status was updated
	updatedVMCP := &mcpv1beta1.VirtualMCPServer{}
	err = k8sClient.Get(context.Background(), types.NamespacedName{
		Name:      vmcp.Name,
		Namespace: vmcp.Namespace,
	}, updatedVMCP)
	require.NoError(t, err)

	assert.Equal(t, mcpv1beta1.VirtualMCPServerPhasePending, updatedVMCP.Status.Phase)
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)

	// Create empty client - resource won't be found but we'll test non-NotFound errors
	// by using a client that returns a generic error
	k8sClient := fake.NewClientBuilder().
		WithScheme(scheme).
		Build()

	reconciler := &VirtualMCPServerReconciler{
		Client: k8sClient,
		Scheme: scheme,
	}

	req := ctrl.Request{
		NamespacedName: types.NamespacedName{
			Name:      testVmcpName,
			Namespace: "default",
		},
	}

	result, err := reconciler.Reconcile(context.Background(), req)

	// For a not found error, should not error and not requeue
	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	// Don't create ConfigMap - it won't be found
	k8sClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp).
		Build()

	reconciler := &VirtualMCPServerReconciler{
		Client: k8sClient,
		Scheme: scheme,
	}

	result, err := reconciler.ensureDeployment(context.Background(), vmcp, nil, []workloads.TypedWorkload{})

	// Should requeue after 5 seconds when ConfigMap not found
	assert.NoError(t, err)
	assert.Equal(t, 5*time.Second, result.RequeueAfter)
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	// Create ConfigMap so checksum can be retrieved
	configMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmcpConfigMapName(vmcp.Name),
			Namespace: "default",
			Annotations: map[string]string{
				checksum.ContentChecksumAnnotation: "test-checksum",
			},
		},
		Data: map[string]string{
			"config.yaml": "test-config",
		},
	}

	k8sClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp, configMap).
		Build()

	reconciler := &VirtualMCPServerReconciler{
		Client: k8sClient,
		Scheme: scheme,
	}

	result, err := reconciler.ensureDeployment(context.Background(), vmcp, nil, []workloads.TypedWorkload{})

	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)

	// Verify deployment was created
	deployment := &appsv1.Deployment{}
	err = k8sClient.Get(context.Background(), types.NamespacedName{
		Name:      vmcp.Name,
		Namespace: vmcp.Namespace,
	}, deployment)
	assert.NoError(t, err)
	assert.Equal(t, vmcp.Name, deployment.Name)
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	configMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmcpConfigMapName(vmcp.Name),
			Namespace: "default",
			Annotations: map[string]string{
				checksum.ContentChecksumAnnotation: "test-checksum",
			},
		},
		Data: map[string]string{
			"config.yaml": "test-config",
		},
	}

	// Create existing deployment with old image
	oldDeployment := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
			Labels:    labelsForVirtualMCPServer(vmcp.Name),
		},
		Spec: appsv1.DeploymentSpec{
			Selector: &metav1.LabelSelector{
				MatchLabels: labelsForVirtualMCPServer(vmcp.Name),
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: labelsForVirtualMCPServer(vmcp.Name),
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:  "vmcp",
							Image: "old-image:v1",
						},
					},
				},
			},
		},
	}

	k8sClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp, configMap, oldDeployment).
		Build()

	reconciler := &VirtualMCPServerReconciler{
		Client: k8sClient,
		Scheme: scheme,
	}

	result, err := reconciler.ensureDeployment(context.Background(), vmcp, nil, []workloads.TypedWorkload{})

	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)

	// Verify deployment was updated
	deployment := &appsv1.Deployment{}
	err = k8sClient.Get(context.Background(), types.NamespacedName{
		Name:      vmcp.Name,
		Namespace: vmcp.Namespace,
	}, deployment)
	assert.NoError(t, err)
	assert.Equal(t, getVmcpImage(), deployment.Spec.Template.Spec.Containers[0].Image)
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	configMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmcpConfigMapName(vmcp.Name),
			Namespace: "default",
			Annotations: map[string]string{
				checksum.ContentChecksumAnnotation: "test-checksum",
			},
		},
		Data: map[string]string{
			"config.yaml": "test-config",
		},
	}

	reconciler := &VirtualMCPServerReconciler{
		Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
		Scheme: scheme,
	}

	// Create deployment matching current spec
	expectedLabels, expectedAnnotations := reconciler.buildPodTemplateMetadata(
		labelsForVirtualMCPServer(vmcp.Name), vmcp, "test-checksum",
	)

	correctDeployment := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:        testVmcpName,
			Namespace:   "default",
			Labels:      labelsForVirtualMCPServer(vmcp.Name),
			Annotations: make(map[string]string),
		},
		Spec: appsv1.DeploymentSpec{
			Selector: &metav1.LabelSelector{
				MatchLabels: labelsForVirtualMCPServer(vmcp.Name),
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels:      expectedLabels,
					Annotations: expectedAnnotations,
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:  "vmcp",
							Image: getVmcpImage(),
							Ports: []corev1.ContainerPort{
								{ContainerPort: 4483},
							},
							Env: mustBuildEnvVarsForVmcp(reconciler, vmcp),
						},
					},
					ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
				},
			},
		},
	}

	k8sClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp, configMap, correctDeployment).
		Build()

	reconciler.Client = k8sClient

	result, err := reconciler.ensureDeployment(context.Background(), vmcp, nil, []workloads.TypedWorkload{})

	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	k8sClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp).
		Build()

	reconciler := &VirtualMCPServerReconciler{
		Client: k8sClient,
		Scheme: scheme,
	}

	result, err := reconciler.ensureService(context.Background(), vmcp)

	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)

	// Verify service was created
	service := &corev1.Service{}
	err = k8sClient.Get(context.Background(), types.NamespacedName{
		Name:      vmcpServiceName(vmcp.Name),
		Namespace: vmcp.Namespace,
	}, service)
	assert.NoError(t, err)
	assert.Equal(t, vmcpServiceName(vmcp.Name), service.Name)
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef:    &mcpv1beta1.MCPGroupRef{Name: testGroupName},
			ServiceType: "LoadBalancer",
		},
	}

	// Create existing service with wrong type
	oldService := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmcpServiceName(vmcp.Name),
			Namespace: "default",
			Labels:    labelsForVirtualMCPServer(vmcp.Name),
		},
		Spec: corev1.ServiceSpec{
			Type:     corev1.ServiceTypeClusterIP,
			Selector: labelsForVirtualMCPServer(vmcp.Name),
			Ports: []corev1.ServicePort{
				{
					Port:       4483,
					TargetPort: intstr.FromInt(4483),
				},
			},
		},
	}

	k8sClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp, oldService).
		Build()

	reconciler := &VirtualMCPServerReconciler{
		Client: k8sClient,
		Scheme: scheme,
	}

	result, err := reconciler.ensureService(context.Background(), vmcp)

	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)

	// Verify service was updated
	service := &corev1.Service{}
	err = k8sClient.Get(context.Background(), types.NamespacedName{
		Name:      vmcpServiceName(vmcp.Name),
		Namespace: vmcp.Namespace,
	}, service)
	assert.NoError(t, err)
	assert.Equal(t, corev1.ServiceTypeLoadBalancer, service.Spec.Type)
}

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

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testVmcpName,
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
		},
	}

	// Create service matching current spec
	correctService := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:        vmcpServiceName(vmcp.Name),
			Namespace:   "default",
			Labels:      labelsForVirtualMCPServer(vmcp.Name),
			Annotations: make(map[string]string),
		},
		Spec: corev1.ServiceSpec{
			Type:     corev1.ServiceTypeClusterIP,
			Selector: labelsForVirtualMCPServer(vmcp.Name),
			Ports: []corev1.ServicePort{
				{
					Port:       4483,
					TargetPort: intstr.FromInt(4483),
				},
			},
		},
	}

	k8sClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp, correctService).
		Build()

	reconciler := &VirtualMCPServerReconciler{
		Client: k8sClient,
		Scheme: scheme,
	}

	result, err := reconciler.ensureService(context.Background(), vmcp)

	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)
}

// TestVirtualMCPServerValidateEmbeddingServerRef tests the EmbeddingServerRef validation.
// validateEmbeddingServerRef only validates existence, not readiness — readiness is
// checked by isEmbeddingServerReady.
func TestVirtualMCPServerValidateEmbeddingServerRef(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name            string
		vmcp            *mcpv1beta1.VirtualMCPServer
		embeddingServer *mcpv1beta1.EmbeddingServer
		expectError     bool
		expectedPhase   mcpv1beta1.VirtualMCPServerPhase
		expectedReason  string
	}{
		{
			name: "no ref configured (skip validation)",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
				},
			},
			expectError: false,
		},
		{
			name: "referenced EmbeddingServer exists and is running",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					EmbeddingServerRef: &mcpv1beta1.EmbeddingServerRef{
						Name: "shared-embedding",
					},
				},
			},
			embeddingServer: &mcpv1beta1.EmbeddingServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "shared-embedding",
					Namespace: "default",
				},
				Status: mcpv1beta1.EmbeddingServerStatus{
					Phase:         mcpv1beta1.EmbeddingServerPhaseReady,
					ReadyReplicas: 1,
				},
			},
			expectError: false,
		},
		{
			name: "referenced EmbeddingServer not found",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					EmbeddingServerRef: &mcpv1beta1.EmbeddingServerRef{
						Name: "missing-embedding",
					},
				},
			},
			expectError:    true,
			expectedPhase:  mcpv1beta1.VirtualMCPServerPhaseFailed,
			expectedReason: mcpv1beta1.ConditionReasonEmbeddingServerNotFound,
		},
		{
			name: "referenced EmbeddingServer exists but not ready (pending) - existence validated",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					EmbeddingServerRef: &mcpv1beta1.EmbeddingServerRef{
						Name: "pending-embedding",
					},
				},
			},
			embeddingServer: &mcpv1beta1.EmbeddingServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "pending-embedding",
					Namespace: "default",
				},
				Status: mcpv1beta1.EmbeddingServerStatus{
					Phase:         mcpv1beta1.EmbeddingServerPhasePending,
					ReadyReplicas: 0,
				},
			},
			expectError: false,
		},
		{
			name: "referenced EmbeddingServer running but zero ready replicas - existence validated",
			vmcp: &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      testVmcpName,
					Namespace: "default",
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					EmbeddingServerRef: &mcpv1beta1.EmbeddingServerRef{
						Name: "no-replicas-embedding",
					},
				},
			},
			embeddingServer: &mcpv1beta1.EmbeddingServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "no-replicas-embedding",
					Namespace: "default",
				},
				Status: mcpv1beta1.EmbeddingServerStatus{
					Phase:         mcpv1beta1.EmbeddingServerPhaseReady,
					ReadyReplicas: 0,
				},
			},
			expectError: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			// Setup fake client with resources
			scheme := runtime.NewScheme()
			_ = mcpv1beta1.AddToScheme(scheme)
			_ = corev1.AddToScheme(scheme)
			_ = appsv1.AddToScheme(scheme)
			_ = rbacv1.AddToScheme(scheme)

			objs := []client.Object{tt.vmcp}
			if tt.embeddingServer != nil {
				objs = append(objs, tt.embeddingServer)
			}

			fakeClient := fake.NewClientBuilder().
				WithScheme(scheme).
				WithObjects(objs...).
				WithStatusSubresource(
					&mcpv1beta1.VirtualMCPServer{},
					&mcpv1beta1.EmbeddingServer{},
				).
				Build()

			r := &VirtualMCPServerReconciler{
				Client:           fakeClient,
				Scheme:           scheme,
				PlatformDetector: ctrlutil.NewSharedPlatformDetector(),
			}

			statusManager := virtualmcpserverstatus.NewStatusManager(tt.vmcp)
			err := r.validateEmbeddingServerRef(context.Background(), tt.vmcp, statusManager)
			// Apply status updates for test assertions
			_ = statusManager.UpdateStatus(context.Background(), &tt.vmcp.Status)

			if tt.expectError {
				assert.Error(t, err)
				assert.Equal(t, tt.expectedPhase, tt.vmcp.Status.Phase)

				// Check condition reason
				for _, cond := range tt.vmcp.Status.Conditions {
					if cond.Type == mcpv1beta1.ConditionTypeEmbeddingServerReady {
						assert.Equal(t, tt.expectedReason, cond.Reason)
						assert.Equal(t, metav1.ConditionFalse, cond.Status)
					}
				}
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

// TestVirtualMCPServerEnsureDeployment_ReplicaSync_SpecDriven verifies that when
// spec.replicas is set, ensureDeployment updates the Deployment to match.
func TestVirtualMCPServerEnsureDeployment_ReplicaSync_SpecDriven(t *testing.T) {
	t.Parallel()

	specReplicas := int32(3)
	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "vmcp-replica-sync",
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
			Replicas: &specReplicas,
		},
	}

	mcpGroup := &mcpv1beta1.MCPGroup{
		ObjectMeta: metav1.ObjectMeta{Name: testGroupName, Namespace: "default"},
		Status:     mcpv1beta1.MCPGroupStatus{Phase: mcpv1beta1.MCPGroupPhaseReady},
	}

	configMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmcpConfigMapName(vmcp.Name),
			Namespace: "default",
			Annotations: map[string]string{
				checksum.ContentChecksumAnnotation: testChecksumValue,
			},
		},
		Data: map[string]string{"config.yaml": "{}"},
	}

	// Existing deployment has 1 replica — simulates a pre-existing state
	existingReplicas := int32(1)
	existingDeployment := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmcp.Name,
			Namespace: "default",
			Labels:    labelsForVirtualMCPServer(vmcp.Name),
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &existingReplicas,
			Selector: &metav1.LabelSelector{MatchLabels: labelsForVirtualMCPServer(vmcp.Name)},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{Labels: labelsForVirtualMCPServer(vmcp.Name)},
				Spec:       corev1.PodSpec{Containers: []corev1.Container{{Name: "vmcp", Image: "test:latest"}}},
			},
		},
	}

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp, mcpGroup, configMap, existingDeployment).
		Build()

	r := &VirtualMCPServerReconciler{
		Client:           fakeClient,
		Scheme:           scheme,
		PlatformDetector: ctrlutil.NewSharedPlatformDetector(),
	}

	result, err := r.ensureDeployment(context.Background(), vmcp, nil, []workloads.TypedWorkload{})
	require.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)

	updated := &appsv1.Deployment{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name: vmcp.Name, Namespace: vmcp.Namespace,
	}, updated)
	require.NoError(t, err)
	require.NotNil(t, updated.Spec.Replicas)
	assert.Equal(t, int32(3), *updated.Spec.Replicas)
}

// TestVirtualMCPServerEnsureDeployment_ReplicaSync_NilPassthrough verifies that when
// spec.replicas is nil, ensureDeployment does not overwrite a live replica count (HPA-managed).
func TestVirtualMCPServerEnsureDeployment_ReplicaSync_NilPassthrough(t *testing.T) {
	t.Parallel()

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "vmcp-nil-passthrough",
			Namespace: "default",
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
			Replicas: nil, // HPA manages replicas
		},
	}

	mcpGroup := &mcpv1beta1.MCPGroup{
		ObjectMeta: metav1.ObjectMeta{Name: testGroupName, Namespace: "default"},
		Status:     mcpv1beta1.MCPGroupStatus{Phase: mcpv1beta1.MCPGroupPhaseReady},
	}

	configMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmcpConfigMapName(vmcp.Name),
			Namespace: "default",
			Annotations: map[string]string{
				checksum.ContentChecksumAnnotation: testChecksumValue,
			},
		},
		Data: map[string]string{"config.yaml": "{}"},
	}

	// Existing deployment has 5 replicas — set by HPA
	hpaReplicas := int32(5)
	existingDeployment := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmcp.Name,
			Namespace: "default",
			Labels:    labelsForVirtualMCPServer(vmcp.Name),
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &hpaReplicas,
			Selector: &metav1.LabelSelector{MatchLabels: labelsForVirtualMCPServer(vmcp.Name)},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{Labels: labelsForVirtualMCPServer(vmcp.Name)},
				Spec:       corev1.PodSpec{Containers: []corev1.Container{{Name: "vmcp", Image: "test:latest"}}},
			},
		},
	}

	scheme := runtime.NewScheme()
	_ = mcpv1beta1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(vmcp, mcpGroup, configMap, existingDeployment).
		Build()

	r := &VirtualMCPServerReconciler{
		Client:           fakeClient,
		Scheme:           scheme,
		PlatformDetector: ctrlutil.NewSharedPlatformDetector(),
	}

	result, err := r.ensureDeployment(context.Background(), vmcp, nil, []workloads.TypedWorkload{})
	require.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, result)

	updated := &appsv1.Deployment{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{
		Name: vmcp.Name, Namespace: vmcp.Namespace,
	}, updated)
	require.NoError(t, err)
	// HPA-managed replica count must not be overwritten
	require.NotNil(t, updated.Spec.Replicas)
	assert.Equal(t, int32(5), *updated.Spec.Replicas)
}

// mustBuildEnvVarsForVmcp is a test helper that calls buildEnvVarsForVmcp and panics on error.
// All test VirtualMCPServers use anonymous auth (no OIDCConfigRef), so the error path is unreachable.
func mustBuildEnvVarsForVmcp(r *VirtualMCPServerReconciler, vmcp *mcpv1beta1.VirtualMCPServer) []corev1.EnvVar {
	env, err := r.buildEnvVarsForVmcp(context.Background(), vmcp, nil, []workloads.TypedWorkload{})
	if err != nil {
		panic("mustBuildEnvVarsForVmcp: " + err.Error())
	}
	return env
}

// TestGetExternalAuthConfigNameFromWorkload tests auth config ref extraction from all workload types
func TestGetExternalAuthConfigNameFromWorkload(t *testing.T) {
	t.Parallel()

	mcpServerMap := map[string]*mcpv1beta1.MCPServer{
		"server-with-auth": {
			Spec: mcpv1beta1.MCPServerSpec{
				ExternalAuthConfigRef: &mcpv1beta1.ExternalAuthConfigRef{
					Name: "server-auth-config",
				},
			},
		},
		"server-no-auth": {
			Spec: mcpv1beta1.MCPServerSpec{},
		},
	}

	mcpRemoteProxyMap := map[string]*mcpv1beta1.MCPRemoteProxy{
		"proxy-with-auth": {
			Spec: mcpv1beta1.MCPRemoteProxySpec{
				ExternalAuthConfigRef: &mcpv1beta1.ExternalAuthConfigRef{
					Name: "proxy-auth-config",
				},
			},
		},
	}

	mcpServerEntryMap := map[string]*mcpv1beta1.MCPServerEntry{
		"entry-with-auth": {
			Spec: mcpv1beta1.MCPServerEntrySpec{
				ExternalAuthConfigRef: &mcpv1beta1.ExternalAuthConfigRef{
					Name: "entry-auth-config",
				},
			},
		},
		"entry-no-auth": {
			Spec: mcpv1beta1.MCPServerEntrySpec{},
		},
	}

	tests := []struct {
		name         string
		workload     workloads.TypedWorkload
		expectedName string
	}{
		{
			name: "MCPServer with auth config ref",
			workload: workloads.TypedWorkload{
				Name: "server-with-auth",
				Type: workloads.WorkloadTypeMCPServer,
			},
			expectedName: "server-auth-config",
		},
		{
			name: "MCPServer without auth config ref",
			workload: workloads.TypedWorkload{
				Name: "server-no-auth",
				Type: workloads.WorkloadTypeMCPServer,
			},
			expectedName: "",
		},
		{
			name: "MCPServer not found in map",
			workload: workloads.TypedWorkload{
				Name: "non-existent",
				Type: workloads.WorkloadTypeMCPServer,
			},
			expectedName: "",
		},
		{
			name: "MCPRemoteProxy with auth config ref",
			workload: workloads.TypedWorkload{
				Name: "proxy-with-auth",
				Type: workloads.WorkloadTypeMCPRemoteProxy,
			},
			expectedName: "proxy-auth-config",
		},
		{
			name: "MCPServerEntry with auth config ref",
			workload: workloads.TypedWorkload{
				Name: "entry-with-auth",
				Type: workloads.WorkloadTypeMCPServerEntry,
			},
			expectedName: "entry-auth-config",
		},
		{
			name: "MCPServerEntry without auth config ref",
			workload: workloads.TypedWorkload{
				Name: "entry-no-auth",
				Type: workloads.WorkloadTypeMCPServerEntry,
			},
			expectedName: "",
		},
		{
			name: "MCPServerEntry not found in map",
			workload: workloads.TypedWorkload{
				Name: "non-existent-entry",
				Type: workloads.WorkloadTypeMCPServerEntry,
			},
			expectedName: "",
		},
		{
			name: "unknown workload type",
			workload: workloads.TypedWorkload{
				Name: "unknown",
				Type: workloads.WorkloadType("UnknownType"),
			},
			expectedName: "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			r := &VirtualMCPServerReconciler{}
			result := r.getExternalAuthConfigNameFromWorkload(
				tt.workload,
				mcpServerMap,
				mcpRemoteProxyMap,
				mcpServerEntryMap,
			)
			assert.Equal(t, tt.expectedName, result)
		})
	}
}

// TestDiscoveredRBACRulesIncludeMCPServerEntries verifies that the RBAC rules
// for discovered mode include mcpserverentries as an allowed resource
func TestDiscoveredRBACRulesIncludeMCPServerEntries(t *testing.T) {
	t.Parallel()

	foundMCPServerEntries := false
	for _, rule := range vmcpDiscoveredRBACRules {
		for _, apiGroup := range rule.APIGroups {
			if apiGroup == "toolhive.stacklok.dev" {
				for _, resource := range rule.Resources {
					if resource == "mcpserverentries" {
						foundMCPServerEntries = true
					}
				}
			}
		}
	}
	assert.True(t, foundMCPServerEntries, "vmcpDiscoveredRBACRules should include mcpserverentries")
}

// TestVirtualMCPServerValidateAuthzUpstreamAvailable verifies that the
// validator fires only when the embedded AuthServer is configured without any
// upstream providers alongside AuthzConfig. Direct-IdP flows (clients present
// an already-validated IdP token) leave AuthServerConfig nil and are valid —
// Cedar evaluates against the identity's claims via the default branch.
//
// The validator also emits an advisory AuthzUpstreamSelectionWarning condition
// when multiple upstreams are declared, naming the auto-selected provider.
func TestVirtualMCPServerValidateAuthzUpstreamAvailable(t *testing.T) {
	t.Parallel()

	inlineAuthzRef := &mcpv1beta1.AuthzConfigRef{
		Type: "inline",
		Inline: &mcpv1beta1.InlineAuthzConfig{
			Policies: []string{`permit(principal, action, resource);`},
		},
	}

	// authzRefWithPrimary builds an inline authz ref with an explicit
	// PrimaryUpstreamProvider — used to exercise the override branch.
	authzRefWithPrimary := func(primary string) *mcpv1beta1.AuthzConfigRef {
		return &mcpv1beta1.AuthzConfigRef{
			Type: "inline",
			Inline: &mcpv1beta1.InlineAuthzConfig{
				Policies:                []string{`permit(principal, action, resource);`},
				PrimaryUpstreamProvider: primary,
			},
		}
	}

	// warningExpectation captures the expected state of the advisory
	// AuthzUpstreamSelectionWarning condition after validation. When
	// expectPresent is false the condition must not appear in status at
	// all — the advisory only applies to the narrow multi-upstream slice.
	type warningExpectation struct {
		expectPresent bool
		status        metav1.ConditionStatus
		reason        string
		messageSubstr string // empty when we don't care about the message
	}

	tests := []struct {
		name             string
		incomingAuth     *mcpv1beta1.IncomingAuthConfig
		authServerConfig *mcpv1beta1.EmbeddedAuthServerConfig
		expectError      bool
		expectedReason   string
		expectedWarning  warningExpectation
	}{
		{
			name:            "no incoming auth is valid",
			incomingAuth:    nil,
			expectedWarning: warningExpectation{expectPresent: false},
		},
		{
			name: "incoming auth without authz is valid",
			incomingAuth: &mcpv1beta1.IncomingAuthConfig{
				Type: "anonymous",
			},
			expectedWarning: warningExpectation{expectPresent: false},
		},
		{
			name: "authz with nil auth server config is valid (direct IdP flow)",
			incomingAuth: &mcpv1beta1.IncomingAuthConfig{
				Type:        "oidc",
				AuthzConfig: inlineAuthzRef,
			},
			authServerConfig: nil,
			expectError:      false,
			expectedWarning:  warningExpectation{expectPresent: false},
		},
		{
			name: "authz with empty upstream providers is invalid",
			incomingAuth: &mcpv1beta1.IncomingAuthConfig{
				Type:        "oidc",
				AuthzConfig: inlineAuthzRef,
			},
			authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
				Issuer:            "https://authserver.example.com",
				UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{},
			},
			expectError:     true,
			expectedReason:  mcpv1beta1.ConditionReasonAuthzRequiresUpstream,
			expectedWarning: warningExpectation{expectPresent: false},
		},
		{
			name: "authz with single upstream is valid",
			incomingAuth: &mcpv1beta1.IncomingAuthConfig{
				Type:        "oidc",
				AuthzConfig: inlineAuthzRef,
			},
			authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
				Issuer: "https://authserver.example.com",
				UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{
					{Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
				},
			},
			expectedWarning: warningExpectation{expectPresent: false},
		},
		{
			name: "authz with multiple upstreams emits advisory warning",
			incomingAuth: &mcpv1beta1.IncomingAuthConfig{
				Type:        "oidc",
				AuthzConfig: inlineAuthzRef,
			},
			authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
				Issuer: "https://authserver.example.com",
				UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{
					{Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
					{Name: "entra", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
				},
			},
			expectedWarning: warningExpectation{
				expectPresent: true,
				status:        metav1.ConditionTrue,
				reason:        mcpv1beta1.ConditionReasonAuthzUpstreamAutoSelected,
				messageSubstr: `"okta"`,
			},
		},
		{
			// Explicit PrimaryUpstreamProvider matching one of the upstreams is
			// valid and emits no advisory — the user has disambiguated the choice.
			name: "explicit primary provider matching an upstream is valid",
			incomingAuth: &mcpv1beta1.IncomingAuthConfig{
				Type:        "oidc",
				AuthzConfig: authzRefWithPrimary("entra"),
			},
			authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
				Issuer: "https://authserver.example.com",
				UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{
					{Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
					{Name: "entra", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
				},
			},
			expectedWarning: warningExpectation{expectPresent: false},
		},
		{
			// Explicit PrimaryUpstreamProvider with multiple upstreams suppresses
			// the advisory warning — auto-selection is no longer happening.
			name: "explicit primary provider suppresses multi-upstream advisory",
			incomingAuth: &mcpv1beta1.IncomingAuthConfig{
				Type:        "oidc",
				AuthzConfig: authzRefWithPrimary("okta"),
			},
			authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
				Issuer: "https://authserver.example.com",
				UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{
					{Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
					{Name: "entra", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
					{Name: "google", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
				},
			},
			expectedWarning: warningExpectation{expectPresent: false},
		},
		{
			// Explicit PrimaryUpstreamProvider that does not match any declared
			// upstream is rejected at admission. Cedar would otherwise deny every
			// request at runtime; failing loudly is the right behavior.
			name: "explicit primary provider not matching any upstream is invalid",
			incomingAuth: &mcpv1beta1.IncomingAuthConfig{
				Type:        "oidc",
				AuthzConfig: authzRefWithPrimary("ping"),
			},
			authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
				Issuer: "https://authserver.example.com",
				UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{
					{Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
					{Name: "entra", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
				},
			},
			expectError:     true,
			expectedReason:  mcpv1beta1.ConditionReasonAuthzUpstreamUnknown,
			expectedWarning: warningExpectation{expectPresent: false},
		},
		{
			// Explicit PrimaryUpstreamProvider with no embedded auth server at
			// all is rejected at admission. The field names an upstream IDP on
			// the embedded AS — without an AS there is nothing for it to refer
			// to, and the converter would otherwise forward an unresolvable
			// name. Distinct condition reason from the upstream-mismatch case
			// so tooling can route the two misconfigurations separately.
			name: "explicit primary provider without embedded auth server is invalid",
			incomingAuth: &mcpv1beta1.IncomingAuthConfig{
				Type:        "oidc",
				AuthzConfig: authzRefWithPrimary("okta"),
			},
			authServerConfig: nil,
			expectError:      true,
			expectedReason:   mcpv1beta1.ConditionReasonAuthzPrimaryProviderRequiresAuthServer,
			expectedWarning:  warningExpectation{expectPresent: false},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			vmcp := &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:       testVmcpName,
					Namespace:  "default",
					Generation: 1,
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef:         &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					IncomingAuth:     tt.incomingAuth,
					AuthServerConfig: tt.authServerConfig,
				},
			}

			r := &VirtualMCPServerReconciler{}
			statusManager := virtualmcpserverstatus.NewStatusManager(vmcp)
			err := r.validateAuthzUpstreamAvailable(t.Context(), vmcp, statusManager)

			if tt.expectError {
				require.Error(t, err)
				// Error path writes phase, message, and the AuthServerConfigValidated
				// condition — UpdateStatus must report a change.
				assert.True(t, statusManager.UpdateStatus(t.Context(), &vmcp.Status))
				assert.Equal(t, mcpv1beta1.VirtualMCPServerPhaseFailed, vmcp.Status.Phase)
				assert.NotEmpty(t, vmcp.Status.Message)

				found := false
				for _, cond := range vmcp.Status.Conditions {
					if cond.Type == mcpv1beta1.ConditionTypeAuthServerConfigValidated {
						found = true
						assert.Equal(t, metav1.ConditionFalse, cond.Status)
						assert.Equal(t, tt.expectedReason, cond.Reason)
					}
				}
				assert.True(t, found, "AuthServerConfigValidated condition should be set to False")
			} else {
				require.NoError(t, err)
				// Positive path: apply any pending status changes (only the
				// multi-upstream case emits the advisory; other valid paths
				// leave the collector unchanged).
				_ = statusManager.UpdateStatus(t.Context(), &vmcp.Status)
				assert.NotEqual(t, mcpv1beta1.VirtualMCPServerPhaseFailed, vmcp.Status.Phase)
				for _, cond := range vmcp.Status.Conditions {
					if cond.Type == mcpv1beta1.ConditionTypeAuthServerConfigValidated {
						assert.NotEqual(t, mcpv1beta1.ConditionReasonAuthzRequiresUpstream, cond.Reason)
					}
				}
			}

			// The advisory AuthzUpstreamSelectionWarning condition should only
			// appear on the narrow multi-upstream path. Every other path must
			// leave it absent so kubectl describe stays clean.
			var warning *metav1.Condition
			for i := range vmcp.Status.Conditions {
				if vmcp.Status.Conditions[i].Type == mcpv1beta1.ConditionTypeAuthzUpstreamSelectionWarning {
					warning = &vmcp.Status.Conditions[i]
					break
				}
			}
			if !tt.expectedWarning.expectPresent {
				assert.Nil(t, warning, "AuthzUpstreamSelectionWarning condition should not be present")
				return
			}
			require.NotNil(t, warning, "AuthzUpstreamSelectionWarning condition should be present")
			assert.Equal(t, tt.expectedWarning.status, warning.Status)
			assert.Equal(t, tt.expectedWarning.reason, warning.Reason)
			if tt.expectedWarning.messageSubstr != "" {
				assert.Contains(t, warning.Message, tt.expectedWarning.messageSubstr)
			}
		})
	}
}

// TestVirtualMCPServerValidateAuthzUpstreamAvailable_ClearsStaleWarning verifies
// the transition case: a VMCP that was previously multi-upstream (advisory True
// on its status) is reconfigured to a single upstream, and the stale advisory
// condition must be removed after the next validation pass.
func TestVirtualMCPServerValidateAuthzUpstreamAvailable_ClearsStaleWarning(t *testing.T) {
	t.Parallel()

	inlineAuthzRef := &mcpv1beta1.AuthzConfigRef{
		Type: "inline",
		Inline: &mcpv1beta1.InlineAuthzConfig{
			Policies: []string{`permit(principal, action, resource);`},
		},
	}

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:       testVmcpName,
			Namespace:  "default",
			Generation: 2,
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
			IncomingAuth: &mcpv1beta1.IncomingAuthConfig{
				Type:        "oidc",
				AuthzConfig: inlineAuthzRef,
			},
			// Single upstream now — the advisory should be cleared.
			AuthServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
				Issuer: "https://authserver.example.com",
				UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{
					{Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
				},
			},
		},
		Status: mcpv1beta1.VirtualMCPServerStatus{
			// Simulate a stale True advisory from a previous multi-upstream
			// reconciliation.
			Conditions: []metav1.Condition{
				{
					Type:    mcpv1beta1.ConditionTypeAuthzUpstreamSelectionWarning,
					Status:  metav1.ConditionTrue,
					Reason:  mcpv1beta1.ConditionReasonAuthzUpstreamAutoSelected,
					Message: `multiple upstreamProviders configured; Cedar policies will evaluate claims from the first upstream ("okta").`,
				},
			},
		},
	}

	r := &VirtualMCPServerReconciler{}
	statusManager := virtualmcpserverstatus.NewStatusManager(vmcp)
	require.NoError(t, r.validateAuthzUpstreamAvailable(t.Context(), vmcp, statusManager))

	// Applying the status should remove the stale condition.
	assert.True(t, statusManager.UpdateStatus(t.Context(), &vmcp.Status),
		"UpdateStatus must report a change because a stale condition was removed")

	for _, cond := range vmcp.Status.Conditions {
		assert.NotEqual(t, mcpv1beta1.ConditionTypeAuthzUpstreamSelectionWarning, cond.Type,
			"stale AuthzUpstreamSelectionWarning condition should have been removed")
	}
}

// TestVirtualMCPServerValidateAuthzUpstreamAvailable_ConfigMapFallThrough
// pins the documented fall-through contract for configMap-sourced authz: the
// new admission rejections never fire because primaryUpstreamProvider lives on
// InlineAuthzConfig only. ConfigMap users get auto-selection of the first
// upstream and the multi-upstream advisory, identical to inline users with no
// explicit override. Locks the inline-only contract until the configMap
// loader (TODO #5208) is implemented.
func TestVirtualMCPServerValidateAuthzUpstreamAvailable_ConfigMapFallThrough(t *testing.T) {
	t.Parallel()

	configMapAuthzRef := &mcpv1beta1.AuthzConfigRef{
		Type: "configMap",
		ConfigMap: &mcpv1beta1.ConfigMapAuthzRef{
			Name: "authz-policies",
			Key:  "authz.json",
		},
	}

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:       testVmcpName,
			Namespace:  "default",
			Generation: 1,
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
			IncomingAuth: &mcpv1beta1.IncomingAuthConfig{
				Type:        "oidc",
				AuthzConfig: configMapAuthzRef,
			},
			AuthServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
				Issuer: "https://authserver.example.com",
				UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{
					{Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
					{Name: "entra", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
				},
			},
		},
	}

	r := &VirtualMCPServerReconciler{}
	statusManager := virtualmcpserverstatus.NewStatusManager(vmcp)
	require.NoError(t, r.validateAuthzUpstreamAvailable(t.Context(), vmcp, statusManager))
	statusManager.UpdateStatus(t.Context(), &vmcp.Status)

	advisoryFound := false
	for _, cond := range vmcp.Status.Conditions {
		if cond.Type == mcpv1beta1.ConditionTypeAuthzUpstreamSelectionWarning {
			advisoryFound = true
			assert.Equal(t, metav1.ConditionTrue, cond.Status)
			assert.Equal(t, mcpv1beta1.ConditionReasonAuthzUpstreamAutoSelected, cond.Reason)
		}
		assert.NotEqual(t, mcpv1beta1.ConditionReasonAuthzPrimaryProviderRequiresAuthServer, cond.Reason,
			"configMap-sourced authz should not trip the no-AS rejection")
		assert.NotEqual(t, mcpv1beta1.ConditionReasonAuthzUpstreamUnknown, cond.Reason,
			"configMap-sourced authz should not trip the upstream-mismatch rejection")
	}
	assert.True(t, advisoryFound, "multi-upstream advisory should be present for configMap authz")
}

// TestVirtualMCPServerValidateAuthzUpstreamAvailable_ClearsStaleAuthzUnknown
// verifies the recovery path for the new failure reasons: a VMCP that was
// previously rejected with AuthServerConfigValidated=False (either reason)
// must transition back to a passing state after the spec is corrected.
// Without this, a fix-then-reconcile cycle would leave the VMCP stuck in
// Failed phase forever.
//
// AuthServerConfigValidated is co-owned by validateAuthServerConfig (sets True
// on success) and validateAuthzUpstreamAvailable (sets False on authz
// rejection). The True transition comes from validateAuthServerConfig running
// first; this test asserts validateAuthzUpstreamAvailable does NOT re-emit a
// False rejection on a corrected spec, so the True from the prior validator
// survives through to status.
func TestVirtualMCPServerValidateAuthzUpstreamAvailable_ClearsStaleAuthzUnknown(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name        string
		staleReason string
	}{
		{
			name:        "recovers from AuthzUpstreamUnknown after fixing the explicit name",
			staleReason: mcpv1beta1.ConditionReasonAuthzUpstreamUnknown,
		},
		{
			name:        "recovers from AuthzPrimaryProviderRequiresAuthServer after configuring AS",
			staleReason: mcpv1beta1.ConditionReasonAuthzPrimaryProviderRequiresAuthServer,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			authzRef := &mcpv1beta1.AuthzConfigRef{
				Type: "inline",
				Inline: &mcpv1beta1.InlineAuthzConfig{
					Policies:                []string{`permit(principal, action, resource);`},
					PrimaryUpstreamProvider: "okta",
				},
			}

			vmcp := &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:       testVmcpName,
					Namespace:  "default",
					Generation: 2,
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					IncomingAuth: &mcpv1beta1.IncomingAuthConfig{
						Type:        "oidc",
						AuthzConfig: authzRef,
					},
					// Spec is now valid: explicit "okta" matches a declared upstream.
					AuthServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
						Issuer: "https://authserver.example.com",
						UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{
							{Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
						},
					},
				},
				Status: mcpv1beta1.VirtualMCPServerStatus{
					Phase: mcpv1beta1.VirtualMCPServerPhaseFailed,
					Conditions: []metav1.Condition{
						{
							Type:    mcpv1beta1.ConditionTypeAuthServerConfigValidated,
							Status:  metav1.ConditionFalse,
							Reason:  tt.staleReason,
							Message: "previous rejection from before the spec was fixed",
						},
					},
				},
			}

			r := &VirtualMCPServerReconciler{}
			statusManager := virtualmcpserverstatus.NewStatusManager(vmcp)

			// Mirror the production reconcile order: AuthServerConfig validates
			// first (sets AuthServerConfigValidated=True on success, overwriting
			// the stale False), then the authz validator runs.
			require.NoError(t, r.validateAuthServerConfig(vmcp, statusManager))
			require.NoError(t, r.validateAuthzUpstreamAvailable(t.Context(), vmcp, statusManager))
			statusManager.UpdateStatus(t.Context(), &vmcp.Status)

			cond := meta.FindStatusCondition(vmcp.Status.Conditions, mcpv1beta1.ConditionTypeAuthServerConfigValidated)
			require.NotNil(t, cond, "AuthServerConfigValidated condition should be present after recovery")
			assert.Equal(t, metav1.ConditionTrue, cond.Status,
				"AuthServerConfigValidated must transition back to True after the spec is corrected")
			assert.NotEqual(t, tt.staleReason, cond.Reason,
				"stale rejection reason must not survive the recovery cycle")
		})
	}
}

// TestVirtualMCPServerValidateAuthServerConfig_IdentitySynthesizedCondition
// is the parity test: same condition shape as MCPExternalAuthConfig emits
// for the same upstreamProviders, on a VirtualMCPServer's inline AuthServerConfig.
func TestVirtualMCPServerValidateAuthServerConfig_IdentitySynthesizedCondition(t *testing.T) {
	t.Parallel()

	oauth2Upstream := func(name string, withUserInfo bool) mcpv1beta1.UpstreamProviderConfig {
		cfg := &mcpv1beta1.OAuth2UpstreamConfig{
			AuthorizationEndpoint: "https://idp.example.com/authorize",
			TokenEndpoint:         "https://idp.example.com/token",
			ClientID:              "client",
		}
		if withUserInfo {
			cfg.UserInfo = &mcpv1beta1.UserInfoConfig{EndpointURL: "https://idp.example.com/userinfo"}
		}
		return mcpv1beta1.UpstreamProviderConfig{
			Name:         name,
			Type:         mcpv1beta1.UpstreamProviderTypeOAuth2,
			OAuth2Config: cfg,
		}
	}

	tests := []struct {
		name           string
		upstreams      []mcpv1beta1.UpstreamProviderConfig
		wantStatus     metav1.ConditionStatus
		wantReason     string
		wantNamesInMsg []string
	}{
		{
			name:       "all OAuth2 upstreams have userInfo: condition False",
			upstreams:  []mcpv1beta1.UpstreamProviderConfig{oauth2Upstream("primary", true)},
			wantStatus: metav1.ConditionFalse,
			wantReason: mcpv1beta1.ConditionReasonIdentitySynthesizedInactive,
		},
		{
			name: "one OAuth2 upstream missing userInfo: condition True with name in message",
			upstreams: []mcpv1beta1.UpstreamProviderConfig{
				oauth2Upstream("primary", true),
				oauth2Upstream("atlassian", false),
			},
			wantStatus:     metav1.ConditionTrue,
			wantReason:     mcpv1beta1.ConditionReasonIdentitySynthesizedActive,
			wantNamesInMsg: []string{"atlassian"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			vmcp := &mcpv1beta1.VirtualMCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:       testVmcpName,
					Namespace:  "default",
					Generation: 1,
				},
				Spec: mcpv1beta1.VirtualMCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
					AuthServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
						Issuer:            "https://authserver.example.com",
						UpstreamProviders: tt.upstreams,
					},
				},
			}

			r := &VirtualMCPServerReconciler{}
			statusManager := virtualmcpserverstatus.NewStatusManager(vmcp)
			// runAuthValidations runs the synthesis advisory before
			// validateAuthServerConfig so the condition tracks the spec on both
			// pass and fail paths. Mirror that ordering here.
			r.applyAuthServerIdentitySynthesizedCondition(vmcp, statusManager)
			require.NoError(t, r.validateAuthServerConfig(vmcp, statusManager))
			statusManager.UpdateStatus(t.Context(), &vmcp.Status)

			cond := findCondition(vmcp.Status.Conditions, mcpv1beta1.ConditionTypeIdentitySynthesized)
			require.NotNil(t, cond, "IdentitySynthesized condition should be set on a valid AuthServerConfig")
			assert.Equal(t, tt.wantStatus, cond.Status)
			assert.Equal(t, tt.wantReason, cond.Reason)
			for _, name := range tt.wantNamesInMsg {
				assert.Contains(t, cond.Message, name,
					"upstream %q should be named in the condition message", name)
			}
		})
	}
}

// TestVirtualMCPServerReconciler_IdentitySynthesizedTransitionsOnValidationFailure
// pins the contract that the IdentitySynthesized advisory is recomputed from
// the current spec on every reconcile, including paths where
// validateAuthServerConfig early-returns (Issuer == "", empty UpstreamProviders,
// invalid AdditionalAuthorizationParams). Without this, breaking the spec
// after a synthesizing upstream was reported leaves a stale True/upstream-name
// dangling next to the new AuthServerConfigValidated=False.
func TestVirtualMCPServerReconciler_IdentitySynthesizedTransitionsOnValidationFailure(t *testing.T) {
	t.Parallel()

	syntheticUpstream := mcpv1beta1.UpstreamProviderConfig{
		Name: "atlassian",
		Type: mcpv1beta1.UpstreamProviderTypeOAuth2,
		OAuth2Config: &mcpv1beta1.OAuth2UpstreamConfig{
			AuthorizationEndpoint: "https://idp.example.com/authorize",
			TokenEndpoint:         "https://idp.example.com/token",
			ClientID:              "client",
			// UserInfo intentionally nil — synthesizes identity.
		},
	}

	vmcp := &mcpv1beta1.VirtualMCPServer{
		ObjectMeta: metav1.ObjectMeta{
			Name:       testVmcpName,
			Namespace:  "default",
			Generation: 1,
		},
		Spec: mcpv1beta1.VirtualMCPServerSpec{
			GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupName},
			AuthServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
				Issuer:            "https://authserver.example.com",
				UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{syntheticUpstream},
			},
		},
	}

	r := &VirtualMCPServerReconciler{}

	// Pass 1: valid spec with synthesizing upstream.
	statusManager := virtualmcpserverstatus.NewStatusManager(vmcp)
	r.applyAuthServerIdentitySynthesizedCondition(vmcp, statusManager)
	require.NoError(t, r.validateAuthServerConfig(vmcp, statusManager))
	statusManager.UpdateStatus(t.Context(), &vmcp.Status)

	cond := findCondition(vmcp.Status.Conditions, mcpv1beta1.ConditionTypeIdentitySynthesized)
	require.NotNil(t, cond, "synthesizing upstream should produce IdentitySynthesized condition")
	assert.Equal(t, metav1.ConditionTrue, cond.Status)
	assert.Equal(t, mcpv1beta1.ConditionReasonIdentitySynthesizedActive, cond.Reason)
	assert.Contains(t, cond.Message, "atlassian", "initial message must name the synthesizing upstream")

	// Pass 2: mutate the spec to break validation. Empty Issuer triggers the
	// first early-return in validateAuthServerConfig and removes the
	// synthesizing upstream that the prior message names.
	vmcp.Spec.AuthServerConfig.Issuer = ""
	vmcp.Spec.AuthServerConfig.UpstreamProviders = nil
	vmcp.Generation = 2

	statusManager = virtualmcpserverstatus.NewStatusManager(vmcp)
	r.applyAuthServerIdentitySynthesizedCondition(vmcp, statusManager)
	require.Error(t, r.validateAuthServerConfig(vmcp, statusManager),
		"empty Issuer must fail validation")
	statusManager.UpdateStatus(t.Context(), &vmcp.Status)

	cond = findCondition(vmcp.Status.Conditions, mcpv1beta1.ConditionTypeIdentitySynthesized)
	require.NotNil(t, cond, "advisory must be recomputed on the validation-failure path, not left stale")
	assert.Equal(t, metav1.ConditionFalse, cond.Status,
		"empty upstream list has no synthesizing providers; advisory must flip to False")
	assert.Equal(t, mcpv1beta1.ConditionReasonIdentitySynthesizedInactive, cond.Reason)
	assert.NotContains(t, cond.Message, "atlassian",
		"stale message naming the now-removed upstream must not survive the broken edit")
}
