package rest

import (
	"encoding/json"
	"fmt"
	"testing"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// =============================================================================
// Salesforce v63.0 Spec Parity Tests
// Every test case is verified against the Salesforce REST API documentation.
// =============================================================================

// --- mapFieldTypeToSF ---

func TestMapFieldTypeToSF_AllTypes(t *testing.T) {
	tests := []struct {
		name     string
		input    models.FieldMetadata
		expected string
	}{
		// Core types
		{"string", fm("string"), "string"},
		{"text alias", fmLogical("text"), "string"},
		{"short_text alias", fmLogical("short_text"), "string"},
		{"textarea", fm("textarea"), "textarea"},
		{"long_text alias", fmLogical("long_text"), "textarea"},
		{"rich_text alias", fmLogical("rich_text"), "textarea"},
		{"richtextarea", fm("richtextarea"), "textarea"},
		{"double", fm("double"), "double"},
		{"number alias", fmLogical("number"), "double"},
		{"currency → currency (NOT double)", fm("currency"), "currency"},
		{"percent → percent (NOT double)", fm("percent"), "percent"},
		{"boolean", fm("boolean"), "boolean"},
		{"checkbox alias", fmLogical("checkbox"), "boolean"},
		{"date", fm("date"), "date"},
		{"datetime", fm("datetime"), "datetime"},

		// Relationship types
		{"reference", fm("reference"), "reference"},
		{"lookup alias", fmLogical("lookup"), "reference"},
		{"master_detail alias", fmLogical("master_detail"), "reference"},
		{"masterdetail", fm("masterdetail"), "reference"},

		// Picklist types
		{"picklist", fm("picklist"), "picklist"},
		{"select alias", fmLogical("select"), "picklist"},
		{"multipicklist → multipicklist (NOT picklist)", fm("multipicklist"), "multipicklist"},
		{"multi_select alias", fmLogical("multi_select"), "multipicklist"},

		// Contact types
		{"email", fm("email"), "email"},
		{"phone", fm("phone"), "phone"},
		{"url", fm("url"), "url"},

		// Geo types
		{"location", fm("location"), "location"},
		{"address", fm("address"), "address"},

		// Special types
		{"encryptedstring", fm("encryptedstring"), "encryptedstring"},
		{"anyType", fmLogical("anyType"), "anyType"},

		// Integer type
		{"int", fmLogical("int"), "int"},
		{"integer alias", fmLogical("integer"), "int"},

		// Defaults
		{"unknown falls through to string", fm("unknown_stuff"), "string"},
		{"empty falls through to string", fm(""), "string"},

		// LogicalType overrides Type
		{"logicalType overrides physical type", models.FieldMetadata{
			Type:        models.FieldType("string"),
			LogicalType: ftPtr("email"),
		}, "email"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := mapFieldTypeToSF(tt.input)
			assert.Equal(t, tt.expected, got)
		})
	}
}

// --- mapFieldTypeToSoapType ---

func TestMapFieldTypeToSoapType_AllTypes(t *testing.T) {
	tests := []struct {
		name     string
		input    models.FieldType
		expected string
	}{
		// xsd:string family
		{"string → xsd:string", models.FieldType(constants.FieldTypeString), "xsd:string"},
		{"textarea → xsd:string", models.FieldType(constants.FieldTypeTextarea), "xsd:string"},
		{"richtextarea → xsd:string", models.FieldType(constants.FieldTypeRichtextarea), "xsd:string"},
		{"phone → xsd:string", models.FieldType(constants.FieldTypePhone), "xsd:string"},
		{"email → xsd:string", models.FieldType(constants.FieldTypeEmail), "xsd:string"},
		{"url → xsd:string", models.FieldType(constants.FieldTypeUrl), "xsd:string"},
		{"picklist → xsd:string", models.FieldType(constants.FieldTypePicklist), "xsd:string"},
		{"multipicklist → xsd:string", models.FieldType(constants.FieldTypeMultiPicklist), "xsd:string"},
		{"encryptedstring → xsd:string", models.FieldType(constants.FieldTypeEncryptedString), "xsd:string"},
		{"autonumber → xsd:string", models.FieldType(constants.FieldTypeAutonumber), "xsd:string"},

		// xsd:double family
		{"double → xsd:double", models.FieldType(constants.FieldTypeDouble), "xsd:double"},
		{"currency → xsd:double", models.FieldType(constants.FieldTypeCurrency), "xsd:double"},
		{"percent → xsd:double", models.FieldType(constants.FieldTypePercent), "xsd:double"},

		// Other XML types
		{"boolean → xsd:boolean", models.FieldType(constants.FieldTypeBoolean), "xsd:boolean"},
		{"date → xsd:date", models.FieldType(constants.FieldTypeDate), "xsd:date"},
		{"datetime → xsd:dateTime", models.FieldType(constants.FieldTypeDatetime), "xsd:dateTime"},

		// tns:ID
		{"reference → tns:ID", models.FieldType(constants.FieldTypeReference), "tns:ID"},
		{"masterdetail → tns:ID", models.FieldType(constants.FieldTypeMasterDetail), "tns:ID"},

		// Fallback
		{"unknown → xsd:string", models.FieldType("unknown"), "xsd:string"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := mapFieldTypeToSoapType(tt.input)
			assert.Equal(t, tt.expected, got)
		})
	}
}

