package resources

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/dynamic"

	"github.com/grafana/grafana/pkg/apimachinery/utils"
)

func TestRepositoryResources_FindResourcePath(t *testing.T) {
	tests := []struct {
		name          string
		resourceName  string
		gvk           schema.GroupVersionKind
		expectedGVR   schema.GroupVersionResource
		forKindError  error
		getError      error
		resourceObj   *unstructured.Unstructured
		expectedPath  string
		expectedError string
	}{
		{
			name:         "dashboard found successfully",
			resourceName: "test-dashboard",
			gvk: schema.GroupVersionKind{
				Group: "dashboard.grafana.app",
				Kind:  "Dashboard",
			},
			expectedGVR: schema.GroupVersionResource{
				Group:    "dashboard.grafana.app",
				Version:  "v0alpha1",
				Resource: "dashboards",
			},
			resourceObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name":      "test-dashboard",
						"namespace": "test-namespace",
						"annotations": map[string]interface{}{
							utils.AnnoKeySourcePath: "dashboards/test-dashboard.json",
						},
					},
				},
			},
			expectedPath: "dashboards/test-dashboard.json",
		},
		{
			name:         "folder found successfully",
			resourceName: "test-folder",
			gvk: schema.GroupVersionKind{
				Group: "folder.grafana.app",
				Kind:  "Folder",
			},
			expectedGVR: schema.GroupVersionResource{
				Group:    "folder.grafana.app",
				Version:  "v0alpha1",
				Resource: "folders",
			},
			resourceObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name":      "test-folder",
						"namespace": "test-namespace",
						"annotations": map[string]interface{}{
							utils.AnnoKeySourcePath: "folders/test-folder",
						},
					},
				},
			},
			expectedPath: "folders/test-folder/", // Trailing slash added for folder resources
		},
		{
			name:         "ForKind fails",
			resourceName: "test-dashboard",
			gvk: schema.GroupVersionKind{
				Group: "dashboard.grafana.app",
				Kind:  "Dashboard",
			},
			forKindError:  errors.New("kind not found"),
			expectedError: "get client for kind Dashboard: kind not found",
		},
		{
			name:         "resource not found",
			resourceName: "nonexistent-dashboard",
			gvk: schema.GroupVersionKind{
				Group: "dashboard.grafana.app",
				Kind:  "Dashboard",
			},
			expectedGVR: schema.GroupVersionResource{
				Group:    "dashboard.grafana.app",
				Version:  "v0alpha1",
				Resource: "dashboards",
			},
			getError:      apierrors.NewNotFound(schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, "nonexistent-dashboard"),
			expectedError: "resource not found: dashboard.grafana.app/dashboards/nonexistent-dashboard",
		},
		{
			name:         "Get operation fails with other error",
			resourceName: "test-dashboard",
			gvk: schema.GroupVersionKind{
				Group: "dashboard.grafana.app",
				Kind:  "Dashboard",
			},
			expectedGVR: schema.GroupVersionResource{
				Group:    "dashboard.grafana.app",
				Version:  "v0alpha1",
				Resource: "dashboards",
			},
			getError:      errors.New("internal server error"),
			expectedError: "failed to get resource dashboard.grafana.app/dashboards/test-dashboard: internal server error",
		},
		{
			name:         "resource has no annotations",
			resourceName: "test-dashboard",
			gvk: schema.GroupVersionKind{
				Group: "dashboard.grafana.app",
				Kind:  "Dashboard",
			},
			expectedGVR: schema.GroupVersionResource{
				Group:    "dashboard.grafana.app",
				Version:  "v0alpha1",
				Resource: "dashboards",
			},
			resourceObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name":      "test-dashboard",
						"namespace": "test-namespace",
						// No annotations
					},
				},
			},
			expectedError: "resource dashboard.grafana.app/dashboards/test-dashboard has no annotations",
		},
		{
			name:         "resource has empty annotations",
			resourceName: "test-dashboard",
			gvk: schema.GroupVersionKind{
				Group: "dashboard.grafana.app",
				Kind:  "Dashboard",
			},
			expectedGVR: schema.GroupVersionResource{
				Group:    "dashboard.grafana.app",
				Version:  "v0alpha1",
				Resource: "dashboards",
			},
			resourceObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name":        "test-dashboard",
						"namespace":   "test-namespace",
						"annotations": map[string]interface{}{},
					},
				},
			},
			expectedError: "resource dashboard.grafana.app/dashboards/test-dashboard has no source path annotation",
		},
		{
			name:         "resource has empty source path annotation",
			resourceName: "test-dashboard",
			gvk: schema.GroupVersionKind{
				Group: "dashboard.grafana.app",
				Kind:  "Dashboard",
			},
			expectedGVR: schema.GroupVersionResource{
				Group:    "dashboard.grafana.app",
				Version:  "v0alpha1",
				Resource: "dashboards",
			},
			resourceObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name":      "test-dashboard",
						"namespace": "test-namespace",
						"annotations": map[string]interface{}{
							utils.AnnoKeySourcePath: "", // Empty path
						},
					},
				},
			},
			expectedError: "resource dashboard.grafana.app/dashboards/test-dashboard has no source path annotation",
		},
		{
			name:         "resource with nested folder path",
			resourceName: "nested-dashboard",
			gvk: schema.GroupVersionKind{
				Group: "dashboard.grafana.app",
				Kind:  "Dashboard",
			},
			expectedGVR: schema.GroupVersionResource{
				Group:    "dashboard.grafana.app",
				Version:  "v0alpha1",
				Resource: "dashboards",
			},
			resourceObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name":      "nested-dashboard",
						"namespace": "test-namespace",
						"annotations": map[string]interface{}{
							utils.AnnoKeySourcePath: "team-a/subfolder/nested-dashboard.json",
						},
					},
				},
			},
			expectedPath: "team-a/subfolder/nested-dashboard.json",
		},
		{
			name:         "folder without trailing slash gets slash added",
			resourceName: "test-folder",
			gvk: schema.GroupVersionKind{
				Group: "folder.grafana.app",
				Kind:  "Folder",
			},
			expectedGVR: schema.GroupVersionResource{
				Group:    "folder.grafana.app",
				Version:  "v0alpha1",
				Resource: "folders",
			},
			resourceObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name":      "test-folder",
						"namespace": "test-namespace",
						"annotations": map[string]interface{}{
							utils.AnnoKeySourcePath: "folders/test-folder", // No trailing slash
						},
					},
				},
			},
			expectedPath: "folders/test-folder/", // Should have trailing slash added
		},
		{
			name:         "folder with trailing slash keeps slash",
			resourceName: "test-folder-2",
			gvk: schema.GroupVersionKind{
				Group: "folder.grafana.app",
				Kind:  "Folder",
			},
			expectedGVR: schema.GroupVersionResource{
				Group:    "folder.grafana.app",
				Version:  "v0alpha1",
				Resource: "folders",
			},
			resourceObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name":      "test-folder-2",
						"namespace": "test-namespace",
						"annotations": map[string]interface{}{
							utils.AnnoKeySourcePath: "folders/test-folder-2/", // Already has trailing slash
						},
					},
				},
			},
			expectedPath: "folders/test-folder-2/", // Should keep existing trailing slash
		},
		{
			name:         "non-folder resource keeps path unchanged",
			resourceName: "test-dashboard",
			gvk: schema.GroupVersionKind{
				Group: "dashboard.grafana.app",
				Kind:  "Dashboard", // Not a folder
			},
			expectedGVR: schema.GroupVersionResource{
				Group:    "dashboard.grafana.app",
				Version:  "v0alpha1",
				Resource: "dashboards",
			},
			resourceObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name":      "test-dashboard",
						"namespace": "test-namespace",
						"annotations": map[string]interface{}{
							utils.AnnoKeySourcePath: "dashboards/test-dashboard", // No trailing slash
						},
					},
				},
			},
			expectedPath: "dashboards/test-dashboard", // Should remain unchanged for non-folders
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mocks
			mockClients := NewMockResourceClients(t)
			mockClient := &MockDynamicResourceInterface{}

			// Create repository resources with mocked dependencies
			resourcesManager := &ResourcesManager{
				clients: mockClients,
			}

			repositoryResources := &repositoryResources{
				ResourcesManager: resourcesManager,
				namespace:        "test-namespace",
				repoName:         "test-repo",
			}

			// Mock ForKind call
			if tt.forKindError != nil {
				mockClients.On("ForKind", mock.Anything, tt.gvk).Return(nil, schema.GroupVersionResource{}, tt.forKindError)
			} else {
				mockClients.On("ForKind", mock.Anything, tt.gvk).Return(mockClient, tt.expectedGVR, nil)

				// Mock Get call if ForKind succeeds
				if tt.getError != nil {
					mockClient.On("Get", mock.Anything, tt.resourceName, metav1.GetOptions{}, mock.Anything).Return(nil, tt.getError)
				} else {
					mockClient.On("Get", mock.Anything, tt.resourceName, metav1.GetOptions{}, mock.Anything).Return(tt.resourceObj, nil)
				}
			}

			// Execute the method
			result, err := repositoryResources.FindResourcePath(context.Background(), tt.resourceName, tt.gvk)

			// Assert results
			if tt.expectedError != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), tt.expectedError)
				require.Empty(t, result)
			} else {
				require.NoError(t, err)
				require.Equal(t, tt.expectedPath, result)
			}

			// Verify all mocks were called as expected
			mockClients.AssertExpectations(t)
			if tt.forKindError == nil {
				mockClient.AssertExpectations(t)
			}
		})
	}
}

