package test

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"testing"

	"github.com/grafana/authlib/types"

	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"github.com/grafana/grafana/pkg/apimachinery/utils"
	"github.com/grafana/grafana/pkg/storage/unified/resource"
	"github.com/grafana/grafana/pkg/storage/unified/resourcepb"

	"github.com/stretchr/testify/require"
)

// RunStorageServerTest runs the storage server test suite
func RunStorageServerTest(t *testing.T, newBackend NewBackendFunc) {
	runTestResourcePermissionScenarios(t, newBackend(context.Background()), GenerateRandomNSPrefix())
	runTestListTrashAccessControl(t, newBackend(context.Background()), GenerateRandomNSPrefix())
}

// func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBackend, nsPrefix string) {
func runTestResourcePermissionScenarios(t *testing.T, backend resource.StorageBackend, nsPrefix string) {
	// Test user
	testUser := &identity.StaticRequester{
		Type:           types.TypeUser,
		Login:          "testuser",
		UserID:         123,
		UserUID:        "u123",
		OrgRole:        identity.RoleAdmin,
		IsGrafanaAdmin: true,
	}

	testCases := []struct {
		name           string
		initialFolder  string
		targetFolder   string
		permissionMap  map[string]bool
		expectSuccess  bool
		expectedChecks int
		expectedError  int32 // HTTP status code for error, 0 if no error expected
	}{
		{
			name:           "Create resource in folder",
			initialFolder:  "folder1",
			targetFolder:   "", // No move, just create
			permissionMap:  map[string]bool{"folder1:create": true},
			expectSuccess:  true,
			expectedChecks: 1,
			expectedError:  0,
		},
		{
			name:           "Create resource denied",
			initialFolder:  "folder1",
			targetFolder:   "", // No move, just create
			permissionMap:  map[string]bool{"folder1:create": false},
			expectSuccess:  false,
			expectedChecks: 1,
			expectedError:  http.StatusForbidden,
		},
		{
			name:           "Update resource in same folder",
			initialFolder:  "folder1",
			targetFolder:   "folder1", // Same folder, just update
			permissionMap:  map[string]bool{"folder1:update": true},
			expectSuccess:  true,
			expectedChecks: 1,
			expectedError:  0,
		},
		{
			name:           "Update resource denied",
			initialFolder:  "folder1",
			targetFolder:   "folder1", // Same folder
			permissionMap:  map[string]bool{"folder1:update": false},
			expectSuccess:  false,
			expectedChecks: 1,
			expectedError:  http.StatusForbidden,
		},
		{
			name:           "Move resource to another folder - allowed",
			initialFolder:  "folder1",
			targetFolder:   "folder2", // Moving to a different folder
			permissionMap:  map[string]bool{"folder1:update": true, "folder2:create": true},
			expectSuccess:  true,
			expectedChecks: 2, // Should check both folders
			expectedError:  0,
		},
		{
			name:           "Move resource - source folder access denied",
			initialFolder:  "folder1",
			targetFolder:   "folder2",
			permissionMap:  map[string]bool{"folder1:update": false, "folder2:create": true},
			expectSuccess:  false,
			expectedChecks: 1, // Should stop at first check
			expectedError:  http.StatusForbidden,
		},
		{
			name:           "Move resource - destination folder access denied",
			initialFolder:  "folder1",
			targetFolder:   "folder2",
			permissionMap:  map[string]bool{"folder1:update": true, "folder2:create": false},
			expectSuccess:  false,
			expectedChecks: 2, // Should do both checks but fail on second
			expectedError:  http.StatusForbidden,
		},
		{
			name:           "Move resource - from empty folder to named folder",
			initialFolder:  "",
			targetFolder:   "folder1",
			permissionMap:  map[string]bool{":update": true, "folder1:create": true},
			expectSuccess:  true,
			expectedChecks: 2,
			expectedError:  0,
		},
	}

	for i, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Create a unique resource name for each test
			resourceName := fmt.Sprintf("test-resource-%d", i)
			resourceUID := fmt.Sprintf("test123-%d", i)

			// Create a mock access client with the test case's permission map
			checksPerformed := []CheckRequestEX{}
			mockAccess := &mockAccessClient{
				allowed:    false, // Default to false
				allowedMap: tc.permissionMap,
				checkFn: func(req types.CheckRequest, folder string) {
					checksPerformed = append(checksPerformed, CheckRequestEX{
						CheckRequest: req,
						Folder:       folder,
					})
				},
			}

			server, err := resource.NewResourceServer(resource.ResourceServerOptions{
				Backend:      backend,
				AccessClient: mockAccess,
			})
			require.NoError(t, err)

			ctx := types.WithAuthInfo(context.Background(), testUser)

			key := &resourcepb.ResourceKey{
				Group:     "test.grafana.app",
				Resource:  "testresources",
				Namespace: nsPrefix + "-ns1",
				Name:      resourceName,
			}

			if tc.targetFolder == "" {
				// Create resource with unique name
				resourceJSON := fmt.Sprintf(`{
					"apiVersion": "test.grafana.app/v1",
					"kind": "TestResource",
					"metadata": {
						"name": "%s",
						"uid": "%s",
						"namespace": "%s",
						"annotations": {
							"grafana.app/folder": "%s"
						}
					},
					"spec": {
						"title": "Test Resource %d"
					}
				}`, resourceName, resourceUID, nsPrefix+"-ns1", tc.initialFolder, i)

				checksPerformed = []CheckRequestEX{}
				created, err := server.Create(ctx, &resourcepb.CreateRequest{
					Value: []byte(resourceJSON),
					Key:   key,
				})
				require.NoError(t, err)

				if tc.expectSuccess {
					require.Nil(t, created.Error)
					require.True(t, created.ResourceVersion > 0)
				} else {
					require.NotNil(t, created.Error)
					require.Equal(t, tc.expectedError, created.Error.Code)
				}

				require.Len(t, checksPerformed, tc.expectedChecks)
				if len(checksPerformed) > 0 {
					require.Equal(t, utils.VerbCreate, checksPerformed[0].Verb)
					require.Equal(t, tc.initialFolder, checksPerformed[0].Folder)
				}
			} else {
				// Create a resource first, then update/move it
				initialResourceJSON := fmt.Sprintf(`{
					"apiVersion": "test.grafana.app/v1",
					"kind": "TestResource",
					"metadata": {
						"name": "%s",
						"uid": "%s",
						"namespace": "%s",
						"annotations": {
							"grafana.app/folder": "%s"
						}
					},
					"spec": {
						"title": "Test Resource %d"
					}
				}`, resourceName, resourceUID, nsPrefix+"-ns1", tc.initialFolder, i)

				// Override permissions for initial creation to always succeed
				mockAccess.allowed = true
				created, err := server.Create(ctx, &resourcepb.CreateRequest{
					Value: []byte(initialResourceJSON),
					Key:   key,
				})
				require.NoError(t, err)
				require.Nil(t, created.Error)

				// Now try the update/move with the configured permissions
				targetResourceJSON := fmt.Sprintf(`{
					"apiVersion": "test.grafana.app/v1",
					"kind": "TestResource",
					"metadata": {
						"name": "%s",
						"uid": "%s",
						"namespace": "%s",
						"annotations": {
							"grafana.app/folder": "%s"
						}
					},
					"spec": {
						"title": "Test Resource %d Updated"
					}
				}`, resourceName, resourceUID, nsPrefix+"-ns1", tc.targetFolder, i)

				mockAccess.allowed = false // Reset to use the map
				checksPerformed = []CheckRequestEX{}

				updated, err := server.Update(ctx, &resourcepb.UpdateRequest{
					Key:             key,
					Value:           []byte(targetResourceJSON),
					ResourceVersion: created.ResourceVersion,
				})
				require.NoError(t, err)

				if tc.expectSuccess {
					require.Nil(t, updated.Error)
					require.True(t, updated.ResourceVersion > created.ResourceVersion)
				} else {
					require.NotNil(t, updated.Error)
					require.Equal(t, tc.expectedError, updated.Error.Code)
				}

				require.Len(t, checksPerformed, tc.expectedChecks)

				// Verify the correct permission checks were made
				if tc.initialFolder != tc.targetFolder && len(checksPerformed) >= 2 {
					// This is a folder move operation
					require.Equal(t, utils.VerbUpdate, checksPerformed[0].Verb)
					require.Equal(t, tc.initialFolder, checksPerformed[0].Folder)

					require.Equal(t, utils.VerbCreate, checksPerformed[1].Verb)
					require.Equal(t, tc.targetFolder, checksPerformed[1].Folder)
				} else if len(checksPerformed) > 0 {
					// Regular update, no folder change
					require.Equal(t, utils.VerbUpdate, checksPerformed[0].Verb)
					require.Equal(t, tc.initialFolder, checksPerformed[0].Folder)
				}
			}
		})
	}
}