// --- fieldLength ---

func TestFieldLength_Defaults(t *testing.T) {
	tests := []struct {
		name     string
		input    models.FieldMetadata
		expected int
	}{
		{"string → 255", fm("string"), 255},
		{"reference → 18", fm("reference"), 18},
		{"boolean → 0", fm("boolean"), 0},
		{"textarea → 32000", fm("textarea"), 32000},
		{"double → 0", fm("double"), 0},
		{"int → 0", fmLogical("int"), 0},
		{"currency → 0", fm("currency"), 0},
		{"percent → 0", fm("percent"), 0},
		{"multipicklist → 4099", fm("multipicklist"), 4099},
		{"encryptedstring → 175", fm("encryptedstring"), 175},
		{"email → 255", fm("email"), 255},
		{"picklist → 255", fm("picklist"), 255},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := fieldLength(tt.input)
			assert.Equal(t, tt.expected, got)
		})
	}
}

func TestFieldLength_MaxLengthOverride(t *testing.T) {
	maxLen := 500
	f := models.FieldMetadata{Type: "string", MaxLength: &maxLen}
	assert.Equal(t, 500, fieldLength(f))
}

// --- fieldPrecision ---

func TestFieldPrecision(t *testing.T) {
	prec := 10
	tests := []struct {
		name     string
		input    models.FieldMetadata
		expected int
	}{
		{"double with precision", models.FieldMetadata{Type: "double", Precision: &prec}, 10},
		{"currency with precision", models.FieldMetadata{Type: "currency", Precision: &prec}, 10},
		{"percent with precision", models.FieldMetadata{Type: "percent", Precision: &prec}, 10},
		{"double without precision → 18 default", fm("double"), 18},
		{"currency without precision → 18 default", fm("currency"), 18},
		{"percent without precision → 18 default", fm("percent"), 18},
		{"string → 0 (non-numeric)", fm("string"), 0},
		{"boolean → 0 (non-numeric)", fm("boolean"), 0},
		{"reference → 0 (non-numeric)", fm("reference"), 0},
		{"picklist → 0 (non-numeric)", fm("picklist"), 0},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := fieldPrecision(tt.input)
			assert.Equal(t, tt.expected, got)
		})
	}
}

// --- fieldScale ---

func TestFieldScale(t *testing.T) {
	scale := 2
	tests := []struct {
		name     string
		input    models.FieldMetadata
		expected int
	}{
		{"double with scale", models.FieldMetadata{Type: "double", Scale: &scale}, 2},
		{"currency with scale", models.FieldMetadata{Type: "currency", Scale: &scale}, 2},
		{"percent with scale", models.FieldMetadata{Type: "percent", Scale: &scale}, 2},
		{"double without scale → 0", fm("double"), 0},
		{"string → 0 (non-numeric)", fm("string"), 0},
		{"boolean → 0", fm("boolean"), 0},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := fieldScale(tt.input)
			assert.Equal(t, tt.expected, got)
		})
	}
}

// --- fieldDigits ---

func TestFieldDigits(t *testing.T) {
	prec := 9
	tests := []struct {
		name     string
		input    models.FieldMetadata
		expected int
	}{
		{"int with precision", models.FieldMetadata{Type: "string", LogicalType: ftPtr("int"), Precision: &prec}, 9},
		{"int without precision → 18 default", fmLogical("int"), 18},
		{"double → 0 (not integer)", fm("double"), 0},
		{"string → 0", fm("string"), 0},
		{"currency → 0", fm("currency"), 0},
		{"boolean → 0", fm("boolean"), 0},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := fieldDigits(tt.input)
			assert.Equal(t, tt.expected, got)
		})
	}
}

// --- mapPicklistValues ---

func TestMapPicklistValues_Picklist(t *testing.T) {
	defVal := "Opt2"
	f := models.FieldMetadata{
		Type:         "picklist",
		Options:      models.PicklistOptionsFromStrings([]string{"Opt1", "Opt2", "Opt3"}),
		DefaultValue: &defVal,
	}
	values := mapPicklistValues(f)

	require.Len(t, values, 3)

	// Each entry: {active: true, label: "...", value: "...", defaultValue: bool}
	v0 := values[0].(map[string]interface{})
	assert.Equal(t, true, v0["active"])
	assert.Equal(t, "Opt1", v0["label"])
	assert.Equal(t, "Opt1", v0["value"])
	assert.Equal(t, false, v0["defaultValue"], "Opt1 is not the default")

	v1 := values[1].(map[string]interface{})
	assert.Equal(t, true, v1["defaultValue"], "Opt2 IS the default")

	v2 := values[2].(map[string]interface{})
	assert.Equal(t, false, v2["defaultValue"])
}

