package schema

import (
	"fmt"
	"strings"
	"testing"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// ---------------------------------------------------------------------------
// ResolveRollupSQLType — precision parity tests
// ---------------------------------------------------------------------------

func TestResolveRollupSQLType_COUNT_ReturnsDecimal(t *testing.T) {
	got, err := ResolveRollupSQLType("COUNT", "")
	if err != nil {
		t.Fatalf("ResolveRollupSQLType(COUNT) returned error: %v", err)
	}
	if got != "DECIMAL(65,30)" {
		t.Fatalf("ResolveRollupSQLType(COUNT) = %q, want DECIMAL(65,30)", got)
	}
}

func TestResolveRollupSQLType_SUM_ReturnsDecimal(t *testing.T) {
	got, err := ResolveRollupSQLType("SUM", "DECIMAL(18,2)")
	if err != nil {
		t.Fatalf("ResolveRollupSQLType(SUM) returned error: %v", err)
	}
	if got != "DECIMAL(65,30)" {
		t.Fatalf("ResolveRollupSQLType(SUM) = %q, want DECIMAL(65,30)", got)
	}
}

func TestResolveRollupSQLType_SUM_NonNumericRejected(t *testing.T) {
	_, err := ResolveRollupSQLType("SUM", "TEXT")
	if err == nil {
		t.Fatal("ResolveRollupSQLType(SUM, TEXT) returned nil error, want rejection")
	}
}

func TestResolveRollupSQLType_AVG_ReturnsDecimal(t *testing.T) {
	got, err := ResolveRollupSQLType("AVG", "INT")
	if err != nil {
		t.Fatalf("ResolveRollupSQLType(AVG) returned error: %v", err)
	}
	if got != "DECIMAL(65,30)" {
		t.Fatalf("ResolveRollupSQLType(AVG) = %q, want DECIMAL(65,30)", got)
	}
}

func TestResolveRollupSQLType_MIN_Passthrough(t *testing.T) {
	got, err := ResolveRollupSQLType("MIN", "DECIMAL(18,2)")
	if err != nil {
		t.Fatalf("ResolveRollupSQLType(MIN) returned error: %v", err)
	}
	if got != "DECIMAL(18,2)" {
		t.Fatalf("ResolveRollupSQLType(MIN) = %q, want DECIMAL(18,2)", got)
	}
}

func TestResolveRollupSQLType_MAX_Passthrough(t *testing.T) {
	got, err := ResolveRollupSQLType("MAX", "VARCHAR(255)")
	if err != nil {
		t.Fatalf("ResolveRollupSQLType(MAX) returned error: %v", err)
	}
	if got != "VARCHAR(255)" {
		t.Fatalf("ResolveRollupSQLType(MAX) = %q, want VARCHAR(255)", got)
	}
}

func TestResolveRollupSQLType_MIN_EmptySource_Rejected(t *testing.T) {
	_, err := ResolveRollupSQLType("MIN", "")
	if err == nil {
		t.Fatal("ResolveRollupSQLType(MIN, empty) returned nil error, want rejection")
	}
}

func TestResolveRollupSQLType_UnsupportedCalcType_Rejected(t *testing.T) {
	_, err := ResolveRollupSQLType("MEDIAN", "INT")
	if err == nil {
		t.Fatal("ResolveRollupSQLType(MEDIAN) returned nil error, want rejection")
	}
}

func TestResolveRollupSQLType_LowercaseCalcType_Accepted(t *testing.T) {
	got, err := ResolveRollupSQLType("sum", "INT")
	if err != nil {
		t.Fatalf("ResolveRollupSQLType(sum) returned error: %v", err)
	}
	if got != "DECIMAL(65,30)" {
		t.Fatalf("ResolveRollupSQLType(sum) = %q, want DECIMAL(65,30)", got)
	}
}

func TestResolveSchemaFieldSQLType_FormulaBackedScalarUsesReturnType(t *testing.T) {
	formula := "Amount__c * 2"
	returnType := constants.FieldTypeDouble
	field := &models.FieldMetadata{
		APIName:    "ComputedAmount__c",
		Type:       constants.FieldTypeString,
		Formula:    &formula,
		ReturnType: &returnType,
	}
	mapper := func(fieldType string) string {
		switch fieldType {
		case "double":
			return "DOUBLE"
		case "string":
			return "TEXT"
		default:
			return "INVALID_TYPE_ERROR"
		}
	}
	got, err := ResolveSchemaFieldSQLType(field, mapper)
	if err != nil {
		t.Fatalf("ResolveSchemaFieldSQLType returned error: %v", err)
	}
	if got != "DOUBLE" {
		t.Fatalf("ResolveSchemaFieldSQLType = %q, want DOUBLE", got)
	}
}

// ---------------------------------------------------------------------------
// CanonicalizeRollupConfig — canonical persistence tests
// ---------------------------------------------------------------------------

func TestCanonicalizeRollupConfig_UppercasesCalcType(t *testing.T) {
	cfg := models.RollupConfig{
		CalcType:          "sum",
		SummaryObject:     "Child",
		SummaryField:      "Amount",
		RelationshipField: "ParentId",
	}
	result := CanonicalizeRollupConfig(cfg, nil)
	if result.CalcType != "SUM" {
		t.Fatalf("CalcType = %q, want SUM", result.CalcType)
	}
}

func TestCanonicalizeRollupConfig_TrimsWhitespace(t *testing.T) {
	cfg := models.RollupConfig{
		CalcType:          "  SUM  ",
		SummaryObject:     "  Child  ",
		SummaryField:      "  Amount  ",
		RelationshipField: "  ParentId  ",
	}
	result := CanonicalizeRollupConfig(cfg, nil)
	if result.SummaryObject != "Child" {
		t.Fatalf("SummaryObject = %q, want Child", result.SummaryObject)
	}
	if result.SummaryField != "Amount" {
		t.Fatalf("SummaryField = %q, want Amount", result.SummaryField)
	}
	if result.RelationshipField != "ParentId" {
		t.Fatalf("RelationshipField = %q, want ParentId", result.RelationshipField)
	}
}

func TestCanonicalizeRollupConfig_SchemaLookupCanonicalizesNames(t *testing.T) {
	cfg := models.RollupConfig{
		CalcType:          "SUM",
		SummaryObject:     "  child__c  ",  // lowercase + whitespace
		SummaryField:      "  amount__c  ", // lowercase + whitespace
		RelationshipField: "  parentid__c  ",
	}
	lookup := func(objectAPIName string) (*models.ObjectMetadata, error) {
		if strings.EqualFold(strings.TrimSpace(objectAPIName), "child__c") {
			return &models.ObjectMetadata{
				APIName: "Child__c",
				Fields: []models.FieldMetadata{
					{APIName: "Amount__c", Type: constants.FieldTypeCurrency},
					{APIName: "ParentId__c"},
				},
			}, nil
		}
		return nil, fmt.Errorf("not found")
	}
	result := CanonicalizeRollupConfig(cfg, lookup)
	if result.SummaryObject != "Child__c" {
		t.Fatalf("SummaryObject = %q, want Child__c (canonical from schema)", result.SummaryObject)
	}
	if result.SummaryField != "Amount__c" {
		t.Fatalf("SummaryField = %q, want Amount__c (canonical from schema)", result.SummaryField)
	}
	if result.RelationshipField != "ParentId__c" {
		t.Fatalf("RelationshipField = %q, want ParentId__c (canonical from schema)", result.RelationshipField)
	}
}

func TestCanonicalizeRollupConfig_COUNT_SetsSummaryFieldToStar(t *testing.T) {
	cfg := models.RollupConfig{
		CalcType:          "COUNT",
		SummaryObject:     "Child",
		SummaryField:      "IgnoredField",
		RelationshipField: "ParentId",
	}
	result := CanonicalizeRollupConfig(cfg, nil)
	if result.SummaryField != "*" {
		t.Fatalf("COUNT SummaryField = %q, want *", result.SummaryField)
	}
}

func TestCanonicalizeRollupConfig_LookupFails_KeepsTrimmedRaw(t *testing.T) {
	cfg := models.RollupConfig{
		CalcType:          "SUM",
		SummaryObject:     "  nonexistent  ",
		SummaryField:      "  Amount  ",
		RelationshipField: "  ParentId  ",
	}
	lookup := func(objectAPIName string) (*models.ObjectMetadata, error) {
		return nil, fmt.Errorf("not found")
	}
	result := CanonicalizeRollupConfig(cfg, lookup)
	// Lookup failed — trimmed raw value kept.
	if result.SummaryObject != "nonexistent" {
		t.Fatalf("SummaryObject = %q, want nonexistent (trimmed raw)", result.SummaryObject)
	}
}

func TestCanonicalizeRollupConfig_PreservesFilter(t *testing.T) {
	filter := "Amount > 0"
	cfg := models.RollupConfig{
		CalcType:          "SUM",
		SummaryObject:     "Child",
		SummaryField:      "Amount",
		RelationshipField: "ParentId",
		Filter:            &filter,
	}
	result := CanonicalizeRollupConfig(cfg, nil)
	if result.Filter == nil || *result.Filter != filter {
		t.Fatalf("Filter = %#v, want %q", result.Filter, filter)
	}
}

// ---------------------------------------------------------------------------
// ValidateRollupGraph — cycle/depth rejection tests
// ---------------------------------------------------------------------------

func TestValidateRollupGraph_NoCycles_Passes(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{APIName: "Parent", Fields: []models.FieldMetadata{
			{APIName: "Total", Type: constants.FieldTypeRollupSummary, RollupConfig: &models.RollupConfig{
				CalcType: "SUM", SummaryObject: "Child", SummaryField: "Amount", RelationshipField: "ParentId",
			}},
		}},
		{APIName: "Child"},
	}
	if err := ValidateRollupGraph(schemas, "Parent"); err != nil {
		t.Errorf("expected no error for acyclic graph, got: %v", err)
	}
}

