package dashboard

import (
	"encoding/json"
	"testing"

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

// helper to build JSON from a map.
func toJSON(t *testing.T, v any) []byte {
	t.Helper()
	data, err := json.Marshal(v)
	require.NoError(t, err)
	return data
}

// minimalDashboardJSON returns a minimal valid dashboard with one value panel.
func minimalDashboardJSON(t *testing.T) []byte {
	t.Helper()
	return toJSON(t, map[string]any{
		"title": "Test",
		"widgets": []map[string]any{
			{
				"id":         "w1",
				"panelTypes": "value",
				"title":      "Requests",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{
								"queryName":  "A",
								"dataSource": "traces",
								"expression": "A",
							},
						},
					},
				},
			},
		},
	})
}

// --- Dashboard-level tests ---

func TestValidate_EmptyTitle(t *testing.T) {
	data := toJSON(t, map[string]any{"title": ""})
	_, err := Validate(data)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "title")
}

func TestValidate_MinimalDashboard(t *testing.T) {
	out, err := Validate(minimalDashboardJSON(t))
	require.NoError(t, err)
	require.NotEmpty(t, out)

	// Verify the output is valid JSON with defaults applied.
	var result map[string]any
	require.NoError(t, json.Unmarshal(out, &result))
	assert.Equal(t, "Test", result["title"])
	assert.Equal(t, "v5", result["version"])

	// Layout should be auto-generated.
	layout, ok := result["layout"].([]any)
	require.True(t, ok)
	assert.Len(t, layout, 1)
}

func TestValidate_NilSlicesSafe(t *testing.T) {
	// The original crash: filters.items serialized as null.
	// Validate should produce clean JSON where all arrays are [] not null.
	out, err := Validate(minimalDashboardJSON(t))
	require.NoError(t, err)

	// The output should not contain "null" for array fields in builder queries.
	var result map[string]any
	require.NoError(t, json.Unmarshal(out, &result))

	widgets := result["widgets"].([]any)
	w := widgets[0].(map[string]any)
	query := w["query"].(map[string]any)
	builder := query["builder"].(map[string]any)
	queryData := builder["queryData"].([]any)
	qd := queryData[0].(map[string]any)

	// These must be [] (empty array), not null.
	if formulas, ok := builder["queryFormulas"]; ok {
		assert.NotNil(t, formulas, "queryFormulas should not be nil")
	}
	// clickhouse_sql and promql should be present as empty arrays.
	if chSQL, ok := query["clickhouse_sql"]; ok {
		assert.NotNil(t, chSQL)
	}
	if promql, ok := query["promql"]; ok {
		assert.NotNil(t, promql)
	}
	_ = qd // suppress unused
}

// --- Variable normalization ---

func TestValidate_DynamicVariableDefaults(t *testing.T) {
	data := toJSON(t, map[string]any{
		"title": "Test",
		"variables": map[string]any{
			"service_name": map[string]any{
				"type":                      "DYNAMIC",
				"dynamicVariablesAttribute": "service.name",
				"dynamicVariablesSource":    "Traces",
			},
		},
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "value", "title": "T",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "traces", "expression": "A"},
						},
					},
				},
			},
		},
	})

	out, err := Validate(data)
	require.NoError(t, err)

	var result map[string]any
	require.NoError(t, json.Unmarshal(out, &result))

	vars := result["variables"].(map[string]any)
	svcVar := vars["service_name"].(map[string]any)

	// DYNAMIC variables should default to multiSelect=true, showALLOption=true.
	assert.Equal(t, true, svcVar["multiSelect"])
	assert.Equal(t, true, svcVar["showALLOption"])
	// Sort should default to ASC for DYNAMIC.
	assert.Equal(t, "ASC", svcVar["sort"])
	// ID should be auto-generated.
	assert.NotEmpty(t, svcVar["id"])
}

func TestValidate_DynamicVariableExplicitFalse(t *testing.T) {
	// Explicitly setting multiSelect=false should be preserved (not overridden to true).
	data := toJSON(t, map[string]any{
		"title": "Test",
		"variables": map[string]any{
			"svc": map[string]any{
				"type":        "DYNAMIC",
				"multiSelect": false,
			},
		},
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "value", "title": "T",
				"query": map[string]any{
					"queryType": "builder",
					"builder":   map[string]any{"queryData": []map[string]any{{"queryName": "A", "dataSource": "traces", "expression": "A"}}},
				},
			},
		},
	})

	out, err := Validate(data)
	require.NoError(t, err)

	var result map[string]any
	require.NoError(t, json.Unmarshal(out, &result))
	svcVar := result["variables"].(map[string]any)["svc"].(map[string]any)
	assert.Equal(t, false, svcVar["multiSelect"])
}