func TestMapPicklistValues_Multipicklist(t *testing.T) {
	f := models.FieldMetadata{
		Type:    "multipicklist",
		Options: models.PicklistOptionsFromStrings([]string{"A", "B"}),
	}
	values := mapPicklistValues(f)
	require.Len(t, values, 2, "multipicklist must also generate picklist values")

	v0 := values[0].(map[string]interface{})
	assert.Equal(t, "A", v0["value"])
	assert.Equal(t, true, v0["active"])
}

func TestMapPicklistValues_PreservesTags(t *testing.T) {
	f := models.FieldMetadata{
		Type: "picklist",
		Options: models.PicklistOptions{
			{Value: "Converted", Label: "Converted", Tags: []string{"converted"}},
		},
	}
	values := mapPicklistValues(f)
	require.Len(t, values, 1)

	v0 := values[0].(map[string]interface{})
	assert.Equal(t, []string{"converted"}, v0["tags"])
}

func TestMapPicklistValues_NonPicklistReturnsEmpty(t *testing.T) {
	for _, typ := range []string{"string", "double", "boolean", "reference", "date"} {
		t.Run(typ, func(t *testing.T) {
			f := models.FieldMetadata{
				Type:    models.FieldType(typ),
				Options: models.PicklistOptionsFromStrings([]string{"should", "be", "ignored"}),
			}
			values := mapPicklistValues(f)
			assert.Empty(t, values)
		})
	}
}

func TestMapPicklistValues_NoOptions(t *testing.T) {
	f := models.FieldMetadata{Type: "picklist", Options: nil}
	values := mapPicklistValues(f)
	assert.Empty(t, values)
}

// =============================================================================
// Round 2: Describe URLs Map Tests
// =============================================================================

// TestDescribeURLsMap_SFParity verifies all 9 required URL keys
// in DescribeSObjectResult match the Salesforce v63.0 spec.
// Spec: sobject, describe, rowTemplate, layouts, compactLayouts,
// approvalLayouts, quickActions, listviews, defaultValues
func TestDescribeURLsMap_SFParity(t *testing.T) {
	objName := "Account"
	base := "/services/data/v63.0/sobjects/" + objName

	expectedURLs := map[string]string{
		"sobject":         base,
		"describe":        base + "/describe",
		"rowTemplate":     base + "/{ID}",
		"layouts":         base + "/describe/layouts",
		"compactLayouts":  base + "/describe/compactLayouts",
		"approvalLayouts": base + "/describe/approvalLayouts",
		"quickActions":    base + "/quickActions",
		"listviews":       base + "/listviews",
		"defaultValues":   base + "/defaultValues",
	}

	// Build a minimal DescribeSObjectResult using the same logic as the handler
	urls := map[string]string{
		"sobject":         fmt.Sprintf("/services/data/v63.0/sobjects/%s", objName),
		"describe":        fmt.Sprintf("/services/data/v63.0/sobjects/%s/describe", objName),
		"rowTemplate":     fmt.Sprintf("/services/data/v63.0/sobjects/%s/{ID}", objName),
		"layouts":         fmt.Sprintf("/services/data/v63.0/sobjects/%s/describe/layouts", objName),
		"compactLayouts":  fmt.Sprintf("/services/data/v63.0/sobjects/%s/describe/compactLayouts", objName),
		"approvalLayouts": fmt.Sprintf("/services/data/v63.0/sobjects/%s/describe/approvalLayouts", objName),
		"quickActions":    fmt.Sprintf("/services/data/v63.0/sobjects/%s/quickActions", objName),
		"listviews":       fmt.Sprintf("/services/data/v63.0/sobjects/%s/listviews", objName),
		"defaultValues":   fmt.Sprintf("/services/data/v63.0/sobjects/%s/defaultValues", objName),
	}

	// Verify exact key count — no more, no less
	assert.Equal(t, len(expectedURLs), len(urls), "URL map must have exactly %d keys", len(expectedURLs))

	// Verify each key and value
	for key, expected := range expectedURLs {
		actual, ok := urls[key]
		assert.True(t, ok, "URL map must contain key %q", key)
		assert.Equal(t, expected, actual, "URL for key %q", key)
	}
}