func TestValidateRollupGraph_SelfLoop_Rejected(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{APIName: "ObjectA", Fields: []models.FieldMetadata{
			{APIName: "Total", Type: constants.FieldTypeRollupSummary, RollupConfig: &models.RollupConfig{
				CalcType: "SUM", SummaryObject: "ObjectA", SummaryField: "Amount", RelationshipField: "ParentId",
			}},
		}},
	}
	err := ValidateRollupGraph(schemas, "ObjectA")
	if err == nil {
		t.Fatal("expected self-loop cycle error, got nil")
	}
	if !strings.Contains(strings.ToLower(err.Error()), "cycle") {
		t.Errorf("error must contain 'cycle', got: %q", err.Error())
	}
}

func TestValidateRollupGraph_TwoCycle_Rejected(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{APIName: "ObjectA", Fields: []models.FieldMetadata{
			{APIName: "F1", Type: constants.FieldTypeRollupSummary, RollupConfig: &models.RollupConfig{
				CalcType: "SUM", SummaryObject: "ObjectB", SummaryField: "Amount", RelationshipField: "ObjectAId",
			}},
		}},
		{APIName: "ObjectB", Fields: []models.FieldMetadata{
			{APIName: "F2", Type: constants.FieldTypeRollupSummary, RollupConfig: &models.RollupConfig{
				CalcType: "SUM", SummaryObject: "ObjectA", SummaryField: "Total", RelationshipField: "ObjectBId",
			}},
		}},
	}
	err := ValidateRollupGraph(schemas, "ObjectA")
	if err == nil {
		t.Fatal("expected 2-cycle error, got nil")
	}
	if !strings.Contains(strings.ToLower(err.Error()), "cycle") {
		t.Errorf("error must contain 'cycle', got: %q", err.Error())
	}
}