// MockDynamicResourceInterface is a mock for dynamic.ResourceInterface
type MockDynamicResourceInterface struct {
	mock.Mock
}

func (m *MockDynamicResourceInterface) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) {
	args := m.Called(ctx, obj, options, subresources)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}

func (m *MockDynamicResourceInterface) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) {
	args := m.Called(ctx, obj, options, subresources)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}

func (m *MockDynamicResourceInterface) UpdateStatus(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions) (*unstructured.Unstructured, error) {
	args := m.Called(ctx, obj, options)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}

func (m *MockDynamicResourceInterface) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
	args := m.Called(ctx, name, options, subresources)
	return args.Error(0)
}

func (m *MockDynamicResourceInterface) DeleteCollection(ctx context.Context, options metav1.DeleteOptions, listOptions metav1.ListOptions) error {
	args := m.Called(ctx, options, listOptions)
	return args.Error(0)
}

func (m *MockDynamicResourceInterface) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
	args := m.Called(ctx, name, options, subresources)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}

func (m *MockDynamicResourceInterface) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
	args := m.Called(ctx, opts)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*unstructured.UnstructuredList), args.Error(1)
}

func (m *MockDynamicResourceInterface) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
	args := m.Called(ctx, opts)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(watch.Interface), args.Error(1)
}