// runTestListTrashAccessControl tests the access control logic for ListTrash
func runTestListTrashAccessControl(t *testing.T, backend resource.StorageBackend, nsPrefix string) {
	// Create two different users
	testUserA := &identity.StaticRequester{
		Type:           types.TypeUser,
		Login:          "testuserA",
		UserID:         123,
		UserUID:        "u123",
		OrgRole:        identity.RoleAdmin,
		IsGrafanaAdmin: true, // admin user
	}

	testUserB := &identity.StaticRequester{
		Type:           types.TypeUser,
		Login:          "testuserB",
		UserID:         456,
		UserUID:        "u456",
		OrgRole:        identity.RoleEditor,
		IsGrafanaAdmin: false, // non-admin user
	}

	mockAccess := &mockAccessClient{
		allowed: true, // Allow regular access
		compileFn: func(user types.AuthInfo, req types.ListRequest) types.ItemChecker {
			return func(name, folder string) bool {
				if req.Verb == utils.VerbSetPermissions {
					if requester, ok := user.(identity.Requester); ok && requester.GetIsGrafanaAdmin() {
						return true // Admin users can access trash
					}
					return false // Non-admin users cannot access trash
				}
				return false
			}
		},
	}

	server, err := resource.NewResourceServer(resource.ResourceServerOptions{
		Backend:      backend,
		AccessClient: mockAccess,
	})
	require.NoError(t, err)

	// Create a resource and delete it with user A
	ctxA := types.WithAuthInfo(context.Background(), testUserA)

	raw := []byte(`{
		"apiVersion": "playlist.grafana.app/v0alpha1",
		"kind": "Playlist",
		"metadata": {
			"name": "trash-test-playlist",
			"uid": "trash-xyz",
			"namespace": "` + nsPrefix + `-trash-test",
			"annotations": {
				"grafana.app/repoName": "elsewhere",
				"grafana.app/repoPath": "path/to/item",
				"grafana.app/repoTimestamp": "2024-02-02T00:00:00Z"
			}
		},
		"spec": {
			"title": "trash test",
			"interval": "5m",
			"items": [
				{
					"type": "dashboard_by_uid",
					"value": "vmie2cmWz"
				}
			]
		}
	}`)

	key := &resourcepb.ResourceKey{
		Group:     "playlist.grafana.app",
		Resource:  "playlists",
		Namespace: nsPrefix + "-trash-test",
		Name:      "trash-test-playlist",
	}

	// Create the resource with user A
	created, err := server.Create(ctxA, &resourcepb.CreateRequest{
		Value: raw,
		Key:   key,
	})
	require.NoError(t, err)
	require.Nil(t, created.Error)

	// Delete the resource with user A
	deleted, err := server.Delete(ctxA, &resourcepb.DeleteRequest{
		Key:             key,
		ResourceVersion: created.ResourceVersion,
	})
	require.NoError(t, err)
	require.True(t, deleted.ResourceVersion > created.ResourceVersion)

	// Test 1: Admin user (user A) should be able to list trash and see their own deleted resource
	trashList, err := server.List(ctxA, &resourcepb.ListRequest{
		Source: resourcepb.ListRequest_TRASH,
		Options: &resourcepb.ListOptions{
			Key: &resourcepb.ResourceKey{
				Group:     key.Group,
				Resource:  key.Resource,
				Namespace: key.Namespace,
			},
		},
	})
	require.NoError(t, err)
	require.Nil(t, trashList.Error)
	require.Len(t, trashList.Items, 1, "Admin user should see the deleted resource in trash")

	// Test 2: Non-admin user (user B) who didn't delete the resource should NOT see it in trash
	ctxB := types.WithAuthInfo(context.Background(), testUserB)
	trashListB, err := server.List(ctxB, &resourcepb.ListRequest{
		Source: resourcepb.ListRequest_TRASH,
		Options: &resourcepb.ListOptions{
			Key: &resourcepb.ResourceKey{
				Group:     key.Group,
				Resource:  key.Resource,
				Namespace: key.Namespace,
			},
		},
	})
	require.NoError(t, err)
	require.Nil(t, trashListB.Error)
	require.Len(t, trashListB.Items, 0, "Non-admin user who didn't delete the resource should not see it in trash")

	// Test 3: Create and delete another resource with user B
	keyB := &resourcepb.ResourceKey{
		Group:     "playlist.grafana.app",
		Resource:  "playlists",
		Namespace: nsPrefix + "-trash-test",
		Name:      "trash-test-playlist-b",
	}

	rawB := []byte(`{
		"apiVersion": "playlist.grafana.app/v0alpha1",
		"kind": "Playlist",
		"metadata": {
			"name": "trash-test-playlist-b",
			"uid": "trash-xyz-b",
			"namespace": "` + nsPrefix + `-trash-test",
			"annotations": {
				"grafana.app/repoName": "elsewhere",
				"grafana.app/repoPath": "path/to/item",
				"grafana.app/repoTimestamp": "2024-02-02T00:00:00Z"
			}
		},
		"spec": {
			"title": "trash test b",
			"interval": "5m",
			"items": [
				{
					"type": "dashboard_by_uid",
					"value": "vmie2cmWz"
				}
			]
		}
	}`)

	// Create the resource with user B
	createdB, err := server.Create(ctxB, &resourcepb.CreateRequest{
		Value: rawB,
		Key:   keyB,
	})
	require.NoError(t, err)
	require.Nil(t, createdB.Error)

	// Delete the resource with user B
	deletedB, err := server.Delete(ctxB, &resourcepb.DeleteRequest{
		Key:             keyB,
		ResourceVersion: createdB.ResourceVersion,
	})
	require.NoError(t, err)
	require.True(t, deletedB.ResourceVersion > createdB.ResourceVersion)

	// Test 4: User B should see their own deleted resource in trash
	trashListB2, err := server.List(ctxB, &resourcepb.ListRequest{
		Source: resourcepb.ListRequest_TRASH,
		Options: &resourcepb.ListOptions{
			Key: &resourcepb.ResourceKey{
				Group:     keyB.Group,
				Resource:  keyB.Resource,
				Namespace: keyB.Namespace,
			},
		},
	})
	require.NoError(t, err)
	require.Nil(t, trashListB2.Error)
	require.Len(t, trashListB2.Items, 1, "User should see their own deleted resource in trash")

	// Test 5: Admin user should see both deleted resources
	trashListA2, err := server.List(ctxA, &resourcepb.ListRequest{
		Source: resourcepb.ListRequest_TRASH,
		Options: &resourcepb.ListOptions{
			Key: &resourcepb.ResourceKey{
				Group:     key.Group,
				Resource:  key.Resource,
				Namespace: key.Namespace,
			},
		},
	})
	require.NoError(t, err)
	require.Nil(t, trashListA2.Error)
	require.Len(t, trashListA2.Items, 2, "Admin user should see all deleted resources in trash")

	// Test 6: Verify the trash items have the correct metadata
	for _, item := range trashListA2.Items {
		var obj map[string]interface{}
		err := json.Unmarshal(item.Value, &obj)
		require.NoError(t, err)

		// Check that the item has deletion timestamp
		metadata, ok := obj["metadata"].(map[string]interface{})
		require.True(t, ok, "Resource should have metadata")
		require.NotNil(t, metadata["deletionTimestamp"], "Trash item should have deletion timestamp")

		// Check that the item has the correct updatedBy field
		annotations, ok := metadata["annotations"].(map[string]interface{})
		require.True(t, ok, "Resource should have annotations")
		require.Contains(t, annotations, "grafana.app/updatedBy", "Trash item should have updatedBy annotation")
	}
}