func TestValidateRollupGraph_MixedCaseSelfLoop_Rejected(t *testing.T) {
	// "  objecta  " canonicalizes to "objecta" == "ObjectA" lowered → self-loop.
	schemas := []*models.ObjectMetadata{
		{APIName: "ObjectA", Fields: []models.FieldMetadata{
			{APIName: "Total", Type: constants.FieldTypeRollupSummary, RollupConfig: &models.RollupConfig{
				CalcType: "SUM", SummaryObject: "  OBJECTA  ", SummaryField: "Amount", RelationshipField: "ParentId",
			}},
		}},
	}
	err := ValidateRollupGraph(schemas, "ObjectA")
	if err == nil {
		t.Fatal("expected mixed-case self-loop cycle error, got nil")
	}
	if !strings.Contains(strings.ToLower(err.Error()), "cycle") {
		t.Errorf("error must contain 'cycle', got: %q", err.Error())
	}
}

func TestValidateRollupGraph_MaxDepthExceeded_Rejected(t *testing.T) {
	// Build a linear chain: L0→L1→...→L8 (9 objects, 8 edges).
	// MaxRollupDepth = 8, so depth 8 is over the cap.
	schemas := make([]*models.ObjectMetadata, 9)
	for i := 0; i < 9; i++ {
		name := fmt.Sprintf("Level%d", i)
		obj := &models.ObjectMetadata{APIName: name}
		if i < 8 {
			childName := fmt.Sprintf("Level%d", i+1)
			obj.Fields = []models.FieldMetadata{
				{APIName: fmt.Sprintf("F%d", i), Type: constants.FieldTypeRollupSummary,
					RollupConfig: &models.RollupConfig{
						CalcType: "SUM", SummaryObject: childName,
						SummaryField:      fmt.Sprintf("F%d", i+1),
						RelationshipField: fmt.Sprintf("Level%dId", i),
					}},
			}
		}
		schemas[i] = obj
	}
	err := ValidateRollupGraph(schemas, "Level0")
	if err == nil {
		t.Fatal("expected depth-exceeded error, got nil")
	}
	msg := err.Error()
	if strings.Contains(strings.ToLower(msg), "cycle detected") {
		t.Errorf("error should be depth-exceeded, not cycle-detected; got: %q", msg)
	}
	if !strings.Contains(strings.ToLower(msg), "depth") {
		t.Errorf("error must contain 'depth', got: %q", msg)
	}
}