// --- GET-shape normalization (the two API asymmetries) ---

func TestValidate_NormalizesGetShapePayload(t *testing.T) {
	// Mirrors a real round-trip: fetch via GET (lowercase dynamicVariablesSource,
	// object-form having, lowercase filter op, legacy "all sources" alias),
	// mutate nothing, send back via PUT. Without normalization this would fail
	// at panel unmarshal (strict []HavingClause), variable validation (strict
	// canonical-form enum), and panel-validator filter operator check.
	data := toJSON(t, map[string]any{
		"title": "Round-trip",
		"variables": map[string]any{
			"service_name": map[string]any{
				"type":                      "DYNAMIC",
				"dynamicVariablesAttribute": "service.name",
				"dynamicVariablesSource":    "metrics", // lowercase as returned by GET
			},
			"any_attr": map[string]any{
				"type":                      "DYNAMIC",
				"dynamicVariablesAttribute": "deployment.environment",
				"dynamicVariablesSource":    "all sources", // legacy alias from older SigNoz versions
			},
		},
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "value", "title": "Requests",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{
								"queryName":  "A",
								"dataSource": "traces",
								"expression": "A",
								"having":     map[string]any{"expression": ""}, // object form as returned by GET
								"filters": map[string]any{
									"op": "AND",
									"items": []any{
										map[string]any{
											"key":   map[string]any{"key": "service.name", "dataType": "string"},
											"op":    "in", // lowercase as returned by GET
											"value": "$service_name",
										},
									},
								},
							},
						},
						"queryFormulas": []map[string]any{
							{"queryName": "F1", "expression": "A", "having": map[string]any{"expression": ""}},
						},
					},
				},
			},
		},
	})

	out, err := Validate(data)
	require.NoError(t, err)

	var result map[string]any
	require.NoError(t, json.Unmarshal(out, &result))

	// Source normalized to Capitalized form.
	svc := result["variables"].(map[string]any)["service_name"].(map[string]any)
	assert.Equal(t, "Metrics", svc["dynamicVariablesSource"])

	// Legacy "all sources" aliased to canonical "All telemetry".
	anyAttr := result["variables"].(map[string]any)["any_attr"].(map[string]any)
	assert.Equal(t, "All telemetry", anyAttr["dynamicVariablesSource"])

	// having coerced to empty array on both queryData and queryFormulas.
	widgets := result["widgets"].([]any)
	builder := widgets[0].(map[string]any)["query"].(map[string]any)["builder"].(map[string]any)

	qd := builder["queryData"].([]any)
	qdHaving, ok := qd[0].(map[string]any)["having"].([]any)
	require.True(t, ok, "queryData[0].having must be an array, got %T", qd[0].(map[string]any)["having"])
	assert.Empty(t, qdHaving)

	// filters.items[].op normalized to uppercase.
	filters := qd[0].(map[string]any)["filters"].(map[string]any)
	items := filters["items"].([]any)
	assert.Equal(t, "IN", items[0].(map[string]any)["op"])

	qf := builder["queryFormulas"].([]any)
	qfHaving, ok := qf[0].(map[string]any)["having"].([]any)
	require.True(t, ok, "queryFormulas[0].having must be an array, got %T", qf[0].(map[string]any)["having"])
	assert.Empty(t, qfHaving)
}

func TestValidate_InvalidDynamicSourceStillRejected(t *testing.T) {
	// Normalization must not silently accept garbage — truly invalid values
	// should still produce a validation error.
	data := toJSON(t, map[string]any{
		"title": "Test",
		"variables": map[string]any{
			"svc": map[string]any{
				"type":                      "DYNAMIC",
				"dynamicVariablesAttribute": "service.name",
				"dynamicVariablesSource":    "foobar",
			},
		},
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "value", "title": "T",
				"query": map[string]any{
					"queryType": "builder",
					"builder":   map[string]any{"queryData": []map[string]any{{"queryName": "A", "dataSource": "traces", "expression": "A"}}},
				},
			},
		},
	})
	_, err := Validate(data)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "dynamicVariablesSource")
}

