package controller

import (
	"context"
	"fmt"
	"testing"

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

	tangledv1alpha1 "github.com/josie/knot-operator/api/v1alpha1"
)

func newTestScheme() *runtime.Scheme {
	scheme := runtime.NewScheme()
	_ = tangledv1alpha1.AddToScheme(scheme)
	_ = corev1.AddToScheme(scheme)
	_ = appsv1.AddToScheme(scheme)
	_ = networkingv1.AddToScheme(scheme)
	return scheme
}

func newTestReconciler(objs ...client.Object) *KnotReconciler {
	scheme := newTestScheme()
	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(objs...).
		WithStatusSubresource(&tangledv1alpha1.Knot{}).
		Build()
	return &KnotReconciler{
		Client: fakeClient,
		Scheme: scheme,
	}
}

func newMinimalKnot(name, namespace string) *tangledv1alpha1.Knot {
	return &tangledv1alpha1.Knot{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Spec: tangledv1alpha1.KnotSpec{
			Hostname: "knot.example.com",
			Owner:    "did:plc:test",
		},
	}
}

func TestSetDefaults(t *testing.T) {
	tests := []struct {
		name     string
		spec     tangledv1alpha1.KnotSpec
		expected tangledv1alpha1.KnotSpec
	}{
		{
			name: "all defaults applied",
			spec: tangledv1alpha1.KnotSpec{
				Hostname: "knot.example.com",
				Owner:    "did:plc:test",
			},
			expected: tangledv1alpha1.KnotSpec{
				Image:           "docker.io/tngl/knot:v1.10.0-alpha",
				ImagePullPolicy: corev1.PullIfNotPresent,
				Replicas:        1,
				Hostname:        "knot.example.com",
				Owner:           "did:plc:test",
				AppviewEndpoint: "https://tangled.org",
				Storage: tangledv1alpha1.KnotStorageSpec{
					RepoSize: "10Gi",
					DBSize:   "1Gi",
					RepoPath: "/data/repos",
					DBPath:   "/data/db",
				},
			},
		},
		{
			name: "custom values preserved",
			spec: tangledv1alpha1.KnotSpec{
				Image:           "custom/image:v1.0.0",
				ImagePullPolicy: corev1.PullAlways,
				Replicas:        3,
				Hostname:        "knot.example.com",
				Owner:           "did:plc:test",
				AppviewEndpoint: "https://custom.endpoint.com",
				Storage: tangledv1alpha1.KnotStorageSpec{
					RepoSize: "50Gi",
					DBSize:   "5Gi",
					RepoPath: "/custom/repos",
					DBPath:   "/custom/db",
				},
			},
			expected: tangledv1alpha1.KnotSpec{
				Image:           "custom/image:v1.0.0",
				ImagePullPolicy: corev1.PullAlways,
				Replicas:        3,
				Hostname:        "knot.example.com",
				Owner:           "did:plc:test",
				AppviewEndpoint: "https://custom.endpoint.com",
				Storage: tangledv1alpha1.KnotStorageSpec{
					RepoSize: "50Gi",
					DBSize:   "5Gi",
					RepoPath: "/custom/repos",
					DBPath:   "/custom/db",
				},
			},
		},
		{
			name: "partial defaults",
			spec: tangledv1alpha1.KnotSpec{
				Image:    "custom/image:v2.0.0",
				Replicas: 2,
				Hostname: "knot.example.com",
				Owner:    "did:plc:test",
				Storage: tangledv1alpha1.KnotStorageSpec{
					RepoSize: "20Gi",
				},
			},
			expected: tangledv1alpha1.KnotSpec{
				Image:           "custom/image:v2.0.0",
				ImagePullPolicy: corev1.PullIfNotPresent,
				Replicas:        2,
				Hostname:        "knot.example.com",
				Owner:           "did:plc:test",
				AppviewEndpoint: "https://tangled.org",
				Storage: tangledv1alpha1.KnotStorageSpec{
					RepoSize: "20Gi",
					DBSize:   "1Gi",
					RepoPath: "/data/repos",
					DBPath:   "/data/db",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			knot := &tangledv1alpha1.Knot{
				Spec: tt.spec,
			}
			r := &KnotReconciler{}
			r.setDefaults(knot)

			assert.Equal(t, tt.expected.Image, knot.Spec.Image)
			assert.Equal(t, tt.expected.ImagePullPolicy, knot.Spec.ImagePullPolicy)
			assert.Equal(t, tt.expected.Replicas, knot.Spec.Replicas)
			assert.Equal(t, tt.expected.AppviewEndpoint, knot.Spec.AppviewEndpoint)
			assert.Equal(t, tt.expected.Storage.RepoSize, knot.Spec.Storage.RepoSize)
			assert.Equal(t, tt.expected.Storage.DBSize, knot.Spec.Storage.DBSize)
			assert.Equal(t, tt.expected.Storage.RepoPath, knot.Spec.Storage.RepoPath)
			assert.Equal(t, tt.expected.Storage.DBPath, knot.Spec.Storage.DBPath)
		})
	}
}

func TestBuildPVC(t *testing.T) {
	tests := []struct {
		name                 string
		knotName             string
		namespace            string
		suffix               string
		size                 string
		storageClass         string
		expectedName         string
		expectedStorageClass *string
	}{
		{
			name:                 "repo PVC without storage class",
			knotName:             "test-knot",
			namespace:            "default",
			suffix:               "repos",
			size:                 "10Gi",
			storageClass:         "",
			expectedName:         "test-knot-repos",
			expectedStorageClass: nil,
		},
		{
			name:         "repo PVC with custom storage class",
			knotName:     "my-knot",
			namespace:    "production",
			suffix:       "repos",
			size:         "100Gi",
			storageClass: "fast-storage",
			expectedName: "my-knot-repos",
			expectedStorageClass: func() *string {
				s := "fast-storage"
				return &s
			}(),
		},
		{
			name:                 "db PVC without storage class",
			knotName:             "test-knot",
			namespace:            "default",
			suffix:               "db",
			size:                 "1Gi",
			storageClass:         "",
			expectedName:         "test-knot-db",
			expectedStorageClass: nil,
		},
		{
			name:         "db PVC with custom storage class",
			knotName:     "my-knot",
			namespace:    "production",
			suffix:       "db",
			size:         "10Gi",
			storageClass: "ssd-storage",
			expectedName: "my-knot-db",
			expectedStorageClass: func() *string {
				s := "ssd-storage"
				return &s
			}(),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			knot := newMinimalKnot(tt.knotName, tt.namespace)

			r := &KnotReconciler{}
			pvc := r.buildPVC(knot, tt.suffix, tt.size, tt.storageClass)

			assert.Equal(t, tt.expectedName, pvc.Name)
			assert.Equal(t, tt.namespace, pvc.Namespace)
			assert.Equal(t, []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, pvc.Spec.AccessModes)
			assert.Equal(t, resource.MustParse(tt.size), pvc.Spec.Resources.Requests[corev1.ResourceStorage])

			if tt.expectedStorageClass == nil {
				assert.Nil(t, pvc.Spec.StorageClassName)
			} else {
				require.NotNil(t, pvc.Spec.StorageClassName)
				assert.Equal(t, *tt.expectedStorageClass, *pvc.Spec.StorageClassName)
			}

			assert.Equal(t, "knot", pvc.Labels["app.kubernetes.io/name"])
			assert.Equal(t, tt.knotName, pvc.Labels["app.kubernetes.io/instance"])
		})
	}
}

