package k8s

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

	authenticationv1 "k8s.io/api/authentication/v1"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/kubernetes/fake"
	k8stesting "k8s.io/client-go/testing"

	"github.com/ocdp/cluster-service/internal/domain/entity"
)

func TestTenantClientEnsureTenantCreatesResources(t *testing.T) {
	ctx := context.Background()
	clientset := fake.NewSimpleClientset()
	client := NewTenantClientForClientset(clientset)
	binding := tenantBinding()

	if err := client.EnsureTenant(ctx, nil, binding); err != nil {
		t.Fatalf("EnsureTenant returned error: %v", err)
	}

	if _, err := clientset.CoreV1().Namespaces().Get(ctx, binding.Namespace, metav1.GetOptions{}); err != nil {
		t.Fatalf("expected namespace: %v", err)
	}
	if _, err := clientset.CoreV1().ServiceAccounts(binding.Namespace).Get(ctx, binding.ServiceAccountName, metav1.GetOptions{}); err != nil {
		t.Fatalf("expected service account: %v", err)
	}
	roleBinding, err := clientset.RbacV1().RoleBindings(binding.Namespace).Get(ctx, binding.RoleBindingName, metav1.GetOptions{})
	if err != nil {
		t.Fatalf("expected role binding: %v", err)
	}
	if roleBinding.RoleRef.Kind != "ClusterRole" || roleBinding.RoleRef.Name != binding.ClusterRoleName {
		t.Fatalf("unexpected role ref: %#v", roleBinding.RoleRef)
	}
	if len(roleBinding.Subjects) != 1 || roleBinding.Subjects[0].Name != binding.ServiceAccountName {
		t.Fatalf("unexpected role binding subjects: %#v", roleBinding.Subjects)
	}
	quota, err := clientset.CoreV1().ResourceQuotas(binding.Namespace).Get(ctx, binding.ResourceQuotaName, metav1.GetOptions{})
	if err != nil {
		t.Fatalf("expected resource quota: %v", err)
	}
	if quota.Spec.Hard.Cpu().String() != "2" {
		t.Fatalf("expected cpu quota 2, got %s", quota.Spec.Hard.Cpu().String())
	}
}

func TestTenantClientEnsureTenantUpdatesExistingResources(t *testing.T) {
	ctx := context.Background()
	binding := tenantBinding()
	clientset := fake.NewSimpleClientset(
		&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: binding.Namespace, Labels: binding.Labels}},
		&corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: binding.ServiceAccountName, Namespace: binding.Namespace}},
		&rbacv1.RoleBinding{
			ObjectMeta: metav1.ObjectMeta{Name: binding.RoleBindingName, Namespace: binding.Namespace},
			RoleRef:    rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "view"},
		},
		&corev1.ResourceQuota{
			ObjectMeta: metav1.ObjectMeta{Name: binding.ResourceQuotaName, Namespace: binding.Namespace},
			Spec: corev1.ResourceQuotaSpec{Hard: corev1.ResourceList{
				corev1.ResourceCPU: resource.MustParse("1"),
			}},
		},
	)
	client := NewTenantClientForClientset(clientset)

	if err := client.EnsureTenant(ctx, nil, binding); err != nil {
		t.Fatalf("EnsureTenant returned error: %v", err)
	}

	roleBinding, err := clientset.RbacV1().RoleBindings(binding.Namespace).Get(ctx, binding.RoleBindingName, metav1.GetOptions{})
	if err != nil {
		t.Fatalf("expected updated role binding: %v", err)
	}
	if roleBinding.RoleRef.Name != binding.ClusterRoleName {
		t.Fatalf("expected role ref %q, got %q", binding.ClusterRoleName, roleBinding.RoleRef.Name)
	}
	if roleBinding.Labels["ocdp.io/tenant"] != binding.Namespace {
		t.Fatalf("expected tenant label on updated role binding, got %#v", roleBinding.Labels)
	}
	quota, err := clientset.CoreV1().ResourceQuotas(binding.Namespace).Get(ctx, binding.ResourceQuotaName, metav1.GetOptions{})
	if err != nil {
		t.Fatalf("expected updated quota: %v", err)
	}
	if quota.Spec.Hard.Cpu().String() != "2" {
		t.Fatalf("expected updated cpu quota 2, got %s", quota.Spec.Hard.Cpu().String())
	}
}

func TestTenantClientSuspendTenantDeletesOnlyRoleBinding(t *testing.T) {
	ctx := context.Background()
	binding := tenantBinding()
	clientset := fake.NewSimpleClientset(
		&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: binding.Namespace, Labels: binding.Labels}},
		&corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: binding.ServiceAccountName, Namespace: binding.Namespace}},
		desiredRoleBinding(binding),
	)
	client := NewTenantClientForClientset(clientset)

	if err := client.SuspendTenant(ctx, nil, binding); err != nil {
		t.Fatalf("SuspendTenant returned error: %v", err)
	}
	if _, err := clientset.RbacV1().RoleBindings(binding.Namespace).Get(ctx, binding.RoleBindingName, metav1.GetOptions{}); !apierrors.IsNotFound(err) {
		t.Fatalf("expected deleted role binding, got err %v", err)
	}
	if _, err := clientset.CoreV1().ServiceAccounts(binding.Namespace).Get(ctx, binding.ServiceAccountName, metav1.GetOptions{}); err != nil {
		t.Fatalf("service account should remain: %v", err)
	}
}

