package schemautil

import (
	"testing"
	"time"

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

func normalizedMultiPicklistString(t *testing.T, value interface{}) string {
	t.Helper()
	switch typed := value.(type) {
	case string:
		return typed
	case NormalizedMultiPicklistJSON:
		return string(typed)
	default:
		t.Fatalf("unexpected multipicklist normalized value type %T", value)
		return ""
	}
}

func TestNormalizeSObjectMultiPicklistJSONStringIsIdempotent(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{
				APIName: "Existing_Database_Deployment__c",
				Type:    constants.FieldTypeMultiPicklist,
				Options: []models.PicklistOption{
					{Value: "Others", Label: "Others"},
				},
			},
		},
	}

	input := models.SObject{
		"Existing_Database_Deployment__c": "[\"Others\"]",
	}

	normalized := NormalizeSObject(schema, input)
	got := normalizedMultiPicklistString(t, normalized["Existing_Database_Deployment__c"])
	if got != "[\"Others\"]" {
		t.Fatalf("NormalizeSObject(JSON array string) = %q, want %q", got, "[\"Others\"]")
	}
}

func TestNormalizeSObjectMultiPicklistDoubleCallPreservesSingleEncoding(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{
				APIName: "Existing_Database_Deployment__c",
				Type:    constants.FieldTypeMultiPicklist,
				Options: []models.PicklistOption{
					{Value: "Others", Label: "Others"},
				},
			},
		},
	}

	first := NormalizeSObject(schema, models.SObject{
		"Existing_Database_Deployment__c": "Others",
	})
	second := NormalizeSObject(schema, first)

	got := normalizedMultiPicklistString(t, second["Existing_Database_Deployment__c"])
	if got != "[\"Others\"]" {
		t.Fatalf("second NormalizeSObject result = %q, want %q", got, "[\"Others\"]")
	}
}

func TestNormalizeSObjectMultiPicklistJSONStringLikeOptionRemainsSingleValue(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{
				APIName: "Existing_Database_Deployment__c",
				Type:    constants.FieldTypeMultiPicklist,
				Options: []models.PicklistOption{
					{Value: `["Others"]`, Label: `["Others"]`},
					{Value: "Others", Label: "Others"},
				},
			},
		},
	}

	normalized := NormalizeSObject(schema, models.SObject{
		"Existing_Database_Deployment__c": `["Others"]`,
	})

	got := normalizedMultiPicklistString(t, normalized["Existing_Database_Deployment__c"])
	if got != `["[\"Others\"]"]` {
		t.Fatalf("NormalizeSObject(JSON-looking single option) = %q, want %q", got, `["[\"Others\"]"]`)
	}
}

func TestNormalizeSObjectMultiPicklistStoredEmptyArrayIsIdempotent(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{
				APIName: "Existing_Database_Deployment__c",
				Type:    constants.FieldTypeMultiPicklist,
				Options: []models.PicklistOption{
					{Value: "Others", Label: "Others"},
				},
			},
		},
	}

	first := NormalizeSObject(schema, models.SObject{
		"Existing_Database_Deployment__c": []string{},
	})
	second := NormalizeSObject(schema, first)

	got := normalizedMultiPicklistString(t, second["Existing_Database_Deployment__c"])
	if got != "[]" {
		t.Fatalf("second NormalizeSObject(empty array) = %q, want %q", got, "[]")
	}
}

func TestNormalizeSObjectMultiPicklistDoubleCallPreservesSingleEncodingWithoutDeclaredOptions(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{
				APIName: "Existing_Database_Deployment__c",
				Type:    constants.FieldTypeMultiPicklist,
			},
		},
	}

	first := NormalizeSObject(schema, models.SObject{
		"Existing_Database_Deployment__c": "Others",
	})
	second := NormalizeSObject(schema, first)

	got := normalizedMultiPicklistString(t, second["Existing_Database_Deployment__c"])
	if got != "[\"Others\"]" {
		t.Fatalf("second NormalizeSObject(optionless field) = %q, want %q", got, "[\"Others\"]")
	}
}