func TestValidateRollupGraph_MaxDepthExactlyAtCap_Passes(t *testing.T) {
	// 8 objects → 7 edges → depth 7, which is < MaxRollupDepth (8) → should pass.
	schemas := make([]*models.ObjectMetadata, 8)
	for i := 0; i < 8; i++ {
		name := fmt.Sprintf("Cap%d", i)
		obj := &models.ObjectMetadata{APIName: name}
		if i < 7 {
			childName := fmt.Sprintf("Cap%d", i+1)
			obj.Fields = []models.FieldMetadata{
				{APIName: fmt.Sprintf("F%d", i), Type: constants.FieldTypeRollupSummary,
					RollupConfig: &models.RollupConfig{
						CalcType: "SUM", SummaryObject: childName,
						SummaryField:      fmt.Sprintf("F%d", i+1),
						RelationshipField: fmt.Sprintf("Cap%dId", i),
					}},
			}
		}
		schemas[i] = obj
	}
	if err := ValidateRollupGraph(schemas, "Cap0"); err != nil {
		t.Errorf("expected no error for 7-edge chain (at cap), got: %v", err)
	}
}

func TestValidateRollupGraph_EmptySchemas_Passes(t *testing.T) {
	if err := ValidateRollupGraph([]*models.ObjectMetadata{}, "Anything"); err != nil {
		t.Errorf("expected nil for empty schemas, got: %v", err)
	}
}

func TestValidateRollupGraph_NoRollupFields_Passes(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{APIName: "ObjectA", Fields: []models.FieldMetadata{{APIName: "Name", Type: "string"}}},
		{APIName: "ObjectB", Fields: []models.FieldMetadata{{APIName: "Amount", Type: "currency"}}},
	}
	if err := ValidateRollupGraph(schemas, "ObjectA"); err != nil {
		t.Errorf("expected nil for schemas with no rollup fields, got: %v", err)
	}
}

// ---------------------------------------------------------------------------
// IsRollupSummaryType
// ---------------------------------------------------------------------------

func TestIsRollupSummaryType(t *testing.T) {
	if !IsRollupSummaryType(constants.FieldTypeRollupSummary) {
		t.Error("IsRollupSummaryType(rollupsummary) should be true")
	}
	if IsRollupSummaryType(constants.FieldTypeString) {
		t.Error("IsRollupSummaryType(string) should be false")
	}
}

