package testcases

import (
	"fmt"
	"net/http"
	"strings"
	"testing"

	authlib "github.com/grafana/authlib/types"
	"github.com/grafana/grafana/pkg/services/folder"
	"github.com/grafana/grafana/pkg/tests/apis"
	"github.com/stretchr/testify/require"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

// foldersAndDashboardsTestCase tests the "folders-dashboards" ResourceMigration
type foldersAndDashboardsTestCase struct {
	parentFolderUID   string
	childFolderUID    string
	dashboardUID      string
	largeDashboardUID string
	libPanelUID       string
}

// NewFoldersAndDashboardsTestCase creates a test case for the compound folders+dashboards migrator
func NewFoldersAndDashboardsTestCase() ResourceMigratorTestCase {
	return &foldersAndDashboardsTestCase{
		parentFolderUID: "parent-folder-uid",
		childFolderUID:  "child-folder-uid",
		dashboardUID:    "", // Will be generated during setup
		libPanelUID:     "", // Will be generated during setup
	}
}

func (tc *foldersAndDashboardsTestCase) Name() string {
	return "folders-dashboards"
}

func (tc *foldersAndDashboardsTestCase) Resources() []schema.GroupVersionResource {
	return []schema.GroupVersionResource{
		{
			Group:    "folder.grafana.app",
			Version:  "v1beta1",
			Resource: "folders",
		},
		{
			Group:    "dashboard.grafana.app",
			Version:  "v1beta1",
			Resource: "dashboards",
		},
	}
}

func (tc *foldersAndDashboardsTestCase) Setup(t *testing.T, helper *apis.K8sTestHelper) {
	t.Helper()

	// Create parent folder
	parent := createTestFolder(t, helper, tc.parentFolderUID, "parent-folder", "")

	// Create child folder (nested under parent)
	child := createTestFolder(t, helper, tc.childFolderUID, "child-folder", parent.UID)

	// Create library panel in child folder
	tc.libPanelUID = createTestLibraryPanel(t, helper, "Test Library Panel", child.UID)

	// Create a large dashboard (~3MB) to test migration performance with big resources.
	// On SQLite without sufficient cache_size, large inserts cause cache spills that
	// escalate to EXCLUSIVE locks and deadlock with concurrent readers.
	tc.largeDashboardUID = createLargeDashboard(t, helper, child.UID, 3*1024*1024)

	// Create dashboard with library panel in child folder
	tc.dashboardUID = createTestDashboardWithLibraryPanel(t, helper, "dashboard-with-library-panel",
		tc.libPanelUID, "Test LP in dashboard", child.UID)
}

func (tc *foldersAndDashboardsTestCase) Verify(t *testing.T, helper *apis.K8sTestHelper, shouldExist bool) {
	t.Helper()

	// Build maps of UIDs by resource type
	folderUIDs := []string{tc.parentFolderUID, tc.childFolderUID}
	dashboardUIDs := []string{tc.dashboardUID, tc.largeDashboardUID}

	expectedFolderCount := 0
	if shouldExist {
		expectedFolderCount = len(folderUIDs)
	}
	orgID := helper.Org1.OrgID
	namespace := authlib.OrgNamespaceFormatter(orgID)

	// Verify folders
	folderCli := helper.GetResourceClient(apis.ResourceClientArgs{
		User:      helper.Org1.Admin,
		Namespace: namespace,
		GVR: schema.GroupVersionResource{
			Group:    "folder.grafana.app",
			Version:  "v1beta1",
			Resource: "folders",
		},
	})
	verifyResourceCount(t, folderCli, expectedFolderCount)
	for _, uid := range folderUIDs {
		verifyResource(t, folderCli, uid, shouldExist)
	}

	// Verify dashboards
	expectedDashboardCount := 0
	if shouldExist {
		expectedDashboardCount = len(dashboardUIDs)
	}
	dashboardCli := helper.GetResourceClient(apis.ResourceClientArgs{
		User:      helper.Org1.Admin,
		Namespace: namespace,
		GVR: schema.GroupVersionResource{
			Group:    "dashboard.grafana.app",
			Version:  "v1beta1",
			Resource: "dashboards",
		},
	})
	verifyResourceCount(t, dashboardCli, expectedDashboardCount)
	for _, uid := range dashboardUIDs {
		verifyResource(t, dashboardCli, uid, shouldExist)
	}
}

// createTestFolder creates a folder with specified UID and optional parent
func createTestFolder(t *testing.T, helper *apis.K8sTestHelper, uid, title, parentUID string) *folder.Folder {
	t.Helper()

	payload := fmt.Sprintf(`{
		"title": "%s",
		"uid": "%s"`, title, uid)

	if parentUID != "" {
		payload += fmt.Sprintf(`,
		"parentUid": "%s"`, parentUID)
	}

	payload += "}"

	folderCreate := apis.DoRequest(helper, apis.RequestParams{
		User:   helper.Org1.Admin,
		Method: http.MethodPost,
		Path:   "/api/folders",
		Body:   []byte(payload),
	}, &folder.Folder{})

	require.NotNil(t, folderCreate.Result)
	require.Equal(t, uid, folderCreate.Result.UID)

	return folderCreate.Result
}

// createTestLibraryPanel creates a library panel in a folder
func createTestLibraryPanel(t *testing.T, helper *apis.K8sTestHelper, name, folderUID string) string {
	t.Helper()

	libPanelPayload := fmt.Sprintf(`{
		"kind": 1,
		"name": "%s",
		"folderUid": "%s",
		"model": {
			"type": "text",
			"title": "%s"
		}
	}`, name, folderUID, name)

	libCreate := apis.DoRequest(helper, apis.RequestParams{
		User:   helper.Org1.Admin,
		Method: http.MethodPost,
		Path:   "/api/library-elements",
		Body:   []byte(libPanelPayload),
	}, &map[string]interface{}{})

	require.NotNil(t, libCreate.Response)
	require.Equal(t, http.StatusOK, libCreate.Response.StatusCode)

	libPanelUID := (*libCreate.Result)["result"].(map[string]interface{})["uid"].(string)
	require.NotEmpty(t, libPanelUID)

	return libPanelUID
}

// createTestDashboardWithLibraryPanel creates a dashboard that uses a library panel
func createTestDashboardWithLibraryPanel(t *testing.T, helper *apis.K8sTestHelper, dashTitle, libPanelUID, libPanelName, folderUID string) string {
	t.Helper()

	dashPayload := fmt.Sprintf(`{
		"dashboard": {
			"title": "%s",
			"panels": [{
				"id": 1,
				"libraryPanel": {
					"uid": "%s",
					"name": "%s"
				}
			}]
		},
		"folderUid": "%s",
		"overwrite": false
	}`, dashTitle, libPanelUID, libPanelName, folderUID)

	dashCreate := apis.DoRequest(helper, apis.RequestParams{
		User:   helper.Org1.Admin,
		Method: http.MethodPost,
		Path:   "/api/dashboards/db",
		Body:   []byte(dashPayload),
	}, &map[string]interface{}{})

	require.NotNil(t, dashCreate.Response)
	require.Equal(t, http.StatusOK, dashCreate.Response.StatusCode)

	dashUID := (*dashCreate.Result)["uid"].(string)
	require.NotEmpty(t, dashUID)
	return dashUID
}

// createLargeDashboard creates a dashboard with padded description to reach targetBytes.
func createLargeDashboard(t *testing.T, helper *apis.K8sTestHelper, folderUID string, targetBytes int) string {
	t.Helper()

	// Generate padding to reach the target size. Each panel has ~100 bytes of overhead,
	// so we use a single panel with a large description field.
	padding := strings.Repeat("x", targetBytes)
	dashPayload := fmt.Sprintf(`{
		"dashboard": {
			"title": "large-dashboard-for-migration-test",
			"panels": [{
				"id": 1,
				"type": "text",
				"title": "padding",
				"options": {"content": "%s"}
			}]
		},
		"folderUid": "%s",
		"overwrite": false
	}`, padding, folderUID)

	dashCreate := apis.DoRequest(helper, apis.RequestParams{
		User:   helper.Org1.Admin,
		Method: http.MethodPost,
		Path:   "/api/dashboards/db",
		Body:   []byte(dashPayload),
	}, &map[string]interface{}{})

	require.NotNil(t, dashCreate.Response)
	require.Equal(t, http.StatusOK, dashCreate.Response.StatusCode,
		"failed to create large dashboard (%d bytes)", targetBytes)

	dashUID := (*dashCreate.Result)["uid"].(string)
	require.NotEmpty(t, dashUID)
	t.Logf("Created large dashboard %s (%d bytes)", dashUID, targetBytes)
	return dashUID
}