func TestValidate_RejectsFilterExpressionMismatch(t *testing.T) {
	// Payload with 3 items but a 2-clause expression must be rejected
	// regardless of whether SigNoz would accept it — the list view and edit
	// modal see different predicates otherwise.
	data := toJSON(t, map[string]any{
		"title": "Mismatch",
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "value", "title": "T",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{
								"queryName":  "A",
								"dataSource": "metrics",
								"expression": "A",
								"filter": map[string]any{
									"expression": "k8s.cluster.name IN $k8s.cluster.name AND k8s.node.name IN $k8s.node.name",
								},
								"filters": map[string]any{
									"op": "AND",
									"items": []any{
										map[string]any{"key": map[string]any{"key": "k8s.cluster.name", "dataType": "string", "id": "k8s.cluster.name--string--", "type": ""}, "op": "IN", "value": "$k8s.cluster.name"},
										map[string]any{"key": map[string]any{"key": "k8s.node.name", "dataType": "string", "id": "k8s.node.name--string--", "type": ""}, "op": "IN", "value": "$k8s.node.name"},
										map[string]any{"key": map[string]any{"key": "k8s.namespace.name", "dataType": "string", "id": "k8s.namespace.name--string--", "type": ""}, "op": "IN", "value": "$k8s.namespace.name"},
									},
								},
							},
						},
					},
				},
			},
		},
	})
	_, err := Validate(data)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "k8s.namespace.name")
	assert.Contains(t, err.Error(), "filter.expression")
}

func TestValidate_NormalizesMalformedFilterItem(t *testing.T) {
	// Mirrors the "CPU Used" dashboard bug: the k8s.node.name filter item
	// has a missing key.dataType, non-canonical key.id, and a value wrapped
	// in a single-element $var array. List/render tolerates this (ClickHouse
	// uses filter.expression) but the edit modal fails to hydrate. Our
	// normalizer should heal all three deviations before the payload leaves
	// Validate.
	data := toJSON(t, map[string]any{
		"title": "Fixture",
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "value", "title": "CPU Used",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{
								"queryName":  "A",
								"dataSource": "metrics",
								"expression": "A",
								"filters": map[string]any{
									"op": "AND",
									"items": []any{
										map[string]any{
											"id":  "broken-item",
											"key": map[string]any{
												"id":   "k8s.node.name",
												"key":  "k8s.node.name",
												"type": "",
											},
											"op":    "IN",
											"value": []any{"$k8s.node.name"},
										},
									},
								},
							},
						},
					},
				},
			},
		},
	})

	out, err := Validate(data)
	require.NoError(t, err)

	var result map[string]any
	require.NoError(t, json.Unmarshal(out, &result))

	item := result["widgets"].([]any)[0].(map[string]any)["query"].(map[string]any)["builder"].(map[string]any)["queryData"].([]any)[0].(map[string]any)["filters"].(map[string]any)["items"].([]any)[0].(map[string]any)
	key := item["key"].(map[string]any)

	assert.Equal(t, "string", key["dataType"], "dataType should be filled")
	assert.Equal(t, "k8s.node.name--string--", key["id"], "id should be canonicalized")
	assert.Equal(t, "$k8s.node.name", item["value"], "value should be unwrapped")
}

// --- Auto-layout ---

func TestValidate_AutoLayoutGenerated(t *testing.T) {
	// No layout provided — should be auto-computed.
	data := toJSON(t, map[string]any{
		"title": "Test",
		"widgets": []map[string]any{
			{
				"id": "a", "panelTypes": "graph", "title": "A",
				"query": map[string]any{
					"queryType": "builder",
					"builder":   map[string]any{"queryData": []map[string]any{{"queryName": "A", "dataSource": "traces", "expression": "A"}}},
				},
			},
			{
				"id": "b", "panelTypes": "graph", "title": "B",
				"query": map[string]any{
					"queryType": "builder",
					"builder":   map[string]any{"queryData": []map[string]any{{"queryName": "A", "dataSource": "traces", "expression": "A"}}},
				},
			},
		},
	})

	out, err := Validate(data)
	require.NoError(t, err)

	var result map[string]any
	require.NoError(t, json.Unmarshal(out, &result))
	layout := result["layout"].([]any)
	assert.Len(t, layout, 2, "should have 2 layout items for 2 widgets")
}

// --- Panel-type semantic validation ---

func TestValidate_ListPanelRejectsMetrics(t *testing.T) {
	data := toJSON(t, map[string]any{
		"title": "Test",
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "list", "title": "T",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "metrics", "expression": "A", "aggregateOperator": "noop"},
						},
					},
				},
			},
		},
	})

	_, err := Validate(data)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "list panel does not support")
}

func TestValidate_InvalidQueryType(t *testing.T) {
	data := toJSON(t, map[string]any{
		"title": "Test",
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "value", "title": "T",
				"query": map[string]any{
					"queryType": "invalid_type",
				},
			},
		},
	})

	_, err := Validate(data)
	require.Error(t, err)
}