func TestBuildDeployment(t *testing.T) {
	t.Run("basic deployment", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Image = "test/image:v1"
		knot.Spec.ImagePullPolicy = corev1.PullAlways
		knot.Spec.Replicas = 2
		knot.Spec.Storage.RepoPath = "/data/repos"
		knot.Spec.Storage.DBPath = "/data/db"

		r := &KnotReconciler{}
		dep := r.buildDeployment(knot)

		assert.Equal(t, "test-knot", dep.Name)
		assert.Equal(t, "default", dep.Namespace)
		assert.Equal(t, int32(2), *dep.Spec.Replicas)

		require.Len(t, dep.Spec.Template.Spec.Containers, 1)
		container := dep.Spec.Template.Spec.Containers[0]

		assert.Equal(t, "knot", container.Name)
		assert.Equal(t, "test/image:v1", container.Image)
		assert.Equal(t, corev1.PullAlways, container.ImagePullPolicy)

		assert.Len(t, container.Ports, 2)
		assert.Equal(t, int32(5555), container.Ports[0].ContainerPort)
		assert.Equal(t, int32(5444), container.Ports[1].ContainerPort)

		require.NotNil(t, container.LivenessProbe)
		assert.Equal(t, "/", container.LivenessProbe.HTTPGet.Path)
		assert.Equal(t, int32(5555), container.LivenessProbe.HTTPGet.Port.IntVal)

		require.NotNil(t, container.ReadinessProbe)
		assert.Equal(t, "/", container.ReadinessProbe.HTTPGet.Path)
		assert.Equal(t, int32(5555), container.ReadinessProbe.HTTPGet.Port.IntVal)

		assert.Len(t, dep.Spec.Template.Spec.Volumes, 2)
		assert.Len(t, container.VolumeMounts, 2)
	})

	t.Run("deployment with SSH enabled", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Image = "test/image:v1"
		knot.Spec.Replicas = 1
		knot.Spec.Storage.RepoPath = "/data/repos"
		knot.Spec.Storage.DBPath = "/data/db"
		knot.Spec.SSH = &tangledv1alpha1.KnotSSHSpec{
			Enabled: true,
		}

		r := &KnotReconciler{}
		dep := r.buildDeployment(knot)

		container := dep.Spec.Template.Spec.Containers[0]
		assert.Len(t, container.Ports, 3)

		var sshPort *corev1.ContainerPort
		for i := range container.Ports {
			if container.Ports[i].Name == "ssh" {
				sshPort = &container.Ports[i]
				break
			}
		}
		require.NotNil(t, sshPort)
		assert.Equal(t, int32(22), sshPort.ContainerPort)
	})

	t.Run("deployment with service account", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Image = "test/image:v1"
		knot.Spec.Replicas = 1
		knot.Spec.Storage.RepoPath = "/data/repos"
		knot.Spec.Storage.DBPath = "/data/db"
		knot.Spec.ServiceAccountName = "custom-sa"

		r := &KnotReconciler{}
		dep := r.buildDeployment(knot)

		assert.Equal(t, "custom-sa", dep.Spec.Template.Spec.ServiceAccountName)
	})
}