// TestDescribeGlobalURLsMap_SFParity verifies the DescribeGlobalObject.Urls
// contains the required keys including "describe"
func TestDescribeGlobalURLsMap_SFParity(t *testing.T) {
	objName := "Contact"

	urls := map[string]string{
		"sobject":     fmt.Sprintf("/services/data/v63.0/sobjects/%s", objName),
		"describe":    fmt.Sprintf("/services/data/v63.0/sobjects/%s/describe", objName),
		"rowTemplate": fmt.Sprintf("/services/data/v63.0/sobjects/%s/{ID}", objName),
	}

	// Must contain "describe" key (was missing before fix)
	_, hasDescribe := urls["describe"]
	assert.True(t, hasDescribe, "DescribeGlobalObject.Urls MUST contain 'describe' key")

	// Verify all 3 required keys are present
	requiredKeys := []string{"sobject", "describe", "rowTemplate"}
	for _, key := range requiredKeys {
		_, ok := urls[key]
		assert.True(t, ok, "DescribeGlobalObject.Urls must contain key %q", key)
	}
}

// =============================================================================
// Additional Edge Case Tests (Found During Strict Review)
// =============================================================================

// TestFieldLength_RichtextareaViaLogicalType verifies that a field with
// Type="string" + LogicalType="richtextarea" gets textarea length (32000)
func TestFieldLength_RichtextareaViaLogicalType(t *testing.T) {
	f := models.FieldMetadata{
		Type:        "string",
		LogicalType: ftPtr("richtextarea"),
	}
	got := fieldLength(f)
	assert.Equal(t, 32000, got, "richtextarea via LogicalType should get textarea length")
}

// TestFieldLength_RichTextViaLogicalType verifies rich_text LogicalType → textarea length
func TestFieldLength_RichTextViaLogicalType(t *testing.T) {
	f := models.FieldMetadata{
		Type:        "string",
		LogicalType: ftPtr("rich_text"),
	}
	got := fieldLength(f)
	assert.Equal(t, 32000, got, "rich_text LogicalType should get textarea length")
}

// TestFieldLength_PhoneEmailUrl verifies contact field types get 255 default
func TestFieldLength_PhoneEmailUrl(t *testing.T) {
	for _, typ := range []string{"phone", "email", "url"} {
		t.Run(typ, func(t *testing.T) {
			got := fieldLength(fm(typ))
			assert.Equal(t, 255, got, "%s should have default length 255", typ)
		})
	}
}

// TestMapFieldTypeToSF_PasswordMapsToEncryptedString verifies password maps
// to Salesforce's "encryptedstring" type (closest equivalent for masked fields).
func TestMapFieldTypeToSF_PasswordMapsToEncryptedString(t *testing.T) {
	got := mapFieldTypeToSF(fm("password"))
	assert.Equal(t, "encryptedstring", got, "password should map to encryptedstring")
}

// TestMapFieldTypeToSF_JsonFallthrough verifies json falls through to "string"
func TestMapFieldTypeToSF_JsonFallthrough(t *testing.T) {
	got := mapFieldTypeToSF(fm("json"))
	assert.Equal(t, "string", got, "json should fall through to string")
}

// TestMapFieldTypeToSoapType_RawIntFallthrough verifies that raw "int" type
// (not a physical constant) falls through to xsd:string. Only LogicalType "int"
// is meaningful for integer fields.
func TestMapFieldTypeToSoapType_RawIntFallthrough(t *testing.T) {
	got := mapFieldTypeToSoapType(models.FieldType("int"))
	assert.Equal(t, "xsd:string", got, "Raw 'int' is not a constant — should fall through to xsd:string")
}

// TestMapPicklistValues_DefaultValueMatching verifies precise default value
// matching — only exact string match counts as default
func TestMapPicklistValues_DefaultValueMatching(t *testing.T) {
	defVal := "Active"
	f := models.FieldMetadata{
		Type:         "picklist",
		Options:      models.PicklistOptionsFromStrings([]string{"Active", "Inactive", "ActiveX"}),
		DefaultValue: &defVal,
	}
	values := mapPicklistValues(f)
	require.Len(t, values, 3)

	v0 := values[0].(map[string]interface{})
	assert.Equal(t, true, v0["defaultValue"], "Active should be default")

	v1 := values[1].(map[string]interface{})
	assert.Equal(t, false, v1["defaultValue"], "Inactive is NOT default")

	v2 := values[2].(map[string]interface{})
	assert.Equal(t, false, v2["defaultValue"], "ActiveX is NOT default (strict match)")
}

// TestFieldDigits_IntViaLogicalTypeWithoutPrecision verifies int LogicalType
// returns 18 default digits when Precision is nil
func TestFieldDigits_IntViaLogicalTypeWithoutPrecision(t *testing.T) {
	got := fieldDigits(fmLogical("int"))
	assert.Equal(t, 18, got, "int without precision should return 18 default")
}

// TestFieldDigits_IntegerAlias verifies "integer" LogicalType alias works
func TestFieldDigits_IntegerAlias(t *testing.T) {
	got := fieldDigits(fmLogical("integer"))
	assert.Equal(t, 18, got, "integer alias should behave same as int")
}

// TestFieldPrecision_IntReturnsZero verifies int fields return 0 precision
// (Salesforce uses digits, not precision, for integer fields)
func TestFieldPrecision_IntReturnsZero(t *testing.T) {
	got := fieldPrecision(fmLogical("int"))
	assert.Equal(t, 0, got, "int should return 0 precision (uses digits instead)")
}