// ---------------------------------------------------------------------------
// ResolveRollup — single-fact contract tests
// ---------------------------------------------------------------------------

func TestResolveRollup_SUM_ReturnsCanonicalConfigAndDecimal(t *testing.T) {
	cfg := models.RollupConfig{
		CalcType:          "  sum  ",
		SummaryObject:     "  child__c  ",
		SummaryField:      "  amount__c  ",
		RelationshipField: "  parentid__c  ",
	}
	lookup := func(objectAPIName string) (*models.ObjectMetadata, error) {
		if strings.EqualFold(strings.TrimSpace(objectAPIName), "child__c") {
			return &models.ObjectMetadata{
				APIName: "Child__c",
				Fields: []models.FieldMetadata{
					{APIName: "Amount__c", Type: constants.FieldTypeCurrency},
					{APIName: "ParentId__c", ReferenceTo: []string{"Parent__c"}},
				},
			}, nil
		}
		return nil, fmt.Errorf("not found")
	}
	resolved, err := ResolveRollup("Parent__c", cfg, lookup, "DECIMAL(18,2)")
	if err != nil {
		t.Fatalf("ResolveRollup returned error: %v", err)
	}
	// SQL type must be DECIMAL(65,30) for SUM.
	if resolved.SQLType != "DECIMAL(65,30)" {
		t.Errorf("SQLType = %q, want DECIMAL(65,30)", resolved.SQLType)
	}
	// Canonical config must have normalized names from schema lookup.
	if resolved.CanonicalConfig.CalcType != "SUM" {
		t.Errorf("CanonicalConfig.CalcType = %q, want SUM", resolved.CanonicalConfig.CalcType)
	}
	if resolved.CanonicalConfig.SummaryObject != "Child__c" {
		t.Errorf("CanonicalConfig.SummaryObject = %q, want Child__c", resolved.CanonicalConfig.SummaryObject)
	}
	if resolved.CanonicalConfig.SummaryField != "Amount__c" {
		t.Errorf("CanonicalConfig.SummaryField = %q, want Amount__c", resolved.CanonicalConfig.SummaryField)
	}
	if resolved.CanonicalConfig.RelationshipField != "ParentId__c" {
		t.Errorf("CanonicalConfig.RelationshipField = %q, want ParentId__c", resolved.CanonicalConfig.RelationshipField)
	}
	if resolved.Result.Type != constants.FieldTypeCurrency || resolved.Result.Precision == nil || *resolved.Result.Precision != 18 || resolved.Result.Scale == nil || *resolved.Result.Scale != 2 {
		t.Fatalf("Result = %+v, want currency(18,2)", resolved.Result)
	}
	if resolved.CanonicalConfig.ResultType == nil || *resolved.CanonicalConfig.ResultType != constants.FieldTypeCurrency || resolved.CanonicalConfig.ResultPrecision == nil || *resolved.CanonicalConfig.ResultPrecision != 18 || resolved.CanonicalConfig.ResultScale == nil || *resolved.CanonicalConfig.ResultScale != 2 {
		t.Fatalf("CanonicalConfig result facets = %+v, want currency(18,2)", resolved.CanonicalConfig)
	}
}

func TestResolveRollup_MIN_PassthroughSourceType(t *testing.T) {
	cfg := models.RollupConfig{
		CalcType:          "MIN",
		SummaryObject:     "Child",
		SummaryField:      "Amount",
		RelationshipField: "ParentId",
	}
	lookup := func(objectAPIName string) (*models.ObjectMetadata, error) {
		if strings.EqualFold(strings.TrimSpace(objectAPIName), "child") {
			return &models.ObjectMetadata{
				APIName: "Child",
				Fields: []models.FieldMetadata{
					{APIName: "Amount", Type: constants.FieldTypeDate},
					{APIName: "ParentId", ReferenceTo: []string{"Parent"}},
				},
			}, nil
		}
		return nil, fmt.Errorf("not found")
	}
	resolved, err := ResolveRollup("Parent", cfg, lookup, "DECIMAL(18,2)")
	if err != nil {
		t.Fatalf("ResolveRollup(MIN) returned error: %v", err)
	}
	if resolved.SQLType != "DECIMAL(18,2)" {
		t.Errorf("SQLType = %q, want DECIMAL(18,2) (MIN passthrough)", resolved.SQLType)
	}
	if resolved.CanonicalConfig.CalcType != "MIN" {
		t.Errorf("CanonicalConfig.CalcType = %q, want MIN", resolved.CanonicalConfig.CalcType)
	}
	if resolved.Result.Type != constants.FieldTypeDate || resolved.Result.Precision != nil || resolved.Result.Scale != nil {
		t.Fatalf("Result = %+v, want date without numeric facets", resolved.Result)
	}
}