func TestNormalizeSObjectRuntimeScopedMultiPicklistStoredEmptyArrayIsIdempotent(t *testing.T) {
	validator := "runtime_scoped_picklist"
	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{
				APIName:   "Existing_Database_Deployment__c",
				Type:      constants.FieldTypeMultiPicklist,
				Validator: &validator,
			},
		},
	}

	first := NormalizeSObject(schema, models.SObject{
		"Existing_Database_Deployment__c": []string{},
	})
	second := NormalizeSObject(schema, first)

	got := normalizedMultiPicklistString(t, second["Existing_Database_Deployment__c"])
	if got != "[]" {
		t.Fatalf("second NormalizeSObject(runtime-scoped empty array) = %q, want %q", got, "[]")
	}
}

func TestNormalizeSObjectOptionlessMultiPicklistJSONStringStaysLiteral(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{
				APIName: "Existing_Database_Deployment__c",
				Type:    constants.FieldTypeMultiPicklist,
			},
		},
	}

	normalized := NormalizeSObject(schema, models.SObject{
		"Existing_Database_Deployment__c": `["Others"]`,
	})

	got := normalizedMultiPicklistString(t, normalized["Existing_Database_Deployment__c"])
	if got != `["[\"Others\"]"]` {
		t.Fatalf("NormalizeSObject(optionless JSON-array string) = %q, want literal option storage", got)
	}
}

func TestNormalizeSObject_CoercesByteBackedStringLikeFields(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName: "Lead",
		Fields: []models.FieldMetadata{
			{APIName: "Title", Type: constants.FieldTypeString},
			{APIName: "OwnerId", Type: constants.FieldTypeReference},
		},
	}

	normalized := NormalizeSObject(schema, models.SObject{
		"Title":   []byte("Japan"),
		"OwnerId": []byte("20000000-0000-0000-0000-000000000002"),
	})

	title, ok := normalized["Title"].(string)
	if !ok || title != "Japan" {
		t.Fatalf("Title = %#v (%T), want string %q", normalized["Title"], normalized["Title"], "Japan")
	}

	ownerID, ok := normalized["OwnerId"].(string)
	if !ok || ownerID != "20000000-0000-0000-0000-000000000002" {
		t.Fatalf("OwnerId = %#v (%T), want string %q", normalized["OwnerId"], normalized["OwnerId"], "20000000-0000-0000-0000-000000000002")
	}
}

func TestFromStorageRecord_NormalizesDateValuedRollupSummaryByReturnType(t *testing.T) {
	returnType := models.FieldType(constants.FieldTypeDate)
	schema := &models.ObjectMetadata{
		APIName: "Order",
		Fields: []models.FieldMetadata{
			{
				APIName:    "Overall_End_Date__c",
				Type:       constants.FieldTypeRollupSummary,
				ReturnType: &returnType,
			},
		},
	}

	got := FromStorageRecord(schema, models.SObject{
		"Overall_End_Date__c": time.Date(2027, time.January, 17, 0, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60)),
	}, nil)
	if got["Overall_End_Date__c"] != "2027-01-17" {
		t.Fatalf("Overall_End_Date__c = %#v, want canonical date string", got["Overall_End_Date__c"])
	}
}

func TestFromStorageRecord_NormalizesDecimalBackedCountRollupToInteger(t *testing.T) {
	returnType := models.FieldType(constants.FieldTypeInt)
	schema := &models.ObjectMetadata{
		APIName: "Campaign",
		Fields: []models.FieldMetadata{{
			APIName:    "NumberOfResponses",
			Type:       constants.FieldTypeRollupSummary,
			ReturnType: &returnType,
		}},
	}

	got := FromStorageRecord(schema, models.SObject{
		"NumberOfResponses": "48.000000000000000000000000000000",
	}, nil)
	if value, ok := got["NumberOfResponses"].(int64); !ok || value != 48 {
		t.Fatalf("NumberOfResponses = %#v (%T), want int64(48)", got["NumberOfResponses"], got["NumberOfResponses"])
	}
}

func TestFromStorageRecordPreservesPercentDecimalPrecision(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{APIName: "Probability", Type: constants.FieldTypePercent},
		},
	}

	got := FromStorageRecord(schema, models.SObject{
		"Probability": "42.500000000000000001",
	}, nil)
	if value, ok := got["Probability"].(float64); !ok || value != 42.5 {
		t.Fatalf("Probability = %#v (%T), want public hydrated float64 42.5", got["Probability"], got["Probability"])
	}
	decimal, ok := ExactDecimalForField(got, "Probability")
	if !ok || decimal.String() != "42.500000000000000001" {
		t.Fatalf("ExactDecimalForField(Probability) = %#v, %v; want exact decimal text preserved", decimal, ok)
	}
}

