package controller

import (
	"context"
	"strings"
	"testing"

	cephv1 "git.unkin.net/unkin/cephrgw-operator/api/v1alpha1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/types"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

func TestClusterReconcile(t *testing.T) {
	requireEnvtest(t)
	ctx := context.Background()
	ns := newNamespace(t, ctx)
	createDestinations(t, ctx, ns)

	cluster := newCluster()
	cluster.SetName("pg")
	cluster.SetNamespace(ns)
	cluster.SetAnnotations(map[string]string{
		annSchedule:    "@daily",
		annDestination: "cephs3_ec4_1",
	})
	_ = unstructured.SetNestedField(cluster.Object, int64(1), "spec", "instances")
	if err := k8sClient.Create(ctx, cluster); err != nil {
		t.Fatalf("create cluster: %v", err)
	}

	r := &ClusterReconciler{baseReconciler{
		Client:        k8sClient,
		Scheme:        clientgoscheme.Scheme,
		Recorder:      record.NewFakeRecorder(16),
		DestNamespace: ns,
		DestConfigMap: "autobackup-destinations",
	}}
	req := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: ns, Name: "pg"}}

	// First reconcile provisions the bucket stack but blocks on access readiness.
	if _, err := r.Reconcile(ctx, req); err != nil {
		t.Fatalf("first reconcile: %v", err)
	}
	var access cephv1.BucketAccess
	if err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: accessName("pg")}, &access); err != nil {
		t.Fatalf("BucketAccess not created: %v", err)
	}
	access.Status.Phase = "Ready"
	access.Status.SecretName = credSecretName("pg")
	if err := k8sClient.Status().Update(ctx, &access); err != nil {
		t.Fatalf("update access status: %v", err)
	}

	if _, err := r.Reconcile(ctx, req); err != nil {
		t.Fatalf("second reconcile: %v", err)
	}

	// Cluster barmanObjectStore patched.
	got := newCluster()
	if err := k8sClient.Get(ctx, req.NamespacedName, got); err != nil {
		t.Fatalf("get cluster: %v", err)
	}
	path, _, _ := unstructured.NestedString(got.Object, "spec", "backup", "barmanObjectStore", "destinationPath")
	if want := "s3://" + bucketName(ns, "pg"); path != want {
		t.Errorf("destinationPath = %q, want %q", path, want)
	}
	endpoint, _, _ := unstructured.NestedString(got.Object, "spec", "backup", "barmanObjectStore", "endpointURL")
	if endpoint != "https://s3.ceph.unkin.net" {
		t.Errorf("endpointURL = %q", endpoint)
	}
	server, _, _ := unstructured.NestedString(got.Object, "spec", "backup", "barmanObjectStore", "serverName")
	if server != "pg" {
		t.Errorf("serverName = %q", server)
	}
	akName, _, _ := unstructured.NestedString(got.Object, "spec", "backup", "barmanObjectStore", "s3Credentials", "accessKeyId", "name")
	if akName != credSecretName("pg") {
		t.Errorf("accessKeyId secret name = %q", akName)
	}
	caName, _, _ := unstructured.NestedString(got.Object, "spec", "backup", "barmanObjectStore", "endpointCA", "name")
	if caName != "vault-ca-cert" {
		t.Errorf("endpointCA name = %q", caName)
	}
	rp, _, _ := unstructured.NestedString(got.Object, "spec", "backup", "retentionPolicy")
	if rp != "30d" {
		t.Errorf("retentionPolicy = %q", rp)
	}

	// ScheduledBackup created with 6-field schedule.
	sb := newScheduledBackup()
	if err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: scheduleName("pg")}, sb); err != nil {
		t.Fatalf("ScheduledBackup not created: %v", err)
	}
	sched, _, _ := unstructured.NestedString(sb.Object, "spec", "schedule")
	if sched != "0 0 0 * * *" {
		t.Errorf("scheduledbackup schedule = %q, want 0 0 0 * * *", sched)
	}
	method, _, _ := unstructured.NestedString(sb.Object, "spec", "method")
	if method != "barmanObjectStore" {
		t.Errorf("method = %q", method)
	}
	clusterName, _, _ := unstructured.NestedString(sb.Object, "spec", "cluster", "name")
	if clusterName != "pg" {
		t.Errorf("cluster.name = %q", clusterName)
	}
	owner, _, _ := unstructured.NestedString(sb.Object, "spec", "backupOwnerReference")
	if owner != "self" {
		t.Errorf("backupOwnerReference = %q", owner)
	}
}

func TestClusterReconcileDestinationConflict(t *testing.T) {
	requireEnvtest(t)
	ctx := context.Background()
	ns := newNamespace(t, ctx)
	createDestinations(t, ctx, ns)

	cluster := newCluster()
	cluster.SetName("pg2")
	cluster.SetNamespace(ns)
	cluster.SetAnnotations(map[string]string{annSchedule: "@daily", annDestination: "cephs3_ec4_1"})
	_ = unstructured.SetNestedField(cluster.Object, int64(1), "spec", "instances")
	// User already configured a different destinationPath.
	_ = unstructured.SetNestedField(cluster.Object, "s3://user-owned-bucket", "spec", "backup", "barmanObjectStore", "destinationPath")
	if err := k8sClient.Create(ctx, cluster); err != nil {
		t.Fatalf("create cluster: %v", err)
	}

	rec := record.NewFakeRecorder(16)
	r := &ClusterReconciler{baseReconciler{
		Client: k8sClient, Scheme: clientgoscheme.Scheme, Recorder: rec,
		DestNamespace: ns, DestConfigMap: "autobackup-destinations",
	}}
	req := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: ns, Name: "pg2"}}

	if _, err := r.Reconcile(ctx, req); err != nil {
		t.Fatalf("first reconcile: %v", err)
	}
	var access cephv1.BucketAccess
	if err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: accessName("pg2")}, &access); err != nil {
		t.Fatalf("BucketAccess not created: %v", err)
	}
	access.Status.Phase = "Ready"
	access.Status.SecretName = credSecretName("pg2")
	if err := k8sClient.Status().Update(ctx, &access); err != nil {
		t.Fatalf("update access status: %v", err)
	}
	if _, err := r.Reconcile(ctx, req); err != nil {
		t.Fatalf("second reconcile: %v", err)
	}

	got := newCluster()
	if err := k8sClient.Get(ctx, req.NamespacedName, got); err != nil {
		t.Fatalf("get cluster: %v", err)
	}
	path, _, _ := unstructured.NestedString(got.Object, "spec", "backup", "barmanObjectStore", "destinationPath")
	if path != "s3://user-owned-bucket" {
		t.Errorf("operator clobbered user destinationPath: got %q", path)
	}

	// ScheduledBackup is still created; only the barman patch is skipped.
	sb := newScheduledBackup()
	if err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: scheduleName("pg2")}, sb); err != nil {
		t.Fatalf("ScheduledBackup should still be created: %v", err)
	}

	var sawConflict bool
	for drained := false; !drained; {
		select {
		case e := <-rec.Events:
			if strings.Contains(e, "DestinationConflict") {
				sawConflict = true
			}
		default:
			drained = true
		}
	}
	if !sawConflict {
		t.Errorf("expected a DestinationConflict warning event")
	}
}
