package conversion

import (
	"context"
	"sync/atomic"
	"testing"
	"time"

	dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
	dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1"
	dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
	dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
	"github.com/grafana/grafana/apps/dashboard/pkg/migration"
	"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
	common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// countingDataSourceProvider tracks how many times Index() is called
type countingDataSourceProvider struct {
	datasources []schemaversion.DataSourceInfo
	callCount   atomic.Int64
}

func newCountingDataSourceProvider(datasources []schemaversion.DataSourceInfo) *countingDataSourceProvider {
	return &countingDataSourceProvider{
		datasources: datasources,
	}
}

func (p *countingDataSourceProvider) Index(_ context.Context) *schemaversion.DatasourceIndex {
	p.callCount.Add(1)
	return schemaversion.NewDatasourceIndex(p.datasources)
}

func (p *countingDataSourceProvider) getCallCount() int64 {
	return p.callCount.Load()
}

// countingLibraryElementProvider tracks how many times GetLibraryElementInfo() is called
type countingLibraryElementProvider struct {
	elements  []schemaversion.LibraryElementInfo
	callCount atomic.Int64
}

func newCountingLibraryElementProvider(elements []schemaversion.LibraryElementInfo) *countingLibraryElementProvider {
	return &countingLibraryElementProvider{
		elements: elements,
	}
}

func (p *countingLibraryElementProvider) GetLibraryElementInfo(_ context.Context) []schemaversion.LibraryElementInfo {
	p.callCount.Add(1)
	return p.elements
}

func (p *countingLibraryElementProvider) getCallCount() int64 {
	return p.callCount.Load()
}

// createTestV0Dashboard creates a minimal v0 dashboard for testing
// The dashboard has a datasource with UID only (no type) to force provider lookup
// and includes library panels to test library element provider caching
func createTestV0Dashboard(namespace, title string) *dashv0.Dashboard {
	return &dashv0.Dashboard{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-dashboard",
			Namespace: namespace,
		},
		Spec: common.Unstructured{
			Object: map[string]interface{}{
				"title":         title,
				"schemaVersion": schemaversion.LATEST_VERSION,
				// Variables with datasource reference that requires lookup
				"templating": map[string]interface{}{
					"list": []interface{}{
						map[string]interface{}{
							"name":  "query_var",
							"type":  "query",
							"query": "label_values(up, job)",
							// Datasource with UID only - type needs to be looked up
							"datasource": map[string]interface{}{
								"uid": "ds1",
								// type is intentionally omitted to trigger provider lookup
							},
						},
					},
				},
				"panels": []interface{}{
					map[string]interface{}{
						"id":    1,
						"title": "Test Panel",
						"type":  "timeseries",
						"targets": []interface{}{
							map[string]interface{}{
								// Datasource with UID only - type needs to be looked up
								"datasource": map[string]interface{}{
									"uid": "ds1",
								},
							},
						},
					},
					// Library panel reference - triggers library element provider lookup
					map[string]interface{}{
						"id":    2,
						"title": "Library Panel with Horizontal Repeat",
						"type":  "library-panel-ref",
						"gridPos": map[string]interface{}{
							"h": 8,
							"w": 12,
							"x": 0,
							"y": 8,
						},
						"libraryPanel": map[string]interface{}{
							"uid":  "lib-panel-repeat-h",
							"name": "Library Panel with Horizontal Repeat",
						},
					},
					// Another library panel reference
					map[string]interface{}{
						"id":    3,
						"title": "Library Panel without Repeat",
						"type":  "library-panel-ref",
						"gridPos": map[string]interface{}{
							"h": 3,
							"w": 6,
							"x": 0,
							"y": 16,
						},
						"libraryPanel": map[string]interface{}{
							"uid":  "lib-panel-no-repeat",
							"name": "Library Panel without Repeat",
						},
					},
				},
			},
		},
	}
}

// createTestV1Dashboard creates a minimal v1beta1 dashboard for testing
// The dashboard has a datasource with UID only (no type) to force provider lookup
// and includes library panels to test library element provider caching
func createTestV1Dashboard(namespace, title string) *dashv1.Dashboard {
	return &dashv1.Dashboard{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-dashboard",
			Namespace: namespace,
		},
		Spec: common.Unstructured{
			Object: map[string]interface{}{
				"title":         title,
				"schemaVersion": schemaversion.LATEST_VERSION,
				// Variables with datasource reference that requires lookup
				"templating": map[string]interface{}{
					"list": []interface{}{
						map[string]interface{}{
							"name":  "query_var",
							"type":  "query",
							"query": "label_values(up, job)",
							// Datasource with UID only - type needs to be looked up
							"datasource": map[string]interface{}{
								"uid": "ds1",
								// type is intentionally omitted to trigger provider lookup
							},
						},
					},
				},
				"panels": []interface{}{
					map[string]interface{}{
						"id":    1,
						"title": "Test Panel",
						"type":  "timeseries",
						"targets": []interface{}{
							map[string]interface{}{
								// Datasource with UID only - type needs to be looked up
								"datasource": map[string]interface{}{
									"uid": "ds1",
								},
							},
						},
					},
					// Library panel reference - triggers library element provider lookup
					map[string]interface{}{
						"id":    2,
						"title": "Library Panel with Vertical Repeat",
						"type":  "library-panel-ref",
						"gridPos": map[string]interface{}{
							"h": 4,
							"w": 6,
							"x": 0,
							"y": 8,
						},
						"libraryPanel": map[string]interface{}{
							"uid":  "lib-panel-repeat-v",
							"name": "Library Panel with Vertical Repeat",
						},
					},
					// Another library panel reference
					map[string]interface{}{
						"id":    3,
						"title": "Library Panel without Repeat",
						"type":  "library-panel-ref",
						"gridPos": map[string]interface{}{
							"h": 3,
							"w": 6,
							"x": 6,
							"y": 8,
						},
						"libraryPanel": map[string]interface{}{
							"uid":  "lib-panel-no-repeat",
							"name": "Library Panel without Repeat",
						},
					},
				},
			},
		},
	}
}