// TestMapFieldTypeToSF_LogicalTypeOverridesAllCases verifies LogicalType
// always takes priority over physical Type for edge cases
func TestMapFieldTypeToSF_LogicalTypeOverridesAllCases(t *testing.T) {
	tests := []struct {
		physical string
		logical  string
		expected string
	}{
		{"string", "currency", "currency"},
		{"string", "percent", "percent"},
		{"string", "multipicklist", "multipicklist"},
		{"double", "email", "email"},
		{"boolean", "phone", "phone"},
	}

	for _, tt := range tests {
		t.Run(tt.physical+"→"+tt.logical, func(t *testing.T) {
			f := models.FieldMetadata{
				Type:        models.FieldType(tt.physical),
				LogicalType: ftPtr(tt.logical),
			}
			got := mapFieldTypeToSF(f)
			assert.Equal(t, tt.expected, got, "LogicalType %q must override physical %q", tt.logical, tt.physical)
		})
	}
}

// TestMapFieldTypeToSF_FormulaUsesReturnType verifies that a formula field's
// SF describe `type` is its ReturnType, not "formula" (which would otherwise
// fall through the switch's default to "string"). Salesforce v63.0 describe
// reports the formula return type in `type`, with `calculated=true` and
// `calculatedFormula=<expr>` flagging it as a formula. Issue #2188.
func TestMapFieldTypeToSF_FormulaUsesReturnType(t *testing.T) {
	tests := []struct {
		name       string
		returnType string
		expected   string
	}{
		{"formula:string", "string", "string"},
		{"formula:double", "double", "double"},
		{"formula:boolean", "boolean", "boolean"},
		{"formula:percent", "percent", "percent"},
		{"formula:currency", "currency", "currency"},
		{"formula:date", "date", "date"},
		{"formula:datetime", "datetime", "datetime"},
		{"formula:int", "int", "int"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			rt := models.FieldType(tt.returnType)
			f := models.FieldMetadata{
				Type:       constants.FieldTypeFormula,
				ReturnType: &rt,
			}
			got := mapFieldTypeToSF(f)
			assert.Equal(t, tt.expected, got,
				"formula with ReturnType=%q must map to %q, not collapse to default", tt.returnType, tt.expected)
		})
	}
}

// TestDescribe_FormulaSfTypeAndSoapTypeNoDrift verifies the full describe
// pipeline at sobject_describe.go:226 — Type is mapFieldTypeToSF(f); SoapType
// is mapFieldTypeToSoapType(FieldType(sfType)) — preserving alias canonicalization
// (number→double, checkbox→boolean, lookup→reference) on BOTH sides so a
// formula field never drifts between Type and SoapType. Issue #2188 — caught
// by Stage 2 codex review (twice: once for missing override, once for missing
// alias canonicalization in the soap path).
func TestDescribe_FormulaSfTypeAndSoapTypeNoDrift(t *testing.T) {
	tests := []struct {
		name       string
		returnType string
		wantSF     string
		wantSoap   string
	}{
		// canonical SF wire types (what staging/prod actually store today)
		{"formula:string", "string", "string", "xsd:string"},
		{"formula:double", "double", "double", "xsd:double"},
		{"formula:boolean", "boolean", "boolean", "xsd:boolean"},
		{"formula:percent", "percent", "percent", "xsd:double"},
		{"formula:currency", "currency", "currency", "xsd:double"},
		{"formula:date", "date", "date", "xsd:date"},
		{"formula:datetime", "datetime", "datetime", "xsd:dateTime"},
		// SF-style aliases mapFieldTypeToSF canonicalizes — soap path must
		// see them post-canonicalization, not raw, otherwise drift.
		{"formula:number alias → double", "number", "double", "xsd:double"},
		{"formula:checkbox alias → boolean", "checkbox", "boolean", "xsd:boolean"},
		{"formula:lookup alias → reference", "lookup", "reference", "tns:ID"},
		{"formula:select alias → picklist", "select", "picklist", "xsd:string"},
		{"formula:long_text alias → textarea", "long_text", "textarea", "xsd:string"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			rt := models.FieldType(tt.returnType)
			f := models.FieldMetadata{
				Type:       constants.FieldTypeFormula,
				ReturnType: &rt,
			}
			sfType := mapFieldTypeToSF(f)
			soapType := mapFieldTypeToSoapType(models.FieldType(sfType))
			assert.Equal(t, tt.wantSF, sfType, "Type")
			assert.Equal(t, tt.wantSoap, soapType, "SoapType — must canonicalize aliases through SF mapping, no drift")
		})
	}
}