func TestReconcileConfigMap(t *testing.T) {
	ctx := context.Background()

	t.Run("creates configmap when not exists", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Hostname = "knot.example.com"
		knot.Spec.Owner = "did:plc:test"
		knot.Spec.AppviewEndpoint = "https://tangled.org"
		knot.Spec.Storage.RepoPath = "/data/repos"
		knot.Spec.Storage.DBPath = "/data/db"

		r := newTestReconciler(knot)
		err := r.reconcileConfigMap(ctx, knot)
		require.NoError(t, err)

		cm := &corev1.ConfigMap{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-config", Namespace: "default"}, cm)
		require.NoError(t, err)

		assert.Equal(t, "knot.example.com", cm.Data["KNOT_SERVER_HOSTNAME"])
		assert.Equal(t, "did:plc:test", cm.Data["KNOT_SERVER_OWNER"])
		assert.Equal(t, "https://tangled.org", cm.Data["APPVIEW_ENDPOINT"])
		assert.Equal(t, "/data/repos", cm.Data["KNOT_REPO_SCAN_PATH"])
		assert.Equal(t, "/data/db/knotserver.db", cm.Data["KNOT_SERVER_DB_PATH"])
		assert.Equal(t, "0.0.0.0:5444", cm.Data["KNOT_SERVER_INTERNAL_LISTEN_ADDR"])
		assert.Equal(t, "0.0.0.0:5555", cm.Data["KNOT_SERVER_LISTEN_ADDR"])
	})

	t.Run("updates configmap when exists", func(t *testing.T) {
		existingCM := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-knot-config",
				Namespace: "default",
			},
			Data: map[string]string{
				"KNOT_SERVER_HOSTNAME": "old.example.com",
			},
		}

		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Hostname = "new.example.com"
		knot.Spec.Owner = "did:plc:new"
		knot.Spec.AppviewEndpoint = "https://new.tangled.org"
		knot.Spec.Storage.RepoPath = "/new/repos"
		knot.Spec.Storage.DBPath = "/new/db"

		r := newTestReconciler(knot, existingCM)
		err := r.reconcileConfigMap(ctx, knot)
		require.NoError(t, err)

		cm := &corev1.ConfigMap{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-config", Namespace: "default"}, cm)
		require.NoError(t, err)

		assert.Equal(t, "new.example.com", cm.Data["KNOT_SERVER_HOSTNAME"])
		assert.Equal(t, "did:plc:new", cm.Data["KNOT_SERVER_OWNER"])
	})
}