func TestFromStorageRecordNormalizesIntegerAverageRollupToDecimal(t *testing.T) {
	returnType := constants.FieldTypeDouble
	schema := &models.ObjectMetadata{
		APIName: "Parent__c",
		Fields: []models.FieldMetadata{{
			APIName:    "AverageQuantity__c",
			Type:       constants.FieldTypeRollupSummary,
			ReturnType: &returnType,
		}},
	}

	got := FromStorageRecord(schema, models.SObject{
		"AverageQuantity__c": "1.500000000000000000000000000000",
	}, nil)
	if value, ok := got["AverageQuantity__c"].(float64); !ok || value != 1.5 {
		t.Fatalf("AverageQuantity__c = %#v (%T), want float64(1.5)", got["AverageQuantity__c"], got["AverageQuantity__c"])
	}
	decimal, ok := ExactDecimalForField(got, "AverageQuantity__c")
	if !ok || decimal.String() != "1.500000000000000000000000000000" {
		t.Fatalf("ExactDecimalForField(AverageQuantity__c) = %#v, %v", decimal, ok)
	}
}

func TestFromStorageRecordPreservesEveryDecimalBackedField(t *testing.T) {
	formulaReturnType := constants.FieldTypeCurrency
	rollupReturnType := constants.FieldTypePercent
	schema := &models.ObjectMetadata{
		APIName: "DecimalShapes",
		Fields: []models.FieldMetadata{
			{APIName: "NumberValue", Type: constants.FieldTypeDouble},
			{APIName: "CurrencyValue", Type: constants.FieldTypeCurrency},
			{APIName: "PercentValue", Type: constants.FieldTypePercent},
			{APIName: "FormulaValue", Type: constants.FieldTypeFormula, ReturnType: &formulaReturnType},
			{APIName: "RollupValue", Type: constants.FieldTypeRollupSummary, ReturnType: &rollupReturnType},
		},
	}
	values := map[string]string{
		"NumberValue":   "12345678901234567890123456789.123456789",
		"CurrencyValue": "-42.5000",
		"PercentValue":  "0.000000000",
		"FormulaValue":  "99.0100",
		"RollupValue":   "12.3400",
	}
	input := make(models.SObject, len(values))
	for field, value := range values {
		input[field] = value
	}

	got := FromStorageRecord(schema, input, nil)
	for field, want := range values {
		if _, ok := got[field].(float64); !ok {
			t.Errorf("%s public value = %#v (%T), want legacy float64", field, got[field], got[field])
		}
		decimal, ok := ExactDecimalForField(got, field)
		if !ok || decimal.String() != want {
			t.Errorf("ExactDecimalForField(%s) = %#v, %v; want %q", field, decimal, ok, want)
		}
	}

	large, _ := ExactDecimalForField(got, "NumberValue")
	coefficient, scale, ok := large.CoefficientAndScale()
	if !ok || coefficient != "12345678901234567890123456789123456789" || scale != 9 {
		t.Fatalf("large decimal parts = %q, %d, %v", coefficient, scale, ok)
	}
	zero, _ := ExactDecimalForField(got, "PercentValue")
	coefficient, scale, ok = zero.CoefficientAndScale()
	if !ok || coefficient != "0" || scale != 9 {
		t.Fatalf("zero decimal parts = %q, %d, %v", coefficient, scale, ok)
	}
}