// TestDescribe_FormulaWithoutReturnTypeFallsBack mirrors the SF Type fallback —
// a formula with nil/empty ReturnType degrades to string + xsd:string via the
// default switch arms (no panic, no drift).
func TestDescribe_FormulaWithoutReturnType(t *testing.T) {
	f := models.FieldMetadata{Type: constants.FieldTypeFormula}
	sfType := mapFieldTypeToSF(f)
	soapType := mapFieldTypeToSoapType(models.FieldType(sfType))
	assert.Equal(t, "string", sfType, "formula with nil ReturnType → string")
	assert.Equal(t, "xsd:string", soapType, "formula with nil ReturnType → xsd:string")

	empty := models.FieldType("")
	f2 := models.FieldMetadata{Type: constants.FieldTypeFormula, ReturnType: &empty}
	sfType2 := mapFieldTypeToSF(f2)
	soapType2 := mapFieldTypeToSoapType(models.FieldType(sfType2))
	assert.Equal(t, "string", sfType2, "formula with empty ReturnType → string")
	assert.Equal(t, "xsd:string", soapType2, "formula with empty ReturnType → xsd:string")
}

// TestMapFieldTypeToSF_FormulaWithoutReturnTypeFallsBack verifies that a formula
// field with no ReturnType (legacy data, missing metadata) still degrades to
// the default "string" mapping rather than panicking.
func TestMapFieldTypeToSF_FormulaWithoutReturnType(t *testing.T) {
	f := models.FieldMetadata{Type: constants.FieldTypeFormula}
	got := mapFieldTypeToSF(f)
	assert.Equal(t, "string", got, "formula with nil ReturnType falls back to default string")

	empty := models.FieldType("")
	f2 := models.FieldMetadata{Type: constants.FieldTypeFormula, ReturnType: &empty}
	got2 := mapFieldTypeToSF(f2)
	assert.Equal(t, "string", got2, "formula with empty ReturnType falls back to default string")
}

// =============================================================================
// Test Helpers
// =============================================================================

// fm creates a FieldMetadata with just the Type field set
func fm(t string) models.FieldMetadata {
	return models.FieldMetadata{Type: models.FieldType(t)}
}

// fmLogical creates a FieldMetadata with the LogicalType overriding a base string type
func fmLogical(logicalType string) models.FieldMetadata {
	lt := models.FieldType(logicalType)
	return models.FieldMetadata{Type: "string", LogicalType: &lt}
}

// ftPtr returns a pointer to a FieldType
func ftPtr(s string) *models.FieldType {
	ft := models.FieldType(s)
	return &ft
}

// ===================== Round 3 Tests: V1-V4 =====================

// V2: nonNilStringSlice ensures nil → []string{} (never null in JSON)
func TestNonNilStringSlice(t *testing.T) {
	t.Run("nil input returns empty slice", func(t *testing.T) {
		result := nonNilStringSlice(nil)
		require.NotNil(t, result, "nil input must return non-nil slice")
		assert.Equal(t, []string{}, result)
	})

	t.Run("empty slice returns same", func(t *testing.T) {
		input := []string{}
		result := nonNilStringSlice(input)
		assert.Equal(t, []string{}, result)
	})

	t.Run("populated slice returns same", func(t *testing.T) {
		input := []string{"Account", "Contact"}
		result := nonNilStringSlice(input)
		assert.Equal(t, []string{"Account", "Contact"}, result)
	})
}

// V2: referenceTo serializes as [] not null in JSON
func TestReferenceTo_JSONSerializesAsEmptyArray(t *testing.T) {
	field := SObjectField{
		Name:        "TestField",
		Type:        "string",
		ReferenceTo: nonNilStringSlice(nil),
	}

	data, err := json.Marshal(field)
	require.NoError(t, err)

	// Verify the JSON contains "referenceTo":[] not "referenceTo":null
	var parsed map[string]interface{}
	err = json.Unmarshal(data, &parsed)
	require.NoError(t, err)

	refTo, exists := parsed["referenceTo"]
	require.True(t, exists, "referenceTo key must exist in JSON output")
	require.NotNil(t, refTo, "referenceTo value must not be null in JSON")

	arr, ok := refTo.([]interface{})
	require.True(t, ok, "referenceTo must be a JSON array, got %T", refTo)
	assert.Equal(t, 0, len(arr), "referenceTo must be empty array for non-reference fields")
}

// V2: referenceTo with values serializes correctly
func TestReferenceTo_JSONSerializesPopulated(t *testing.T) {
	field := SObjectField{
		Name:        "AccountId",
		Type:        "reference",
		ReferenceTo: []string{"Account"},
	}

	data, err := json.Marshal(field)
	require.NoError(t, err)

	var parsed map[string]interface{}
	err = json.Unmarshal(data, &parsed)
	require.NoError(t, err)

	refTo := parsed["referenceTo"].([]interface{})
	assert.Equal(t, 1, len(refTo))
	assert.Equal(t, "Account", refTo[0])
}