// Mock access client for testing
type mockAccessClient struct {
	allowed    bool
	allowedMap map[string]bool
	checkFn    func(types.CheckRequest, string)
	compileFn  func(user types.AuthInfo, req types.ListRequest) types.ItemChecker
}

func (m *mockAccessClient) Check(ctx context.Context, user types.AuthInfo, req types.CheckRequest, folder string) (types.CheckResponse, error) {
	if m.checkFn != nil {
		m.checkFn(req, folder)
	}

	// Check specific folder:verb mappings if provided
	if m.allowedMap != nil {
		key := fmt.Sprintf("%s:%s", folder, req.Verb)
		if allowed, exists := m.allowedMap[key]; exists {
			return types.CheckResponse{Allowed: allowed}, nil
		}
	}

	return types.CheckResponse{Allowed: m.allowed}, nil
}

func (m *mockAccessClient) Compile(ctx context.Context, user types.AuthInfo, req types.ListRequest) (types.ItemChecker, types.Zookie, error) {
	if m.compileFn != nil {
		return m.compileFn(user, req), types.NoopZookie{}, nil
	}
	return func(name, folder string) bool {
		key := fmt.Sprintf("%s:%s", folder, req.Verb)
		if allowed, exists := m.allowedMap[key]; exists {
			return allowed
		}
		return m.allowed
	}, types.NoopZookie{}, nil
}

type CheckRequestEX struct {
	types.CheckRequest
	Folder string
}