func TestFromStorageRecordNormalizesBooleanByEffectiveFieldType(t *testing.T) {
	booleanType := constants.FieldTypeBoolean
	schema := &models.ObjectMetadata{APIName: "BooleanShapes", Fields: []models.FieldMetadata{
		{APIName: "PhysicalTrue", Type: constants.FieldTypeBoolean},
		{APIName: "FormulaFalse", Type: constants.FieldTypeFormula, ReturnType: &booleanType},
		{APIName: "RollupTrue", Type: constants.FieldTypeRollupSummary, ReturnType: &booleanType},
	}}

	got := FromStorageRecord(schema, models.SObject{
		"PhysicalTrue": int64(1),
		"FormulaFalse": "false",
		"RollupTrue":   int64(1),
	}, nil)
	if value, ok := got["PhysicalTrue"].(bool); !ok || !value {
		t.Fatalf("PhysicalTrue = %#v (%T), want true", got["PhysicalTrue"], got["PhysicalTrue"])
	}
	if value, ok := got["FormulaFalse"].(bool); !ok || value {
		t.Fatalf("FormulaFalse = %#v (%T), want false", got["FormulaFalse"], got["FormulaFalse"])
	}
	if value, ok := got["RollupTrue"].(bool); !ok || !value {
		t.Fatalf("RollupTrue = %#v (%T), want true", got["RollupTrue"], got["RollupTrue"])
	}
}

func TestEffectiveFieldTypePrefersCanonicalRollupResultType(t *testing.T) {
	dateType := models.FieldType(constants.FieldTypeDate)
	currencyType := models.FieldType(constants.FieldTypeCurrency)
	doubleType := models.FieldType(constants.FieldTypeDouble)
	intType := models.FieldType(constants.FieldTypeInt)

	tests := []struct {
		name  string
		field models.FieldMetadata
		want  constants.SchemaFieldType
	}{
		{
			name: "config-only MIN result keeps formula-source date type",
			field: models.FieldMetadata{
				Type: constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					ResultType: &dateType,
				},
			},
			want: constants.FieldTypeDate,
		},
		{
			name: "canonical result wins over hydrated compatibility mirror",
			field: models.FieldMetadata{
				Type:       constants.FieldTypeRollupSummary,
				ReturnType: &doubleType,
				RollupConfig: &models.RollupConfig{
					ResultType: &dateType,
				},
			},
			want: constants.FieldTypeDate,
		},
		{
			name: "SUM result remains currency",
			field: models.FieldMetadata{
				Type: constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					ResultType: &currencyType,
				},
			},
			want: constants.FieldTypeCurrency,
		},
		{
			name: "AVG result remains double",
			field: models.FieldMetadata{
				Type: constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					ResultType: &doubleType,
				},
			},
			want: constants.FieldTypeDouble,
		},
		{
			name: "COUNT result remains int",
			field: models.FieldMetadata{
				Type: constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					ResultType: &intType,
				},
			},
			want: constants.FieldTypeInt,
		},
		{
			name: "hydrated legacy metadata falls back to ReturnType",
			field: models.FieldMetadata{
				Type:       constants.FieldTypeRollupSummary,
				ReturnType: &dateType,
			},
			want: constants.FieldTypeDate,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := EffectiveFieldType(tt.field); got != tt.want {
				t.Fatalf("EffectiveFieldType(%+v) = %q, want %q", tt.field, got, tt.want)
			}
		})
	}
}

func TestExactDecimalRejectsNonPlainDecimalGrammar(t *testing.T) {
	for _, value := range []ExactDecimal{"1e2", "+1.20", ".5", "5.", "01.20", "1/2"} {
		if _, _, ok := value.CoefficientAndScale(); ok {
			t.Errorf("CoefficientAndScale(%q) accepted non-plain decimal", value)
		}
	}
}

func TestCopyExactDecimalsForPresentFields(t *testing.T) {
	source := models.SObject{"Visible": 1.25, "Hidden": 9.5}
	SetExactDecimalForField(source, "Visible", ExactDecimal("1.2500"))
	SetExactDecimalForField(source, "Hidden", ExactDecimal("9.5000"))
	target := models.SObject{"Visible": 1.25}
	SetExactDecimalForField(target, "Stale", ExactDecimal("7.00"))

	CopyExactDecimalsForPresentFields(source, target)
	if decimal, ok := ExactDecimalForField(target, "Visible"); !ok || decimal.String() != "1.2500" {
		t.Fatalf("visible exact decimal = %#v, %v", decimal, ok)
	}
	if _, ok := ExactDecimalForField(target, "Hidden"); ok {
		t.Fatal("hidden exact decimal carrier was copied")
	}
	if _, ok := ExactDecimalForField(target, "Stale"); ok {
		t.Fatal("stale exact decimal carrier remained in target")
	}
}