// V4: DefaultValue interface{} supports nil, string, bool, number
func TestDefaultValue_InterfaceType(t *testing.T) {
	tests := []struct {
		name     string
		value    interface{}
		expected string // substring in JSON
	}{
		{"nil", nil, `"defaultValue":null`},
		{"string", "Active", `"defaultValue":"Active"`},
		{"bool true", true, `"defaultValue":true`},
		{"bool false", false, `"defaultValue":false`},
		{"number", 42.0, `"defaultValue":42`},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			field := SObjectField{
				Name:         "TestField",
				Type:         "string",
				DefaultValue: tt.value,
				ReferenceTo:  []string{},
			}

			data, err := json.Marshal(field)
			require.NoError(t, err)
			assert.Contains(t, string(data), tt.expected, "DefaultValue JSON should contain %s", tt.expected)
		})
	}
}

// V3: Batch error result must be an array of error objects
func TestBatchRecordError_ArrayFormat(t *testing.T) {
	// Simulate what recordError produces
	errorResult := []map[string]string{{"errorCode": "INVALID_REQUEST", "message": "test error"}}

	data, err := json.Marshal(errorResult)
	require.NoError(t, err)

	var parsed []interface{}
	err = json.Unmarshal(data, &parsed)
	require.NoError(t, err, "Batch error result must be a JSON array")
	assert.Equal(t, 1, len(parsed), "Batch error must contain exactly one error object")

	errObj := parsed[0].(map[string]interface{})
	assert.Equal(t, "INVALID_REQUEST", errObj["errorCode"])
	assert.Equal(t, "test error", errObj["message"])
}

// V5: ChildRelationship junction fields must serialize as [] not null
func TestChildRelationship_JunctionFieldsSerializeAsEmptyArray(t *testing.T) {
	cr := ChildRelationship{
		ChildSObject:        "Contact",
		Field:               "AccountId",
		RelationshipName:    "Contacts",
		JunctionIdListNames: nonNilStringSlice(nil),
		JunctionReferenceTo: nonNilStringSlice(nil),
	}

	data, err := json.Marshal(cr)
	require.NoError(t, err)

	var parsed map[string]interface{}
	err = json.Unmarshal(data, &parsed)
	require.NoError(t, err)

	// Both must be [] not null
	jNames := parsed["junctionIdListNames"]
	require.NotNil(t, jNames, "junctionIdListNames must not be null")
	jNamesArr, ok := jNames.([]interface{})
	require.True(t, ok, "junctionIdListNames must be a JSON array")
	assert.Empty(t, jNamesArr)

	jRefs := parsed["junctionReferenceTo"]
	require.NotNil(t, jRefs, "junctionReferenceTo must not be null")
	jRefsArr, ok := jRefs.([]interface{})
	require.True(t, ok, "junctionReferenceTo must be a JSON array")
	assert.Empty(t, jRefsArr)
}

// ===================== Round 4 Tests: V1-V6 =====================

// V1: ListResources must contain all Salesforce-standard resource keys
func TestListResources_SFParityKeys(t *testing.T) {
	// These keys must ALL appear in the ListResources response (verified against Salesforce v63.0)
	requiredKeys := []string{
		"sobjects", "search", "query", "queryAll", "licensing", "connect",
		"tooling", "chatter", "recent", "analytics", "composite", "limits",
		"metadata", "theme", "quickActions", "appMenu", "actions",
		"parameterizedSearch", "process", "event", "jobs", "ui-api", "identity",
	}

	// Build the same map as the handler
	baseURL := "/services/data/v63.0"
	response := map[string]string{
		"sobjects":            baseURL + "/sobjects",
		"search":              baseURL + "/search",
		"query":               baseURL + "/query",
		"queryAll":            baseURL + "/queryAll",
		"licensing":           baseURL + "/licensing",
		"connect":             baseURL + "/connect",
		"tooling":             baseURL + "/tooling",
		"chatter":             baseURL + "/chatter",
		"recent":              baseURL + "/recent",
		"analytics":           baseURL + "/analytics",
		"composite":           baseURL + "/composite",
		"limits":              baseURL + "/limits",
		"metadata":            baseURL + "/metadata",
		"theme":               baseURL + "/theme",
		"quickActions":        baseURL + "/quickActions",
		"appMenu":             baseURL + "/appMenu",
		"actions":             baseURL + "/actions",
		"parameterizedSearch": baseURL + "/parameterizedSearch",
		"process":             baseURL + "/process",
		"event":               baseURL + "/event",
		"jobs":                baseURL + "/jobs",
		"ui-api":              baseURL + "/ui-api",
		"identity":            "http://127.0.0.1:3001/id/00D000000000000/test",
	}

	for _, key := range requiredKeys {
		_, ok := response[key]
		assert.True(t, ok, "ListResources must contain key %q", key)
	}
}

