package jobs

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apiserver/pkg/admission"
	"k8s.io/apiserver/pkg/authentication/user"

	provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
)

func TestValidateJob(t *testing.T) {
	tests := []struct {
		name          string
		job           *provisioning.Job
		wantErr       bool
		validateError func(t *testing.T, err error)
	}{
		{
			name: "valid pull job",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionPull,
					Repository: "test-repo",
					Pull: &provisioning.SyncJobOptions{
						Incremental: true,
					},
				},
			},
			wantErr: false,
		},
		{
			name: "missing action",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Repository: "test-repo",
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.action: Required value")
			},
		},
		{
			name: "invalid action",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobAction("invalid"),
					Repository: "test-repo",
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.action: Invalid value")
			},
		},
		{
			name: "missing repository",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action: provisioning.JobActionPull,
					Pull: &provisioning.SyncJobOptions{
						Incremental: true,
					},
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.repository: Required value")
			},
		},
		{
			name: "pull action without pull options",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionPull,
					Repository: "test-repo",
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.pull: Required value")
			},
		},
		{
			name: "push action without push options",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionPush,
					Repository: "test-repo",
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.push: Required value")
			},
		},
		{
			name: "valid push job with valid branch",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionPush,
					Repository: "test-repo",
					Push: &provisioning.ExportJobOptions{
						Branch:  "main",
						Message: "Test commit",
					},
				},
			},
			wantErr: false,
		},
		{
			name: "push job with invalid branch name",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionPush,
					Repository: "test-repo",
					Push: &provisioning.ExportJobOptions{
						Branch:  "feature..branch", // Invalid: contains consecutive dots
						Message: "Test commit",
					},
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.push.branch")
				require.Contains(t, err.Error(), "invalid git branch name")
			},
		},
		{
			name: "push job with invalid path",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionPush,
					Repository: "test-repo",
					Push: &provisioning.ExportJobOptions{
						Path:    "../../../etc/passwd", // Invalid: path traversal
						Message: "Test commit",
					},
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.push.path")
			},
		},
		{
			name: "delete action without options",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionDelete,
					Repository: "test-repo",
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.delete: Required value")
			},
		},
		{
			name: "delete action without paths or resources",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionDelete,
					Repository: "test-repo",
					Delete:     &provisioning.DeleteJobOptions{},
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "at least one path or resource must be specified")
			},
		},
		{
			name: "valid delete action with paths",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionDelete,
					Repository: "test-repo",
					Delete: &provisioning.DeleteJobOptions{
						Paths: []string{"dashboard.json", "folder/other.json"},
					},
				},
			},
			wantErr: false,
		},
		{
			name: "valid delete action with resources",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionDelete,
					Repository: "test-repo",
					Delete: &provisioning.DeleteJobOptions{
						Resources: []provisioning.ResourceRef{
							{
								Name: "my-dashboard",
								Kind: "Dashboard",
							},
						},
					},
				},
			},
			wantErr: false,
		},
		{
			name: "delete action with invalid path",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionDelete,
					Repository: "test-repo",
					Delete: &provisioning.DeleteJobOptions{
						Paths: []string{"../../etc/passwd"}, // Invalid: path traversal
					},
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.delete.paths[0]")
			},
		},
		{
			name: "delete action with resource missing name",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionDelete,
					Repository: "test-repo",
					Delete: &provisioning.DeleteJobOptions{
						Resources: []provisioning.ResourceRef{
							{
								Kind: "Dashboard",
							},
						},
					},
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.delete.resources[0].name")
			},
		},
		{
			name: "move action without options",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionMove,
					Repository: "test-repo",
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.move: Required value")
			},
		},
		{
			name: "move action without paths or resources",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionMove,
					Repository: "test-repo",
					Move: &provisioning.MoveJobOptions{
						TargetPath: "new-location/",
					},
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "at least one path or resource must be specified")
			},
		},
		{
			name: "move action without target path",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionMove,
					Repository: "test-repo",
					Move: &provisioning.MoveJobOptions{
						Paths: []string{"dashboard.json"},
					},
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.move.targetPath: Required value")
			},
		},
		{
			name: "valid move action",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionMove,
					Repository: "test-repo",
					Move: &provisioning.MoveJobOptions{
						Paths:      []string{"old-location/dashboard.json"},
						TargetPath: "new-location/",
					},
				},
			},
			wantErr: false,
		},
		{
			name: "move action with invalid target path",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionMove,
					Repository: "test-repo",
					Move: &provisioning.MoveJobOptions{
						Paths:      []string{"dashboard.json"},
						TargetPath: "../../../etc/", // Invalid: path traversal
					},
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.move.targetPath")
			},
		},
		{
			name: "valid migrate job",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionMigrate,
					Repository: "test-repo",
					Migrate: &provisioning.MigrateJobOptions{
						Message: "Migrate from unified",
					},
				},
			},
			wantErr: false,
		},
		{
			name: "migrate action without migrate options",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionMigrate,
					Repository: "test-repo",
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.migrate: Required value")
			},
		},
		{
			name: "valid pr job",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionPullRequest,
					Repository: "test-repo",
					PullRequest: &provisioning.PullRequestJobOptions{
						PR:  123,
						Ref: "refs/pull/123/head",
					},
				},
			},
			wantErr: false,
		},
		{
			name: "delete action with resource missing kind",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionDelete,
					Repository: "test-repo",
					Delete: &provisioning.DeleteJobOptions{
						Resources: []provisioning.ResourceRef{
							{
								Name: "my-dashboard",
							},
						},
					},
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.delete.resources[0].kind")
			},
		},
		{
			name: "move action with valid resources",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionMove,
					Repository: "test-repo",
					Move: &provisioning.MoveJobOptions{
						Resources: []provisioning.ResourceRef{
							{
								Name: "my-dashboard",
								Kind: "Dashboard",
							},
						},
						TargetPath: "new-location/",
					},
				},
			},
			wantErr: false,
		},
		{
			name: "move action with resource missing kind",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionMove,
					Repository: "test-repo",
					Move: &provisioning.MoveJobOptions{
						Resources: []provisioning.ResourceRef{
							{
								Name: "my-dashboard",
							},
						},
						TargetPath: "new-location/",
					},
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.move.resources[0].kind")
			},
		},
		{
			name: "move action with both paths and resources",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionMove,
					Repository: "test-repo",
					Move: &provisioning.MoveJobOptions{
						Paths: []string{"dashboard.json"},
						Resources: []provisioning.ResourceRef{
							{
								Name: "my-dashboard",
								Kind: "Dashboard",
							},
						},
						TargetPath: "new-location/",
					},
				},
			},
			wantErr: false,
		},
		{
			name: "move action with invalid source path",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionMove,
					Repository: "test-repo",
					Move: &provisioning.MoveJobOptions{
						Paths:      []string{"../invalid/path"},
						TargetPath: "valid/target/",
					},
				},
			},
			wantErr: true,
			validateError: func(t *testing.T, err error) {
				require.Contains(t, err.Error(), "spec.move.paths[0]")
			},
		},
		{
			name: "delete action with both paths and resources",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionDelete,
					Repository: "test-repo",
					Delete: &provisioning.DeleteJobOptions{
						Paths: []string{"dashboard.json"},
						Resources: []provisioning.ResourceRef{
							{
								Name: "my-dashboard",
								Kind: "Dashboard",
							},
						},
					},
				},
			},
			wantErr: false,
		},
		{
			name: "push action with valid path",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionPush,
					Repository: "test-repo",
					Push: &provisioning.ExportJobOptions{
						Path:    "some/valid/path",
						Message: "Test commit",
					},
				},
			},
			wantErr: false,
		},
		{
			name: "valid fix-folder-metadata job without options",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionFixFolderMetadata,
					Repository: "test-repo",
				},
			},
			wantErr: false,
		},
		{
			name: "valid releaseResources job",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionReleaseResources,
					Repository: "test-repo",
				},
			},
			wantErr: false,
		},
		{
			name: "valid deleteResources job",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionDeleteResources,
					Repository: "test-repo",
				},
			},
			wantErr: false,
		},
		{
			name: "valid fix-folder-metadata job with options",
			job: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-job",
				},
				Spec: provisioning.JobSpec{
					Action:            provisioning.JobActionFixFolderMetadata,
					Repository:        "test-repo",
					FixFolderMetadata: &provisioning.FixFolderMetadataJobOptions{},
				},
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidateJob(tt.job)
			if tt.wantErr {
				require.Error(t, err)
				if tt.validateError != nil {
					tt.validateError(t, err)
				}
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestAdmissionValidator_Validate(t *testing.T) {
	tests := []struct {
		name            string
		obj             interface{}
		operation       admission.Operation
		wantErr         bool
		wantErrContains string
	}{
		{
			name: "valid job passes validation",
			obj: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionPull,
					Repository: "test-repo",
					Pull:       &provisioning.SyncJobOptions{Incremental: true},
				},
			},
			operation: admission.Create,
			wantErr:   false,
		},
		{
			name: "invalid job fails validation",
			obj: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
				Spec: provisioning.JobSpec{
					// Missing action
					Repository: "test-repo",
				},
			},
			operation: admission.Create,
			wantErr:   true,
		},
		{
			name:    "returns nil for nil object",
			obj:     nil,
			wantErr: false,
		},
		{
			name: "returns error for non-job object",
			obj: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{Name: "test"},
			},
			operation:       admission.Create,
			wantErr:         true,
			wantErrContains: "expected job",
		},
		{
			name: "skips validation for objects being deleted",
			obj: &provisioning.Job{
				ObjectMeta: metav1.ObjectMeta{
					Name:              "test-job",
					DeletionTimestamp: &metav1.Time{Time: time.Now()},
				},
				Spec: provisioning.JobSpec{
					// Invalid - missing action
					Repository: "test-repo",
				},
			},
			operation: admission.Update,
			wantErr:   false,
		},
	}

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

			var obj runtime.Object
			if tt.obj != nil {
				obj = tt.obj.(runtime.Object)
			}

			attr := newAdmissionTestAttributes(obj, tt.operation)

			err := v.Validate(context.Background(), attr, nil)

			if tt.wantErr {
				require.Error(t, err)
				if tt.wantErrContains != "" {
					require.Contains(t, err.Error(), tt.wantErrContains)
				}
				return
			}

			require.NoError(t, err)
		})
	}
}