func TestReconcileSecret(t *testing.T) {
	ctx := context.Background()

	t.Run("creates secret when not exists", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")

		r := newTestReconciler(knot)
		err := r.reconcileSecret(ctx, knot)
		require.NoError(t, err)

		secret := &corev1.Secret{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-secret", Namespace: "default"}, secret)
		require.NoError(t, err)

		var serverSecret string
		if val, ok := secret.Data["KNOT_SERVER_SECRET"]; ok {
			serverSecret = string(val)
		} else if val, ok := secret.StringData["KNOT_SERVER_SECRET"]; ok {
			serverSecret = val
		}
		assert.Len(t, serverSecret, 64)
	})

	t.Run("skips existing secret", func(t *testing.T) {
		existingSecret := &corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-knot-secret",
				Namespace: "default",
			},
			Data: map[string][]byte{
				"KNOT_SERVER_SECRET": []byte("existing-secret-value"),
			},
		}

		knot := newMinimalKnot("test-knot", "default")

		r := newTestReconciler(knot, existingSecret)
		err := r.reconcileSecret(ctx, knot)
		require.NoError(t, err)

		secret := &corev1.Secret{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-secret", Namespace: "default"}, secret)
		require.NoError(t, err)

		assert.Equal(t, "existing-secret-value", string(secret.Data["KNOT_SERVER_SECRET"]))
	})

	t.Run("regenerates secret with empty value", func(t *testing.T) {
		existingSecret := &corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-knot-secret",
				Namespace: "default",
			},
			Data: map[string][]byte{
				"KNOT_SERVER_SECRET": []byte(""),
			},
		}

		knot := newMinimalKnot("test-knot", "default")

		r := newTestReconciler(knot, existingSecret)
		err := r.reconcileSecret(ctx, knot)
		require.NoError(t, err)

		secret := &corev1.Secret{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-secret", Namespace: "default"}, secret)
		require.NoError(t, err)

		var serverSecret string
		if val, ok := secret.Data["KNOT_SERVER_SECRET"]; ok {
			serverSecret = string(val)
		} else if val, ok := secret.StringData["KNOT_SERVER_SECRET"]; ok {
			serverSecret = val
		}
		assert.Len(t, serverSecret, 64)
	})
}