// TestConversionCaching_V0_to_V2alpha1 verifies caching works when converting V0 to V2alpha1
func TestConversionCaching_V0_to_V2alpha1(t *testing.T) {
	datasources := []schemaversion.DataSourceInfo{
		{UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true},
	}
	elements := []schemaversion.LibraryElementInfo{
		{UID: "lib-panel-repeat-h", Name: "Library Panel with Horizontal Repeat", Type: "timeseries"},
		{UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"},
	}

	underlyingDS := newCountingDataSourceProvider(datasources)
	underlyingLE := newCountingLibraryElementProvider(elements)

	cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute)
	cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute)

	migration.ResetForTesting()
	migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL)

	// Convert multiple dashboards in the same namespace
	numDashboards := 5
	namespace := "default"

	for i := 0; i < numDashboards; i++ {
		source := createTestV0Dashboard(namespace, "Dashboard "+string(rune('A'+i)))
		target := &dashv2alpha1.Dashboard{}

		err := Convert_V0_to_V2alpha1(source, target, nil, cachedDS, cachedLE)
		require.NoError(t, err, "conversion %d should succeed", i)
		require.NotNil(t, target.Spec)
	}

	// With caching, the underlying datasource provider should only be called once per namespace
	// The test dashboard has datasources without type that require lookup
	assert.Equal(t, int64(1), underlyingDS.getCallCount(),
		"datasource provider should be called only once for %d conversions in same namespace", numDashboards)
	// Library element provider should also be called only once per namespace due to caching
	assert.Equal(t, int64(1), underlyingLE.getCallCount(),
		"library element provider should be called only once for %d conversions in same namespace", numDashboards)
}

// TestConversionCaching_V0_to_V2beta1 verifies caching works when converting V0 to V2beta1
func TestConversionCaching_V0_to_V2beta1(t *testing.T) {
	datasources := []schemaversion.DataSourceInfo{
		{UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true},
	}
	elements := []schemaversion.LibraryElementInfo{
		{UID: "lib-panel-repeat-h", Name: "Library Panel with Horizontal Repeat", Type: "timeseries"},
		{UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"},
	}

	underlyingDS := newCountingDataSourceProvider(datasources)
	underlyingLE := newCountingLibraryElementProvider(elements)

	cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute)
	cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute)

	migration.ResetForTesting()
	migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL)

	numDashboards := 5
	namespace := "default"

	for i := 0; i < numDashboards; i++ {
		source := createTestV0Dashboard(namespace, "Dashboard "+string(rune('A'+i)))
		target := &dashv2beta1.Dashboard{}

		err := Convert_V0_to_V2beta1(source, target, nil, cachedDS, cachedLE)
		require.NoError(t, err, "conversion %d should succeed", i)
		require.NotNil(t, target.Spec)
	}

	assert.Equal(t, int64(1), underlyingDS.getCallCount(),
		"datasource provider should be called only once for %d conversions in same namespace", numDashboards)
	assert.Equal(t, int64(1), underlyingLE.getCallCount(),
		"library element provider should be called only once for %d conversions in same namespace", numDashboards)
}

// TestConversionCaching_V1beta1_to_V2alpha1 verifies caching works when converting V1beta1 to V2alpha1
func TestConversionCaching_V1beta1_to_V2alpha1(t *testing.T) {
	datasources := []schemaversion.DataSourceInfo{
		{UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true},
	}
	elements := []schemaversion.LibraryElementInfo{
		{UID: "lib-panel-repeat-v", Name: "Library Panel with Vertical Repeat", Type: "timeseries"},
		{UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"},
	}

	underlyingDS := newCountingDataSourceProvider(datasources)
	underlyingLE := newCountingLibraryElementProvider(elements)

	cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute)
	cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute)

	migration.ResetForTesting()
	migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL)

	numDashboards := 5
	namespace := "default"

	for i := 0; i < numDashboards; i++ {
		source := createTestV1Dashboard(namespace, "Dashboard "+string(rune('A'+i)))
		target := &dashv2alpha1.Dashboard{}

		err := Convert_V1_to_V2alpha1(source, target, nil, cachedDS, cachedLE)
		require.NoError(t, err, "conversion %d should succeed", i)
		require.NotNil(t, target.Spec)
	}

	assert.Equal(t, int64(1), underlyingDS.getCallCount(),
		"datasource provider should be called only once for %d conversions in same namespace", numDashboards)
	assert.Equal(t, int64(1), underlyingLE.getCallCount(),
		"library element provider should be called only once for %d conversions in same namespace", numDashboards)
}