func (m *MockDynamicResourceInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) {
	args := m.Called(ctx, name, pt, data, options, subresources)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}

func (m *MockDynamicResourceInterface) Apply(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions, subresources ...string) (*unstructured.Unstructured, error) {
	args := m.Called(ctx, name, obj, options, subresources)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}

func (m *MockDynamicResourceInterface) ApplyStatus(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions) (*unstructured.Unstructured, error) {
	args := m.Called(ctx, name, obj, options)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}

// Ensure MockDynamicResourceInterface implements dynamic.ResourceInterface
var _ dynamic.ResourceInterface = (*MockDynamicResourceInterface)(nil)

func TestCheckResourceOwnership(t *testing.T) {
	tests := []struct {
		name              string
		existingResource  *unstructured.Unstructured
		requestingManager utils.ManagerProperties
		expectError       bool
		expectedMessage   string
	}{
		{
			name:             "no existing resource - allow operation",
			existingResource: nil, // Explicitly nil to represent non-existing resource
			requestingManager: utils.ManagerProperties{
				Kind:     utils.ManagerKindRepo,
				Identity: "repo-1",
			},
			expectError: false,
		},
		{
			name: "existing resource with no manager - allow operation",
			existingResource: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name": "test-resource",
					},
				},
			},
			requestingManager: utils.ManagerProperties{
				Kind:     utils.ManagerKindRepo,
				Identity: "repo-1",
			},
			expectError: false,
		},
		{
			name: "same manager - allow operation",
			existingResource: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name": "test-resource",
						"annotations": map[string]interface{}{
							utils.AnnoKeyManagerKind:     "repo",
							utils.AnnoKeyManagerIdentity: "repo-1",
						},
					},
				},
			},
			requestingManager: utils.ManagerProperties{
				Kind:     utils.ManagerKindRepo,
				Identity: "repo-1",
			},
			expectError: false,
		},
		{
			name: "different manager but allows edits - allow operation",
			existingResource: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name": "test-resource",
						"annotations": map[string]interface{}{
							utils.AnnoKeyManagerKind:        "repo",
							utils.AnnoKeyManagerIdentity:    "repo-1",
							utils.AnnoKeyManagerAllowsEdits: "true",
						},
					},
				},
			},
			requestingManager: utils.ManagerProperties{
				Kind:     utils.ManagerKindRepo,
				Identity: "repo-2",
			},
			expectError: false,
		},
		{
			name: "different manager and doesn't allow edits - deny operation",
			existingResource: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name": "test-resource",
						"annotations": map[string]interface{}{
							utils.AnnoKeyManagerKind:     "repo",
							utils.AnnoKeyManagerIdentity: "repo-1",
						},
					},
				},
			},
			requestingManager: utils.ManagerProperties{
				Kind:     utils.ManagerKindRepo,
				Identity: "repo-2",
			},
			expectError:     true,
			expectedMessage: "resource 'test-resource' is managed by repo 'repo-1' and cannot be modified by repo 'repo-2'",
		},
		{
			name: "different manager types - deny operation",
			existingResource: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"metadata": map[string]interface{}{
						"name": "test-resource",
						"annotations": map[string]interface{}{
							utils.AnnoKeyManagerKind:     "terraform",
							utils.AnnoKeyManagerIdentity: "tf-stacks-1",
						},
					},
				},
			},
			requestingManager: utils.ManagerProperties{
				Kind:     utils.ManagerKindRepo,
				Identity: "repo-1",
			},
			expectError:     true,
			expectedMessage: "resource 'test-resource' is managed by terraform 'tf-stacks-1' and cannot be modified by repo 'repo-1'",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Test the package-level ownership check function directly
			err := CheckResourceOwnership(tt.existingResource, "test-resource", tt.requestingManager)

			if tt.expectError {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.expectedMessage)
				assert.True(t, apierrors.IsBadRequest(err))
			} else {
				require.NoError(t, err)
			}
		})
	}
}