func TestReconcileService(t *testing.T) {
	ctx := context.Background()

	t.Run("creates service when not exists", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")

		r := newTestReconciler(knot)
		err := r.reconcileService(ctx, knot)
		require.NoError(t, err)

		svc := &corev1.Service{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, svc)
		require.NoError(t, err)

		assert.Equal(t, corev1.ServiceTypeClusterIP, svc.Spec.Type)
		require.Len(t, svc.Spec.Ports, 2)

		var httpPort, internalPort *corev1.ServicePort
		for i := range svc.Spec.Ports {
			switch svc.Spec.Ports[i].Name {
			case "http":
				httpPort = &svc.Spec.Ports[i]
			case "internal":
				internalPort = &svc.Spec.Ports[i]
			}
		}

		require.NotNil(t, httpPort)
		assert.Equal(t, int32(5555), httpPort.Port)

		require.NotNil(t, internalPort)
		assert.Equal(t, int32(5444), internalPort.Port)
	})

	t.Run("updates existing service", func(t *testing.T) {
		existingSvc := &corev1.Service{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-knot",
				Namespace: "default",
			},
			Spec: corev1.ServiceSpec{
				Ports: []corev1.ServicePort{
					{Name: "old-port", Port: 8080},
				},
			},
		}

		knot := newMinimalKnot("test-knot", "default")

		r := newTestReconciler(knot, existingSvc)
		err := r.reconcileService(ctx, knot)
		require.NoError(t, err)

		svc := &corev1.Service{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, svc)
		require.NoError(t, err)

		assert.Len(t, svc.Spec.Ports, 2)
	})
}

func TestReconcileSSHService(t *testing.T) {
	ctx := context.Background()

	t.Run("returns nil when SSH spec is nil", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.SSH = nil

		r := newTestReconciler(knot)
		err := r.reconcileSSHService(ctx, knot)
		require.NoError(t, err)

		svcList := &corev1.ServiceList{}
		err = r.List(ctx, svcList)
		require.NoError(t, err)
		assert.Empty(t, svcList.Items)
	})

	t.Run("creates SSH service with defaults", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.SSH = &tangledv1alpha1.KnotSSHSpec{
			Enabled: true,
		}

		r := newTestReconciler(knot)
		err := r.reconcileSSHService(ctx, knot)
		require.NoError(t, err)

		svc := &corev1.Service{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-ssh", Namespace: "default"}, svc)
		require.NoError(t, err)

		assert.Equal(t, corev1.ServiceTypeLoadBalancer, svc.Spec.Type)
		require.Len(t, svc.Spec.Ports, 1)
		assert.Equal(t, int32(22), svc.Spec.Ports[0].Port)
		assert.Equal(t, "ssh", svc.Spec.Ports[0].Name)
	})

	t.Run("creates SSH service with custom port and NodePort type", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.SSH = &tangledv1alpha1.KnotSSHSpec{
			Enabled:     true,
			Port:        2222,
			ServiceType: corev1.ServiceTypeNodePort,
			NodePort:    30022,
		}

		r := newTestReconciler(knot)
		err := r.reconcileSSHService(ctx, knot)
		require.NoError(t, err)

		svc := &corev1.Service{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-ssh", Namespace: "default"}, svc)
		require.NoError(t, err)

		assert.Equal(t, corev1.ServiceTypeNodePort, svc.Spec.Type)
		assert.Equal(t, int32(2222), svc.Spec.Ports[0].Port)
		assert.Equal(t, int32(30022), svc.Spec.Ports[0].NodePort)
	})

	t.Run("creates SSH service with LoadBalancer IP", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.SSH = &tangledv1alpha1.KnotSSHSpec{
			Enabled:        true,
			ServiceType:    corev1.ServiceTypeLoadBalancer,
			LoadBalancerIP: "10.0.0.100",
		}

		r := newTestReconciler(knot)
		err := r.reconcileSSHService(ctx, knot)
		require.NoError(t, err)

		svc := &corev1.Service{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-ssh", Namespace: "default"}, svc)
		require.NoError(t, err)

		assert.Equal(t, "10.0.0.100", svc.Spec.LoadBalancerIP)
	})
}