func newAdmissionTestAttributes(obj runtime.Object, op admission.Operation) admission.Attributes {
	return admission.NewAttributesRecord(
		obj,
		nil,
		provisioning.JobResourceInfo.GroupVersionKind(),
		"default",
		"test",
		provisioning.JobResourceInfo.GroupVersionResource(),
		"",
		op,
		nil,
		false,
		&user.DefaultInfo{},
	)
}

func TestHistoricJobAdmissionValidator_Validate(t *testing.T) {
	tests := []struct {
		name            string
		obj             interface{}
		operation       admission.Operation
		wantErr         bool
		wantErrContains string
	}{
		{
			name: "valid historic job passes validation",
			obj: &provisioning.HistoricJob{
				ObjectMeta: metav1.ObjectMeta{Name: "test-historic-job"},
				Spec: provisioning.JobSpec{
					Action:     provisioning.JobActionPull,
					Repository: "test-repo",
				},
			},
			operation: admission.Create,
			wantErr:   false,
		},
		{
			name: "historic job missing action fails validation",
			obj: &provisioning.HistoricJob{
				ObjectMeta: metav1.ObjectMeta{Name: "test-historic-job"},
				Spec: provisioning.JobSpec{
					Repository: "test-repo",
				},
			},
			operation:       admission.Create,
			wantErr:         true,
			wantErrContains: "action must be specified",
		},
		{
			name: "historic job missing repository fails validation",
			obj: &provisioning.HistoricJob{
				ObjectMeta: metav1.ObjectMeta{Name: "test-historic-job"},
				Spec: provisioning.JobSpec{
					Action: provisioning.JobActionPull,
				},
			},
			operation:       admission.Create,
			wantErr:         true,
			wantErrContains: "repository must be specified",
		},
		{
			name:    "returns nil for nil object",
			obj:     nil,
			wantErr: false,
		},
		{
			name: "returns error for non-historic-job object",
			obj: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{Name: "test"},
			},
			operation:       admission.Create,
			wantErr:         true,
			wantErrContains: "expected historic job",
		},
		{
			name: "skips validation for objects being deleted",
			obj: &provisioning.HistoricJob{
				ObjectMeta: metav1.ObjectMeta{
					Name:              "test-historic-job",
					DeletionTimestamp: &metav1.Time{Time: time.Now()},
				},
				Spec: provisioning.JobSpec{
					// Invalid - missing action and repository
				},
			},
			operation: admission.Update,
			wantErr:   false,
		},
	}

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

			var obj runtime.Object
			if tt.obj != nil {
				obj = tt.obj.(runtime.Object)
			}

			attr := newHistoricJobAdmissionTestAttributes(obj, tt.operation)

			err := v.Validate(context.Background(), attr, nil)

			if tt.wantErr {
				require.Error(t, err)
				if tt.wantErrContains != "" {
					require.Contains(t, err.Error(), tt.wantErrContains)
				}
				return
			}

			require.NoError(t, err)
		})
	}
}

func newHistoricJobAdmissionTestAttributes(obj runtime.Object, op admission.Operation) admission.Attributes {
	return admission.NewAttributesRecord(
		obj,
		nil,
		provisioning.HistoricJobResourceInfo.GroupVersionKind(),
		"default",
		"test",
		provisioning.HistoricJobResourceInfo.GroupVersionResource(),
		"",
		op,
		nil,
		false,
		&user.DefaultInfo{},
	)
}