func TestTenantClientDeleteTenantDeletesTenantResources(t *testing.T) {
	ctx := context.Background()
	binding := tenantBinding()
	clientset := fake.NewSimpleClientset(
		&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: binding.Namespace, Labels: binding.Labels}},
		&corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: binding.ServiceAccountName, Namespace: binding.Namespace}},
		desiredRoleBinding(binding),
		&corev1.ResourceQuota{ObjectMeta: metav1.ObjectMeta{Name: binding.ResourceQuotaName, Namespace: binding.Namespace}},
	)
	client := NewTenantClientForClientset(clientset)

	if err := client.DeleteTenant(ctx, nil, binding); err != nil {
		t.Fatalf("DeleteTenant returned error: %v", err)
	}
	if _, err := clientset.RbacV1().RoleBindings(binding.Namespace).Get(ctx, binding.RoleBindingName, metav1.GetOptions{}); !apierrors.IsNotFound(err) {
		t.Fatalf("expected role binding deleted, got %v", err)
	}
	if _, err := clientset.CoreV1().ResourceQuotas(binding.Namespace).Get(ctx, binding.ResourceQuotaName, metav1.GetOptions{}); !apierrors.IsNotFound(err) {
		t.Fatalf("expected resource quota deleted, got %v", err)
	}
	if _, err := clientset.CoreV1().ServiceAccounts(binding.Namespace).Get(ctx, binding.ServiceAccountName, metav1.GetOptions{}); !apierrors.IsNotFound(err) {
		t.Fatalf("expected service account deleted, got %v", err)
	}
	if _, err := clientset.CoreV1().Namespaces().Get(ctx, binding.Namespace, metav1.GetOptions{}); !apierrors.IsNotFound(err) {
		t.Fatalf("expected namespace deleted, got %v", err)
	}
}

func TestTenantClientDeleteTenantRejectsProtectedNamespace(t *testing.T) {
	ctx := context.Background()
	client := NewTenantClientForClientset(fake.NewSimpleClientset(
		&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}},
	))
	binding := entity.NewTenantBinding("default")

	err := client.DeleteTenant(ctx, nil, binding)
	if !errors.Is(err, entity.ErrProtectedNamespace) {
		t.Fatalf("expected protected namespace error, got %v", err)
	}
}

func TestTenantClientIssueKubeconfigCapsTokenTTL(t *testing.T) {
	ctx := context.Background()
	binding := tenantBinding()
	clientset := fake.NewSimpleClientset(&corev1.ServiceAccount{
		ObjectMeta: metav1.ObjectMeta{Name: binding.ServiceAccountName, Namespace: binding.Namespace},
	})
	var requestedExpirationSeconds int64
	expiresAt := time.Now().Add(entity.MaxTenantKubeconfigTTL).UTC()
	clientset.Fake.PrependReactor("create", "serviceaccounts", func(action k8stesting.Action) (bool, runtime.Object, error) {
		if action.GetSubresource() != "token" {
			return false, nil, nil
		}
		createAction := action.(k8stesting.CreateAction)
		tokenRequest := createAction.GetObject().(*authenticationv1.TokenRequest)
		if tokenRequest.Spec.ExpirationSeconds != nil {
			requestedExpirationSeconds = *tokenRequest.Spec.ExpirationSeconds
		}
		return true, &authenticationv1.TokenRequest{
			Status: authenticationv1.TokenRequestStatus{
				Token:               "short-lived-token",
				ExpirationTimestamp: metav1.NewTime(expiresAt),
			},
		}, nil
	})
	client := NewTenantClientForClientset(clientset)

	kubeconfig, err := client.IssueKubeconfig(ctx, &entity.Cluster{Name: "test", Host: "https://example.invalid"}, binding, 24*time.Hour)
	if err != nil {
		t.Fatalf("IssueKubeconfig returned error: %v", err)
	}

	if requestedExpirationSeconds != int64(entity.MaxTenantKubeconfigTTL.Seconds()) {
		t.Fatalf("expected capped ttl %d, got %d", int64(entity.MaxTenantKubeconfigTTL.Seconds()), requestedExpirationSeconds)
	}
	if !kubeconfig.ExpiresAt.Equal(expiresAt) {
		t.Fatalf("expected expiration %s, got %s", expiresAt, kubeconfig.ExpiresAt)
	}
	if !strings.Contains(kubeconfig.Kubeconfig, "short-lived-token") {
		t.Fatal("expected kubeconfig to contain issued token")
	}
	if !strings.Contains(kubeconfig.Kubeconfig, "namespace: tenant-a") {
		t.Fatalf("expected kubeconfig namespace, got:\n%s", kubeconfig.Kubeconfig)
	}
}

func tenantBinding() entity.TenantBinding {
	binding := entity.NewTenantBinding("tenant-a")
	binding.ResourceQuotaHard = corev1.ResourceList{
		corev1.ResourceCPU:    resource.MustParse("2"),
		corev1.ResourceMemory: resource.MustParse("4Gi"),
	}
	return binding
}