func TestReconcileIngress(t *testing.T) {
	ctx := context.Background()

	t.Run("creates ingress", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Hostname = "knot.example.com"
		knot.Spec.Ingress = &tangledv1alpha1.KnotIngressSpec{
			Enabled:          true,
			IngressClassName: "nginx",
			Annotations: map[string]string{
				"nginx.ingress.kubernetes.io/ssl-redirect": "true",
			},
		}

		r := newTestReconciler(knot)
		err := r.reconcileIngress(ctx, knot)
		require.NoError(t, err)

		ingress := &networkingv1.Ingress{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, ingress)
		require.NoError(t, err)

		require.NotNil(t, ingress.Spec.IngressClassName)
		assert.Equal(t, "nginx", *ingress.Spec.IngressClassName)
		assert.Equal(t, "true", ingress.Annotations["nginx.ingress.kubernetes.io/ssl-redirect"])

		require.Len(t, ingress.Spec.Rules, 1)
		assert.Equal(t, "knot.example.com", ingress.Spec.Rules[0].Host)
		require.NotNil(t, ingress.Spec.Rules[0].HTTP)
		require.Len(t, ingress.Spec.Rules[0].HTTP.Paths, 1)
		assert.Equal(t, "/", ingress.Spec.Rules[0].HTTP.Paths[0].Path)
		assert.Equal(t, int32(5555), ingress.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Port.Number)
	})

	t.Run("creates ingress with TLS", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Hostname = "knot.example.com"
		knot.Spec.Ingress = &tangledv1alpha1.KnotIngressSpec{
			Enabled: true,
			TLS: &tangledv1alpha1.KnotIngressTLSSpec{
				Enabled:    true,
				SecretName: "knot-tls",
			},
		}

		r := newTestReconciler(knot)
		err := r.reconcileIngress(ctx, knot)
		require.NoError(t, err)

		ingress := &networkingv1.Ingress{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, ingress)
		require.NoError(t, err)

		require.Len(t, ingress.Spec.TLS, 1)
		assert.Equal(t, []string{"knot.example.com"}, ingress.Spec.TLS[0].Hosts)
		assert.Equal(t, "knot-tls", ingress.Spec.TLS[0].SecretName)
	})

	t.Run("updates existing ingress", func(t *testing.T) {
		existingIngress := &networkingv1.Ingress{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-knot",
				Namespace: "default",
			},
		}

		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Hostname = "new.example.com"
		knot.Spec.Ingress = &tangledv1alpha1.KnotIngressSpec{
			Enabled: true,
			Annotations: map[string]string{
				"new-annotation": "value",
			},
		}

		r := newTestReconciler(knot, existingIngress)
		err := r.reconcileIngress(ctx, knot)
		require.NoError(t, err)

		ingress := &networkingv1.Ingress{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, ingress)
		require.NoError(t, err)

		assert.Equal(t, "value", ingress.Annotations["new-annotation"])
		assert.Equal(t, "new.example.com", ingress.Spec.Rules[0].Host)
	})
}

