package server

import (
	"os"
	"regexp"
	"testing"

	"buf.build/go/protovalidate"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/managed_resources"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testhelpers"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/yaml"
)

func TestDefaultTemplateYAMLMatchesTypedObjects(t *testing.T) {
	srcData, err := yaml.Marshal(newDefaultTemplate())
	require.NoError(t, err)

	// Kubernetes objects when marshaled into YAML have these fields.
	// We don't want them in the default template, so just remove them before comparison.
	srcData = removeLine(srcData, "spec: {}")
	srcData = removeLine(srcData, "status: {}")
	srcData = removeLine(srcData, "creationTimestamp: null")

	srcYAML := string(srcData)
	if !assert.YAMLEq(t, srcYAML, string(defaultTemplate), "Data in default_template.yaml doesn't match the YAML constructed from typed objects") {
		t.Log(srcYAML)
	}
}

func TestDefaultTemplateRendering(t *testing.T) {
	v, err := protovalidate.New()
	require.NoError(t, err)
	r := &managed_resources.Renderer{
		Mapper:    newMapper(),
		Validator: v,
	}
	expectedRenderedData, err := os.ReadFile("testdata/default_template_rendered.yaml")
	require.NoError(t, err)

	renderedData, err := r.RenderEnvironmentTemplate(
		&managed_resources.EnvironmentTemplate{
			Name: managed_resources.DefaultTemplateName,
			Data: defaultTemplate,
		},
		&managed_resources.TemplatingInfo{
			Agent: &managed_resources.Agent{
				Id:   testhelpers.AgentkKey1.ID,
				Name: "my-agent",
				Url:  "https://gitlab.example.com/agents/my-agent",
			},
			Environment: &managed_resources.Environment{
				Id:      123,
				Name:    "Production",
				Slug:    "prod",
				Url:     ptr.To("https://prod.example.com/"),
				PageUrl: "https://gitlab.example.com/env/1",
				Tier:    "production",
			},
			Project: &managed_resources.Project{
				Id:   testhelpers.ProjectID,
				Slug: "project1",
				Path: "group1/project1",
				Url:  "https://gitlab.example.com/group1/project1",
			},
			Pipeline: &managed_resources.Pipeline{
				Id: 443322,
			},
			Job: &managed_resources.Job{
				Id: 123123123,
			},
			User: &managed_resources.User{
				Id:       testhelpers.UserID,
				Username: "neo",
			},
		},
		func(u *unstructured.Unstructured) error {
			return nil
		},
	)
	require.NoError(t, err)

	renderedYAML := string(renderedData.Data)
	if !assert.YAMLEq(t, string(expectedRenderedData), renderedYAML, "Data in default_template_rendered.yaml doesn't match the rendered YAML") {
		t.Log(renderedYAML)
	}
	assert.Equal(t, managed_resources.DefaultTemplateName, renderedData.Name)
}

// newDefaultTemplate is the typed source of truth of the default template.
// Update it here, then in default_template.yaml, then testdata/default_template_rendered.yaml.
func newDefaultTemplate() *managed_resources.EnvironmentTemplateData[runtime.Object] {
	const namespaceName = "{{ .environment.slug }}-{{ .project.id }}-{{ .agent.id }}"
	return &managed_resources.EnvironmentTemplateData[runtime.Object]{
		Objects: []runtime.Object{
			&corev1.Namespace{
				TypeMeta: metav1.TypeMeta{
					Kind:       "Namespace",
					APIVersion: corev1.SchemeGroupVersion.String(),
				},
				ObjectMeta: metav1.ObjectMeta{
					Name: namespaceName,
				},
			},
			&rbacv1.RoleBinding{
				TypeMeta: metav1.TypeMeta{
					Kind:       "RoleBinding",
					APIVersion: rbacv1.SchemeGroupVersion.String(),
				},
				ObjectMeta: metav1.ObjectMeta{
					Name:      "bind-ci-job-{{ .environment.slug }}-{{ .project.id }}-{{ .agent.id }}",
					Namespace: namespaceName,
				},
				Subjects: []rbacv1.Subject{
					{
						Kind:     rbacv1.GroupKind,
						APIGroup: "",                                                             // Defaults to "rbac.authorization.k8s.io" for User and Group subjects.
						Name:     "gitlab:project_env:{{ .project.id }}:{{ .environment.slug }}", // this matches what we set in kubernetes_api module.
					},
				},
				RoleRef: rbacv1.RoleRef{
					APIGroup: "",
					Kind:     "ClusterRole",
					Name:     "admin",
				},
			},
		},
		EnvironmentTemplateDataFields: managed_resources.EnvironmentTemplateDataFields{
			ApplyResources:  "on_start",
			DeleteResources: "on_stop",
		},
	}
}

func removeLine(src []byte, line string) []byte {
	r := regexp.MustCompile(`(?m)^\s*` + regexp.QuoteMeta(line) + `\s*\n`)
	return r.ReplaceAllLiteral(src, nil)
}