func TestValidate_InvalidDataSource(t *testing.T) {
	data := toJSON(t, map[string]any{
		"title": "Test",
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "value", "title": "T",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "banana", "expression": "A"},
						},
					},
				},
			},
		},
	})

	_, err := Validate(data)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "banana")
}

// --- Formula validation ---

func TestValidate_FormulaReferencesExist(t *testing.T) {
	data := toJSON(t, map[string]any{
		"title": "Test",
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "value", "title": "Error Rate",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "traces", "expression": "A", "disabled": true},
						},
						"queryFormulas": []map[string]any{
							{"queryName": "F1", "expression": "A / Z"},
						},
					},
				},
			},
		},
	})

	_, err := Validate(data)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "Z")
}

func TestValidate_ValidErrorRateFormula(t *testing.T) {
	data := toJSON(t, map[string]any{
		"title": "Test",
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "value", "title": "Error Rate",
				"yAxisUnit": "percentunit",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "traces", "expression": "A", "disabled": true},
							{"queryName": "B", "dataSource": "traces", "expression": "B", "disabled": true},
						},
						"queryFormulas": []map[string]any{
							{"queryName": "F1", "expression": "A / (A+B)"},
						},
					},
				},
			},
		},
	})

	out, err := Validate(data)
	require.NoError(t, err)
	assert.NotEmpty(t, out)
}

// --- Full RED dashboard (integration) ---

func TestValidate_FullREDDashboard(t *testing.T) {
	data := toJSON(t, map[string]any{
		"title":       "sample-flask-service — RED Metrics",
		"description": "Rate, Errors, and Duration metrics",
		"tags":        []string{"red", "flask"},
		"variables": map[string]any{
			"service_name": map[string]any{
				"type":                      "DYNAMIC",
				"dynamicVariablesAttribute": "service.name",
				"dynamicVariablesSource":    "Traces",
				"multiSelect":               true,
				"showALLOption":             true,
			},
		},
		"layout": []map[string]any{
			{"i": "requests", "x": 0, "y": 0, "w": 4, "h": 3},
			{"i": "error_rate", "x": 4, "y": 0, "w": 4, "h": 3},
			{"i": "latency", "x": 8, "y": 0, "w": 4, "h": 3},
			{"i": "req_graph", "x": 0, "y": 3, "w": 6, "h": 7},
			{"i": "latency_graph", "x": 6, "y": 3, "w": 6, "h": 7},
		},
		"widgets": []map[string]any{
			{
				"id": "requests", "panelTypes": "value", "title": "Total Requests",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{
								"queryName": "A", "dataSource": "traces", "expression": "A",
								"aggregateOperator": "count",
								"filter":            map[string]any{"expression": "service.name IN $service_name"},
							},
						},
					},
				},
			},
			{
				"id": "error_rate", "panelTypes": "value", "title": "Error Rate",
				"yAxisUnit": "percentunit",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "traces", "expression": "A", "disabled": true, "aggregateOperator": "count"},
							{"queryName": "B", "dataSource": "traces", "expression": "B", "disabled": true, "aggregateOperator": "count"},
						},
						"queryFormulas": []map[string]any{
							{"queryName": "F1", "expression": "A / (A+B)"},
						},
					},
				},
			},
			{
				"id": "latency", "panelTypes": "value", "title": "P95 Latency",
				"yAxisUnit": "ns",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "traces", "expression": "A", "aggregateOperator": "p95"},
						},
					},
				},
			},
			{
				"id": "req_graph", "panelTypes": "graph", "title": "Request Rate",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "traces", "expression": "A", "aggregateOperator": "count"},
						},
					},
				},
			},
			{
				"id": "latency_graph", "panelTypes": "graph", "title": "Latency (P50/P95/P99)",
				"yAxisUnit": "ns",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "traces", "expression": "A", "aggregateOperator": "p50", "legend": "P50"},
							{"queryName": "B", "dataSource": "traces", "expression": "B", "aggregateOperator": "p95", "legend": "P95"},
							{"queryName": "C", "dataSource": "traces", "expression": "C", "aggregateOperator": "p99", "legend": "P99"},
						},
					},
				},
			},
		},
	})

	out, err := Validate(data)
	require.NoError(t, err)
	require.NotEmpty(t, out)

	var result map[string]any
	require.NoError(t, json.Unmarshal(out, &result))

	// Version set.
	assert.Equal(t, "v5", result["version"])

	// All 5 widgets present.
	widgets := result["widgets"].([]any)
	assert.Len(t, widgets, 5)

	// Layout matches.
	layout := result["layout"].([]any)
	assert.Len(t, layout, 5)

	// Variable normalized.
	vars := result["variables"].(map[string]any)
	svc := vars["service_name"].(map[string]any)
	assert.NotEmpty(t, svc["id"])
	assert.Equal(t, "ASC", svc["sort"])
}