func TestReconcile(t *testing.T) {
	ctx := context.Background()

	t.Run("returns empty result when knot not found", func(t *testing.T) {
		r := newTestReconciler()
		result, err := r.Reconcile(ctx, ctrl.Request{
			NamespacedName: types.NamespacedName{Name: "nonexistent", Namespace: "default"},
		})

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

	t.Run("adds finalizer on new knot", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")

		r := newTestReconciler(knot)

		r.setDefaults(knot)

		_, err := r.Reconcile(ctx, ctrl.Request{
			NamespacedName: types.NamespacedName{Name: "test-knot", Namespace: "default"},
		})
		require.NoError(t, err)

		updatedKnot := &tangledv1alpha1.Knot{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, updatedKnot)
		require.NoError(t, err)

		assert.Contains(t, updatedKnot.Finalizers, knotFinalizer)
	})

	t.Run("creates all required resources", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Finalizers = []string{knotFinalizer}

		r := newTestReconciler(knot)

		_, err := r.Reconcile(ctx, ctrl.Request{
			NamespacedName: types.NamespacedName{Name: "test-knot", Namespace: "default"},
		})
		require.NoError(t, err)

		repoPVC := &corev1.PersistentVolumeClaim{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-repos", Namespace: "default"}, repoPVC)
		require.NoError(t, err)

		dbPVC := &corev1.PersistentVolumeClaim{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-db", Namespace: "default"}, dbPVC)
		require.NoError(t, err)

		cm := &corev1.ConfigMap{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-config", Namespace: "default"}, cm)
		require.NoError(t, err)

		secret := &corev1.Secret{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-secret", Namespace: "default"}, secret)
		require.NoError(t, err)

		svc := &corev1.Service{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, svc)
		require.NoError(t, err)

		dep := &appsv1.Deployment{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, dep)
		require.NoError(t, err)
	})

	t.Run("creates SSH service when enabled", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Finalizers = []string{knotFinalizer}
		knot.Spec.SSH = &tangledv1alpha1.KnotSSHSpec{
			Enabled: true,
		}

		r := newTestReconciler(knot)

		_, err := r.Reconcile(ctx, ctrl.Request{
			NamespacedName: types.NamespacedName{Name: "test-knot", Namespace: "default"},
		})
		require.NoError(t, err)

		sshSvc := &corev1.Service{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot-ssh", Namespace: "default"}, sshSvc)
		require.NoError(t, err)
	})

	t.Run("creates ingress when enabled", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Finalizers = []string{knotFinalizer}
		knot.Spec.Ingress = &tangledv1alpha1.KnotIngressSpec{
			Enabled: true,
		}

		r := newTestReconciler(knot)

		_, err := r.Reconcile(ctx, ctrl.Request{
			NamespacedName: types.NamespacedName{Name: "test-knot", Namespace: "default"},
		})
		require.NoError(t, err)

		ingress := &networkingv1.Ingress{}
		err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, ingress)
		require.NoError(t, err)
	})
}

func TestUpdateStatus(t *testing.T) {
	ctx := context.Background()

	t.Run("sets URL with http when no TLS", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Hostname = "knot.example.com"
		knot.Spec.Replicas = 1
		knot.Status.Conditions = []metav1.Condition{}

		r := newTestReconciler(knot)

		_, err := r.updateStatus(ctx, knot)
		require.NoError(t, err)

		assert.Equal(t, "http://knot.example.com", knot.Status.URL)
	})

	t.Run("sets URL with https when TLS enabled", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Hostname = "knot.example.com"
		knot.Spec.Replicas = 1
		knot.Spec.Ingress = &tangledv1alpha1.KnotIngressSpec{
			Enabled: true,
			TLS: &tangledv1alpha1.KnotIngressTLSSpec{
				Enabled: true,
			},
		}
		knot.Status.Conditions = []metav1.Condition{}

		r := newTestReconciler(knot)

		_, err := r.updateStatus(ctx, knot)
		require.NoError(t, err)

		assert.Equal(t, "https://knot.example.com", knot.Status.URL)
	})

	t.Run("sets SSH URL when SSH enabled", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Hostname = "knot.example.com"
		knot.Spec.Replicas = 1
		knot.Spec.SSH = &tangledv1alpha1.KnotSSHSpec{
			Enabled: true,
		}
		knot.Status.Conditions = []metav1.Condition{}

		r := newTestReconciler(knot)

		_, err := r.updateStatus(ctx, knot)
		require.NoError(t, err)

		assert.Equal(t, "git@knot.example.com", knot.Status.SSHURL)
	})

	t.Run("sets phase to Pending when not all replicas ready", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Hostname = "knot.example.com"
		knot.Spec.Replicas = 3
		knot.Status.ReadyReplicas = 1
		knot.Status.Conditions = []metav1.Condition{}

		r := newTestReconciler(knot)

		_, err := r.updateStatus(ctx, knot)
		require.NoError(t, err)

		assert.Equal(t, "Pending", knot.Status.Phase)
	})

	t.Run("sets phase to Running when all replicas ready", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Spec.Hostname = "knot.example.com"
		knot.Spec.Replicas = 2

		dep := &appsv1.Deployment{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-knot",
				Namespace: "default",
			},
			Status: appsv1.DeploymentStatus{
				ReadyReplicas:     2,
				AvailableReplicas: 2,
			},
		}

		knot.Status.Conditions = []metav1.Condition{}

		r := newTestReconciler(knot, dep)

		_, err := r.updateStatus(ctx, knot)
		require.NoError(t, err)

		assert.Equal(t, "Running", knot.Status.Phase)
		assert.Equal(t, int32(2), knot.Status.ReadyReplicas)
	})
}