func TestResolveRollup_COUNT_NoSourceFieldNeeded(t *testing.T) {
	cfg := models.RollupConfig{
		CalcType:          "COUNT",
		SummaryObject:     "Child",
		SummaryField:      "Ignored",
		RelationshipField: "ParentId",
	}
	lookup := func(objectAPIName string) (*models.ObjectMetadata, error) {
		if strings.EqualFold(strings.TrimSpace(objectAPIName), "child") {
			return &models.ObjectMetadata{
				APIName: "Child",
				Fields: []models.FieldMetadata{
					{APIName: "ParentId", ReferenceTo: []string{"Parent"}},
				},
			}, nil
		}
		return nil, fmt.Errorf("not found")
	}
	resolved, err := ResolveRollup("Parent", cfg, lookup, "")
	if err != nil {
		t.Fatalf("ResolveRollup(COUNT) returned error: %v", err)
	}
	if resolved.SQLType != "DECIMAL(65,30)" {
		t.Errorf("SQLType = %q, want DECIMAL(65,30)", resolved.SQLType)
	}
	if resolved.CanonicalConfig.SummaryField != "*" {
		t.Errorf("CanonicalConfig.SummaryField = %q, want * (COUNT always uses *)", resolved.CanonicalConfig.SummaryField)
	}
	if resolved.Result.Type != constants.FieldTypeInt || resolved.Result.Precision == nil || *resolved.Result.Precision != 18 || resolved.Result.Scale == nil || *resolved.Result.Scale != 0 {
		t.Fatalf("Result = %+v, want int(18,0)", resolved.Result)
	}
}

func TestResolveRollup_UnsupportedCalcType_Rejected(t *testing.T) {
	cfg := models.RollupConfig{
		CalcType:          "MEDIAN",
		SummaryObject:     "Child",
		SummaryField:      "Amount",
		RelationshipField: "ParentId",
	}
	lookup := func(objectAPIName string) (*models.ObjectMetadata, error) {
		if strings.EqualFold(strings.TrimSpace(objectAPIName), "child") {
			return &models.ObjectMetadata{
				APIName: "Child",
				Fields: []models.FieldMetadata{
					{APIName: "Amount"},
					{APIName: "ParentId", ReferenceTo: []string{"Parent"}},
				},
			}, nil
		}
		return nil, fmt.Errorf("not found")
	}
	_, err := ResolveRollup("Parent", cfg, lookup, "INT")
	if err == nil {
		t.Fatal("ResolveRollup(MEDIAN) returned nil error, want rejection")
	}
}

func TestResolveRollup_RelationshipFieldWrongTarget_Rejected(t *testing.T) {
	cfg := models.RollupConfig{
		CalcType:          "SUM",
		SummaryObject:     "Child",
		SummaryField:      "Amount",
		RelationshipField: "ProjectId",
	}
	lookup := func(objectAPIName string) (*models.ObjectMetadata, error) {
		if strings.EqualFold(strings.TrimSpace(objectAPIName), "child") {
			return &models.ObjectMetadata{
				APIName: "Child",
				Fields: []models.FieldMetadata{
					{APIName: "Amount"},
					{APIName: "ProjectId", ReferenceTo: []string{"Project"}},
				},
			}, nil
		}
		return nil, fmt.Errorf("not found")
	}
	_, err := ResolveRollup("Parent", cfg, lookup, "DECIMAL(18,2)")
	if err == nil {
		t.Fatal("ResolveRollup with wrong relationship target returned nil error, want rejection")
	}
	if !strings.Contains(err.Error(), "relationshipField") {
		t.Fatalf("error = %v, want relationshipField diagnostic", err)
	}
}