// --- ValidateFromMap convenience ---

func TestValidateFromMap_Works(t *testing.T) {
	m := map[string]any{
		"title": "Test",
		"widgets": []any{
			map[string]any{
				"id": "w1", "panelTypes": "value", "title": "T",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []any{
							map[string]any{"queryName": "A", "dataSource": "traces", "expression": "A"},
						},
					},
				},
			},
		},
	}

	out, err := ValidateFromMap(m)
	require.NoError(t, err)
	assert.NotEmpty(t, out)
}

// --- Row panels pass through ---

func TestValidate_RowPanelPassesThrough(t *testing.T) {
	data := toJSON(t, map[string]any{
		"title": "Test",
		"widgets": []map[string]any{
			{"id": "row1", "panelTypes": "row", "title": "Section"},
			{
				"id": "w1", "panelTypes": "value", "title": "V",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "traces", "expression": "A"},
						},
					},
				},
			},
		},
	})

	out, err := Validate(data)
	require.NoError(t, err)

	var result map[string]any
	require.NoError(t, json.Unmarshal(out, &result))
	widgets := result["widgets"].([]any)
	assert.Len(t, widgets, 2)
}

// --- Threshold round-trip preservation ---

func TestValidate_ThresholdFieldsPreserved(t *testing.T) {
	data := toJSON(t, map[string]any{
		"title": "Test",
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "graph", "title": "Latency",
				"thresholds": []map[string]any{
					{
						"index":                 "0",
						"isEditEnabled":         true,
						"keyIndex":              0,
						"selectedGraph":         "A",
						"thresholdColor":        "#FF0000",
						"thresholdFormat":       "Text",
						"thresholdLabel":        "SLO Breach",
						"thresholdOperator":     ">",
						"thresholdTableOptions": "1",
						"thresholdUnit":         "ms",
						"thresholdValue":        500,
					},
				},
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "traces", "expression": "A"},
						},
					},
				},
			},
		},
	})

	out, err := Validate(data)
	require.NoError(t, err)

	var result map[string]any
	require.NoError(t, json.Unmarshal(out, &result))

	widgets := result["widgets"].([]any)
	w := widgets[0].(map[string]any)
	thresholds := w["thresholds"].([]any)
	require.Len(t, thresholds, 1)

	th := thresholds[0].(map[string]any)
	assert.Equal(t, "0", th["index"])
	assert.Equal(t, true, th["isEditEnabled"])
	assert.Equal(t, "A", th["selectedGraph"])
	assert.Equal(t, "#FF0000", th["thresholdColor"])
	assert.Equal(t, "Text", th["thresholdFormat"])
	assert.Equal(t, "SLO Breach", th["thresholdLabel"])
	assert.Equal(t, ">", th["thresholdOperator"])
	assert.Equal(t, "1", th["thresholdTableOptions"])
	assert.Equal(t, "ms", th["thresholdUnit"])
	assert.Equal(t, float64(500), th["thresholdValue"])
}

// --- DecimalPrecision string variant ---

func TestValidate_DecimalPrecisionStringAccepted(t *testing.T) {
	data := toJSON(t, map[string]any{
		"title": "Test",
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "value", "title": "V",
				"decimalPrecision": "full precision",
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "traces", "expression": "A"},
						},
					},
				},
			},
		},
	})

	out, err := Validate(data)
	require.NoError(t, err)

	var result map[string]any
	require.NoError(t, json.Unmarshal(out, &result))

	widgets := result["widgets"].([]any)
	w := widgets[0].(map[string]any)
	assert.Equal(t, "full precision", w["decimalPrecision"])
}

func TestValidate_DecimalPrecisionIntAccepted(t *testing.T) {
	data := toJSON(t, map[string]any{
		"title": "Test",
		"widgets": []map[string]any{
			{
				"id": "w1", "panelTypes": "value", "title": "V",
				"decimalPrecision": 2,
				"query": map[string]any{
					"queryType": "builder",
					"builder": map[string]any{
						"queryData": []map[string]any{
							{"queryName": "A", "dataSource": "traces", "expression": "A"},
						},
					},
				},
			},
		},
	})

	out, err := Validate(data)
	require.NoError(t, err)

	var result map[string]any
	require.NoError(t, json.Unmarshal(out, &result))

	widgets := result["widgets"].([]any)
	w := widgets[0].(map[string]any)
	assert.Equal(t, float64(2), w["decimalPrecision"])
}