func TestHandleError(t *testing.T) {
	ctx := context.Background()

	t.Run("sets degraded condition and failed phase", func(t *testing.T) {
		knot := newMinimalKnot("test-knot", "default")
		knot.Status.Conditions = []metav1.Condition{}

		r := newTestReconciler(knot)

		testErr := fmt.Errorf("test error")
		result, err := r.handleError(ctx, knot, testErr, "Test failure message")

		assert.Error(t, err)
		assert.Equal(t, testErr, err)
		assert.Equal(t, defaultRequeueTime, result.RequeueAfter)

		assert.Equal(t, "Failed", knot.Status.Phase)

		var degradedCondition *metav1.Condition
		for i := range knot.Status.Conditions {
			if knot.Status.Conditions[i].Type == typeDegraded {
				degradedCondition = &knot.Status.Conditions[i]
				break
			}
		}
		require.NotNil(t, degradedCondition)
		assert.Equal(t, metav1.ConditionTrue, degradedCondition.Status)
		assert.Equal(t, "ReconciliationFailed", degradedCondition.Reason)
		assert.Contains(t, degradedCondition.Message, "Test failure message")
		assert.Contains(t, degradedCondition.Message, "test error")
	})
}

func TestGenerateRandomHex(t *testing.T) {
	t.Run("generates correct length", func(t *testing.T) {
		hex, err := generateRandomHex(32)
		require.NoError(t, err)
		assert.Len(t, hex, 64)
	})

	t.Run("generates unique values", func(t *testing.T) {
		hex1, err := generateRandomHex(32)
		require.NoError(t, err)

		hex2, err := generateRandomHex(32)
		require.NoError(t, err)

		assert.NotEqual(t, hex1, hex2)
	})
}

func TestLabelsForKnot(t *testing.T) {
	knot := newMinimalKnot("my-knot", "default")
	r := &KnotReconciler{}

	labels := r.labelsForKnot(knot)

	assert.Equal(t, "knot", labels["app.kubernetes.io/name"])
	assert.Equal(t, "my-knot", labels["app.kubernetes.io/instance"])
	assert.Equal(t, "knot-operator", labels["app.kubernetes.io/managed-by"])
	assert.Equal(t, "server", labels["app.kubernetes.io/component"])
}

func TestSelectorLabelsForKnot(t *testing.T) {
	knot := newMinimalKnot("my-knot", "default")
	r := &KnotReconciler{}

	labels := r.selectorLabelsForKnot(knot)

	assert.Equal(t, "knot", labels["app.kubernetes.io/name"])
	assert.Equal(t, "my-knot", labels["app.kubernetes.io/instance"])
	assert.Len(t, labels, 2)
}