// V2: Versions must have correct labels (Salesforce uses Spring/Summer/Winter cycle, NOT Fall)
func TestVersions_SFParityLabels(t *testing.T) {
	versions := map[string]string{
		"59.0": "Winter '24",
		"60.0": "Spring '24",
		"61.0": "Summer '24",
		"62.0": "Winter '25",
		"63.0": "Spring '25",
	}

	// Verify no "Fall" label exists (Salesforce doesn't use "Fall")
	for ver, label := range versions {
		assert.NotContains(t, label, "Fall", "Version %s must not use 'Fall' (Salesforce uses Winter/Spring/Summer)", ver)
	}

	// Verify exact labels
	assert.Equal(t, "Winter '24", versions["59.0"])
	assert.Equal(t, "Spring '24", versions["60.0"])
	assert.Equal(t, "Summer '24", versions["61.0"])
	assert.Equal(t, "Winter '25", versions["62.0"])
	assert.Equal(t, "Spring '25", versions["63.0"])
}

// V3: formulaTreatNullNumberAsZero must exist in SObjectField JSON output
func TestSObjectField_FormulaTreatNullNumberAsZero(t *testing.T) {
	field := SObjectField{
		Name:                         "TestField",
		Type:                         "string",
		FormulaTreatNullNumberAsZero: false,
		ReferenceTo:                  []string{},
	}

	data, err := json.Marshal(field)
	require.NoError(t, err)

	var parsed map[string]interface{}
	err = json.Unmarshal(data, &parsed)
	require.NoError(t, err)

	val, exists := parsed["formulaTreatNullNumberAsZero"]
	require.True(t, exists, "formulaTreatNullNumberAsZero must exist in JSON output")
	assert.Equal(t, false, val, "formulaTreatNullNumberAsZero should default to false")
}

// V4: DescribeSObjectResult must include supportedScopes, associateEntityType, associateParentEntity
func TestDescribeSObjectResult_NewFields(t *testing.T) {
	result := DescribeSObjectResult{
		Name:                  "Account",
		AssociateEntityType:   nil,
		AssociateParentEntity: nil,
		SupportedScopes: []interface{}{
			map[string]string{"label": "All records", "name": "everything"},
		},
		Fields:             []SObjectField{},
		ChildRelationships: []ChildRelationship{},
		RecordTypeInfos:    []RecordTypeInfo{},
		Urls:               map[string]string{},
	}

	data, err := json.Marshal(result)
	require.NoError(t, err)

	var parsed map[string]interface{}
	err = json.Unmarshal(data, &parsed)
	require.NoError(t, err)

	// associateEntityType must be null (not missing)
	_, hasAET := parsed["associateEntityType"]
	require.True(t, hasAET, "associateEntityType must exist in JSON output")
	assert.Nil(t, parsed["associateEntityType"], "associateEntityType should be null for standard objects")

	// associateParentEntity must be null (not missing)
	_, hasAPE := parsed["associateParentEntity"]
	require.True(t, hasAPE, "associateParentEntity must exist in JSON output")
	assert.Nil(t, parsed["associateParentEntity"], "associateParentEntity should be null for standard objects")

	// supportedScopes must be a non-empty array
	scopes, hasScopes := parsed["supportedScopes"]
	require.True(t, hasScopes, "supportedScopes must exist in JSON output")
	scopeArr, ok := scopes.([]interface{})
	require.True(t, ok, "supportedScopes must be a JSON array")
	assert.True(t, len(scopeArr) > 0, "supportedScopes should have at least one scope")
}

// V5: DescribeGlobalObject must include associateEntityType, associateParentEntity
func TestDescribeGlobalObject_AssociateFields(t *testing.T) {
	obj := DescribeGlobalObject{
		Name:                  "Account",
		AssociateEntityType:   nil,
		AssociateParentEntity: nil,
		Urls:                  map[string]string{},
	}

	data, err := json.Marshal(obj)
	require.NoError(t, err)

	var parsed map[string]interface{}
	err = json.Unmarshal(data, &parsed)
	require.NoError(t, err)

	_, hasAET := parsed["associateEntityType"]
	require.True(t, hasAET, "associateEntityType must exist in DescribeGlobalObject JSON")

	_, hasAPE := parsed["associateParentEntity"]
	require.True(t, hasAPE, "associateParentEntity must exist in DescribeGlobalObject JSON")
}

// V6: displayLocationInDecimal and queryByDistance must be true only for location fields
func TestSObjectField_LocationConditionalFields(t *testing.T) {
	t.Run("location field", func(t *testing.T) {
		sfType := mapFieldTypeToSF(fm("location"))
		assert.Equal(t, "location", sfType)
		// Handler sets: DisplayLocationInDecimal: sfType == "location"
		assert.True(t, sfType == "location", "location type should set displayLocationInDecimal=true")
	})

	t.Run("non-location field", func(t *testing.T) {
		sfType := mapFieldTypeToSF(fm("string"))
		assert.Equal(t, "string", sfType)
		assert.False(t, sfType == "location", "non-location type should set displayLocationInDecimal=false")
	})
}