// TestResolveRollup_AVG_ResolvesToDecimal verifies that AVG resolves to
// DECIMAL(65,30) — precision parity with COUNT/SUM.
func TestResolveRollup_AVG_ResolvesToDecimal(t *testing.T) {
	cfg := models.RollupConfig{
		CalcType:          "AVG",
		SummaryObject:     "Child__c",
		SummaryField:      "Amount__c",
		RelationshipField: "ParentId",
	}
	lookup := func(objectAPIName string) (*models.ObjectMetadata, error) {
		if strings.EqualFold(strings.TrimSpace(objectAPIName), "child__c") {
			return &models.ObjectMetadata{
				APIName: "Child__c",
				Fields: []models.FieldMetadata{
					{APIName: "Amount__c"},
					{APIName: "ParentId", ReferenceTo: []string{"Parent__c"}},
				},
			}, nil
		}
		return nil, fmt.Errorf("not found")
	}
	resolved, err := ResolveRollup("Parent__c", cfg, lookup, "DECIMAL(18,2)")
	if err != nil {
		t.Fatalf("ResolveRollup AVG: %v", err)
	}
	if resolved.SQLType != "DECIMAL(65,30)" {
		t.Errorf("AVG SQLType = %q, want DECIMAL(65,30)", resolved.SQLType)
	}
	if resolved.CanonicalConfig.CalcType != "AVG" {
		t.Errorf("CanonicalConfig.CalcType = %q, want AVG", resolved.CanonicalConfig.CalcType)
	}
}

func TestResolveRollup_AVG_NestedCountPublishesDecimalResult(t *testing.T) {
	cfg := models.RollupConfig{
		CalcType:          "AVG",
		SummaryObject:     "Child__c",
		SummaryField:      "ChildCount__c",
		RelationshipField: "ParentId",
	}
	lookup := func(objectAPIName string) (*models.ObjectMetadata, error) {
		if strings.EqualFold(strings.TrimSpace(objectAPIName), "child__c") {
			return &models.ObjectMetadata{
				APIName: "Child__c",
				Fields: []models.FieldMetadata{
					{APIName: "ChildCount__c", Type: constants.FieldTypeRollupSummary, RollupConfig: &models.RollupConfig{CalcType: "COUNT"}},
					{APIName: "ParentId", ReferenceTo: []string{"Parent__c"}},
				},
			}, nil
		}
		return nil, fmt.Errorf("not found")
	}

	resolved, err := ResolveRollup("Parent__c", cfg, lookup, "DECIMAL(65,30)")
	if err != nil {
		t.Fatalf("ResolveRollup AVG: %v", err)
	}
	if resolved.Result.Type != constants.FieldTypeDouble || resolved.Result.Precision == nil || *resolved.Result.Precision != 65 || resolved.Result.Scale == nil || *resolved.Result.Scale != 30 {
		t.Fatalf("AVG result = %+v, want double(65,30)", resolved.Result)
	}
	if resolved.CanonicalConfig.ResultType == nil || *resolved.CanonicalConfig.ResultType != constants.FieldTypeDouble || resolved.CanonicalConfig.ResultPrecision == nil || *resolved.CanonicalConfig.ResultPrecision != 65 || resolved.CanonicalConfig.ResultScale == nil || *resolved.CanonicalConfig.ResultScale != 30 {
		t.Fatalf("AVG result facets = %+v, want double(65,30)", resolved.CanonicalConfig)
	}
}