// TestConversionCaching_V1beta1_to_V2beta1 verifies caching works when converting V1beta1 to V2beta1
func TestConversionCaching_V1beta1_to_V2beta1(t *testing.T) {
	datasources := []schemaversion.DataSourceInfo{
		{UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true},
	}
	elements := []schemaversion.LibraryElementInfo{
		{UID: "lib-panel-repeat-v", Name: "Library Panel with Vertical Repeat", Type: "timeseries"},
		{UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"},
	}

	underlyingDS := newCountingDataSourceProvider(datasources)
	underlyingLE := newCountingLibraryElementProvider(elements)

	cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute)
	cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute)

	migration.ResetForTesting()
	migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL)

	numDashboards := 5
	namespace := "default"

	for i := 0; i < numDashboards; i++ {
		source := createTestV1Dashboard(namespace, "Dashboard "+string(rune('A'+i)))
		target := &dashv2beta1.Dashboard{}

		err := Convert_V1_to_V2beta1(source, target, nil, cachedDS, cachedLE)
		require.NoError(t, err, "conversion %d should succeed", i)
		require.NotNil(t, target.Spec)
	}

	assert.Equal(t, int64(1), underlyingDS.getCallCount(),
		"datasource provider should be called only once for %d conversions in same namespace", numDashboards)
	assert.Equal(t, int64(1), underlyingLE.getCallCount(),
		"library element provider should be called only once for %d conversions in same namespace", numDashboards)
}

// TestConversionCaching_MultipleNamespaces verifies that different namespaces get separate cache entries
func TestConversionCaching_MultipleNamespaces(t *testing.T) {
	datasources := []schemaversion.DataSourceInfo{
		{UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true},
	}
	elements := []schemaversion.LibraryElementInfo{
		{UID: "lib-panel-repeat-h", Name: "Library Panel with Horizontal Repeat", Type: "timeseries"},
		{UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"},
	}

	underlyingDS := newCountingDataSourceProvider(datasources)
	underlyingLE := newCountingLibraryElementProvider(elements)

	cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute)
	cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute)

	migration.ResetForTesting()
	migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL)

	namespaces := []string{"default", "org-2", "org-3"}
	numDashboardsPerNs := 3

	for _, ns := range namespaces {
		for i := 0; i < numDashboardsPerNs; i++ {
			source := createTestV0Dashboard(ns, "Dashboard "+string(rune('A'+i)))
			target := &dashv2alpha1.Dashboard{}

			err := Convert_V0_to_V2alpha1(source, target, nil, cachedDS, cachedLE)
			require.NoError(t, err, "conversion for namespace %s should succeed", ns)
		}
	}

	// With caching, each namespace should result in one call to the underlying provider
	expectedCalls := int64(len(namespaces))
	assert.Equal(t, expectedCalls, underlyingDS.getCallCount(),
		"datasource provider should be called once per namespace (%d namespaces)", len(namespaces))
	assert.Equal(t, expectedCalls, underlyingLE.getCallCount(),
		"library element provider should be called once per namespace (%d namespaces)", len(namespaces))
}

// TestConversionCaching_CacheDisabled verifies that TTL=0 disables caching
func TestConversionCaching_CacheDisabled(t *testing.T) {
	datasources := []schemaversion.DataSourceInfo{
		{UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true},
	}
	elements := []schemaversion.LibraryElementInfo{
		{UID: "lib-panel-repeat-h", Name: "Library Panel with Horizontal Repeat", Type: "timeseries"},
		{UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"},
	}

	underlyingDS := newCountingDataSourceProvider(datasources)
	underlyingLE := newCountingLibraryElementProvider(elements)

	// TTL of 0 should disable caching - the wrapper returns the underlying provider directly
	cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, 0)
	cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, 0)

	migration.ResetForTesting()
	migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL)

	numDashboards := 3
	namespace := "default"

	for i := 0; i < numDashboards; i++ {
		source := createTestV0Dashboard(namespace, "Dashboard "+string(rune('A'+i)))
		target := &dashv2alpha1.Dashboard{}

		err := Convert_V0_to_V2alpha1(source, target, nil, cachedDS, cachedLE)
		require.NoError(t, err, "conversion %d should succeed", i)
	}

	// Without caching, each conversion calls the underlying provider multiple times
	// (once for each datasource lookup needed - variables and panels)
	// The key check is that the count is GREATER than 1 per conversion (no caching benefit)
	assert.Greater(t, underlyingDS.getCallCount(), int64(numDashboards),
		"with cache disabled, conversions should call datasource provider multiple times")
	// Library element provider is also called for each conversion without caching
	assert.GreaterOrEqual(t, underlyingLE.getCallCount(), int64(numDashboards),
		"with cache disabled, conversions should call library element provider multiple times")
}