func TestResolveRollup_FormulaBackedScalarPublishesReturnType(t *testing.T) {
	formula := "Amount__c * 2"
	returnType := constants.FieldTypeDouble
	lookup := func(objectAPIName string) (*models.ObjectMetadata, error) {
		if strings.EqualFold(strings.TrimSpace(objectAPIName), "child__c") {
			return &models.ObjectMetadata{
				APIName: "Child__c",
				Fields: []models.FieldMetadata{
					{
						APIName:    "ComputedAmount__c",
						Type:       constants.FieldTypeString,
						Formula:    &formula,
						ReturnType: &returnType,
					},
					{APIName: "ParentId", ReferenceTo: []string{"Parent__c"}},
				},
			}, nil
		}
		return nil, fmt.Errorf("not found")
	}

	resolved, err := ResolveRollup("Parent__c", models.RollupConfig{
		CalcType:          "SUM",
		SummaryObject:     "Child__c",
		SummaryField:      "ComputedAmount__c",
		RelationshipField: "ParentId",
	}, lookup, "DOUBLE")
	if err != nil {
		t.Fatalf("ResolveRollup SUM: %v", err)
	}
	if resolved.Result.Type != constants.FieldTypeDouble || resolved.Result.Precision == nil || *resolved.Result.Precision != 18 || resolved.Result.Scale == nil || *resolved.Result.Scale != 0 {
		t.Fatalf("SUM result = %+v, want double(18,0)", resolved.Result)
	}
	if resolved.CanonicalConfig.ResultType == nil || *resolved.CanonicalConfig.ResultType != constants.FieldTypeDouble || resolved.CanonicalConfig.ResultPrecision == nil || *resolved.CanonicalConfig.ResultPrecision != 18 || resolved.CanonicalConfig.ResultScale == nil || *resolved.CanonicalConfig.ResultScale != 0 {
		t.Fatalf("SUM result facets = %+v, want double(18,0)", resolved.CanonicalConfig)
	}
}

func TestResolveRollup_ParentOverIntegerAveragePreservesDecimalResult(t *testing.T) {
	lookup := func(objectAPIName string) (*models.ObjectMetadata, error) {
		switch strings.ToLower(strings.TrimSpace(objectAPIName)) {
		case "child__c":
			return &models.ObjectMetadata{
				APIName: "Child__c",
				Fields: []models.FieldMetadata{
					{APIName: "AverageQuantity__c", Type: constants.FieldTypeRollupSummary, RollupConfig: &models.RollupConfig{
						CalcType:          "AVG",
						SummaryObject:     "Grandchild__c",
						SummaryField:      "Quantity__c",
						RelationshipField: "ChildId",
					}},
					{APIName: "ParentId", ReferenceTo: []string{"Parent__c"}},
				},
			}, nil
		case "grandchild__c":
			return &models.ObjectMetadata{
				APIName: "Grandchild__c",
				Fields: []models.FieldMetadata{
					{APIName: "Quantity__c", Type: constants.FieldTypeInt},
					{APIName: "ChildId", ReferenceTo: []string{"Child__c"}},
				},
			}, nil
		default:
			return nil, fmt.Errorf("not found")
		}
	}

	for _, calcType := range []string{"SUM", "MIN", "MAX"} {
		t.Run(calcType, func(t *testing.T) {
			resolved, err := ResolveRollup("Parent__c", models.RollupConfig{
				CalcType:          calcType,
				SummaryObject:     "Child__c",
				SummaryField:      "AverageQuantity__c",
				RelationshipField: "ParentId",
			}, lookup, "DECIMAL(65,30)")
			if err != nil {
				t.Fatalf("ResolveRollup %s: %v", calcType, err)
			}
			if resolved.Result.Type != constants.FieldTypeDouble || resolved.Result.Precision == nil || *resolved.Result.Precision != 65 || resolved.Result.Scale == nil || *resolved.Result.Scale != 30 {
				t.Fatalf("%s result = %+v, want double(65,30)", calcType, resolved.Result)
			}
			if resolved.CanonicalConfig.ResultType == nil || *resolved.CanonicalConfig.ResultType != constants.FieldTypeDouble || resolved.CanonicalConfig.ResultPrecision == nil || *resolved.CanonicalConfig.ResultPrecision != 65 || resolved.CanonicalConfig.ResultScale == nil || *resolved.CanonicalConfig.ResultScale != 30 {
				t.Fatalf("%s result facets = %+v, want double(65,30)", calcType, resolved.CanonicalConfig)
			}
		})
	}
}
