package packs

import (
	"context"
	"encoding/json"
	"os"
	"strings"
	"testing"

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

// fakeSchemaLookup implements schemaLookup for validator tests.
type fakeSchemaLookup struct {
	schemas map[string]*models.ObjectMetadata
}

func (f *fakeSchemaLookup) GetSchema(_ context.Context, apiName string) *models.ObjectMetadata {
	return f.schemas[apiName]
}

func TestActionDef_AppliesToSchema(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name   string
		action standardActionDef
		schema string
		want   bool
	}{
		{
			name:   "no filters — applies to all",
			action: standardActionDef{Name: "Edit"},
			schema: "Lead",
			want:   true,
		},
		{
			name:   "ObjectAPIName match",
			action: standardActionDef{Name: "CreateQuote", ObjectAPIName: "Opportunity"},
			schema: "Opportunity",
			want:   true,
		},
		{
			name:   "ObjectAPIName mismatch",
			action: standardActionDef{Name: "CreateQuote", ObjectAPIName: "Opportunity"},
			schema: "Lead",
			want:   false,
		},
		{
			name:   "ObjectAPIName is case-insensitive",
			action: standardActionDef{Name: "CreateQuote", ObjectAPIName: "opportunity"},
			schema: "Opportunity",
			want:   true,
		},
		{
			name:   "AppliesTo allowlist hit",
			action: standardActionDef{Name: "ChangeOwner", AppliesTo: []string{"Lead", "Account"}},
			schema: "Lead",
			want:   true,
		},
		{
			name:   "AppliesTo allowlist miss",
			action: standardActionDef{Name: "ChangeOwner", AppliesTo: []string{"Lead", "Account"}},
			schema: "User",
			want:   false,
		},
		{
			name:   "AppliesTo is case-insensitive",
			action: standardActionDef{Name: "ChangeOwner", AppliesTo: []string{"lead"}},
			schema: "Lead",
			want:   true,
		},
		{
			name:   "ObjectAPIName takes precedence over AppliesTo",
			action: standardActionDef{Name: "Foo", ObjectAPIName: "Quote", AppliesTo: []string{"Lead"}},
			schema: "Lead",
			want:   false,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := tc.action.appliesToSchema(tc.schema)
			assert.Equal(t, tc.want, got)
		})
	}
}

// TestStandardActionsJSON_ChangeOwnerPresent is the regression guard that
// ensures the embedded config actually contains the ChangeOwner action
// seeded for the right allowlist.
func TestStandardActionsJSON_ChangeOwnerPresent(t *testing.T) {
	var actions []standardActionDef
	err := json.Unmarshal(standardActionsJSON, &actions)
	assert.NoError(t, err)

	var changeOwner *standardActionDef
	for i := range actions {
		if actions[i].Name == "ChangeOwner" {
			changeOwner = &actions[i]
			break
		}
	}
	if !assert.NotNil(t, changeOwner, "ChangeOwner entry missing from standard_actions.json") {
		return
	}
	assert.Equal(t, "ChangeOwner", changeOwner.Type)
	assert.Equal(t, "Change Owner", changeOwner.Label)
	assert.Empty(t, changeOwner.ObjectAPIName, "ChangeOwner must use AppliesTo, not ObjectAPIName")

	// Expected allowlist — the 10 objects that ship with OwnerId in Salesforce.
	expected := []string{"Lead", "Account", "Contact", "Opportunity", "Case", "Campaign", "Contract", "Order", "Task", "Event"}
	assert.ElementsMatch(t, expected, changeOwner.AppliesTo)

	// System tables must NOT be in the allowlist.
	for _, name := range changeOwner.AppliesTo {
		assert.False(t,
			name == "User" || name == "Group" || name == "Queue",
			"system table %q must not be in ChangeOwner allowlist", name)
	}

	// Visibility condition belt-and-braces.
	vc, _ := changeOwner.Config["visibilityCondition"].(string)
	assert.Contains(t, vc, "OwnerId")
}

func TestStandardActionsJSON_OpportunityClonePresent(t *testing.T) {
	var actions []standardActionDef
	err := json.Unmarshal(standardActionsJSON, &actions)
	assert.NoError(t, err)

	var cloneAction *standardActionDef
	for i := range actions {
		if actions[i].Name == "Clone" && actions[i].ObjectAPIName == "Opportunity" {
			cloneAction = &actions[i]
			break
		}
	}
	if !assert.NotNil(t, cloneAction, "Opportunity Clone entry missing from standard_actions.json") {
		return
	}

	assert.Equal(t, "Standard", cloneAction.Type)
	assert.Equal(t, "Clone", cloneAction.Label)
	assert.Equal(t, "Opportunity", cloneAction.ObjectAPIName)
	assert.Equal(t, "Copy", cloneAction.Icon)
}

func TestStandardActionsJSON_CreateContactPresent(t *testing.T) {
	var actions []standardActionDef
	err := json.Unmarshal(standardActionsJSON, &actions)
	assert.NoError(t, err)

	var createContact *standardActionDef
	for i := range actions {
		if actions[i].Name == "CreateContact" {
			createContact = &actions[i]
			break
		}
	}
	if !assert.NotNil(t, createContact, "CreateContact entry missing from standard_actions.json") {
		return
	}

	assert.Equal(t, "NavigateToNewRecord", createContact.Type)
	assert.Equal(t, "New Contact", createContact.Label)
	assert.Equal(t, "Account", createContact.ObjectAPIName)
	assert.Equal(t, "Contact", createContact.TargetObject)
	assert.Equal(t, "AccountId", createContact.Config["lookupField"])

	_, hasPrefillMappings := createContact.Config["prefillMappings"]
	assert.False(t, hasPrefillMappings, "CreateContact should rely on lookupField only for Account pre-linking")
}

func TestValidateStandardActionFieldMappings_HappyPath(t *testing.T) {
	schema := &fakeSchemaLookup{schemas: map[string]*models.ObjectMetadata{
		"Opportunity": {APIName: "Opportunity", FieldsMap: map[string]*models.FieldMetadata{
			"Name": {APIName: "Name"}, "AccountId": {APIName: "AccountId"}, "ContractId": {APIName: "ContractId"},
		}},
		"Quote": {APIName: "Quote", FieldsMap: map[string]*models.FieldMetadata{
			"Name": {APIName: "Name"}, "AccountId": {APIName: "AccountId"}, "Applicable_Law__c": {APIName: "Applicable_Law__c"},
		}},
		"Contract": {APIName: "Contract", FieldsMap: map[string]*models.FieldMetadata{
			"Applicable_Law__c": {APIName: "Applicable_Law__c"},
		}},
	}}
	action := standardActionDef{
		Name:          "CreateQuote",
		Type:          constants.ActionTypeCreateRecord,
		ObjectAPIName: "Opportunity",
		TargetObject:  "Quote",
		Config: map[string]interface{}{
			"workflow": map[string]interface{}{
				"fieldMappings": map[string]interface{}{
					"Name":              "Name",
					"AccountId":         "AccountId",
					"Applicable_Law__c": "Contract.Applicable_Law__c",
				},
			},
		},
	}
	warnings := validateStandardActionFieldMappings(context.Background(), action, schema)
	assert.Empty(t, warnings)
}

func TestValidateStandardActionFieldMappings_LowercaseMetadataFieldMap(t *testing.T) {
	schema := &fakeSchemaLookup{schemas: map[string]*models.ObjectMetadata{
		"Quote": {APIName: "Quote", FieldsMap: map[string]*models.FieldMetadata{
			"contractid":               {APIName: "ContractId"},
			"service_start_date__c":    {APIName: "Service_Start_Date__c"},
			"acv_amount_do_not_use__c": {APIName: "ACV_Amount_do_not_use__c"},
		}},
		"Order": {APIName: "Order", FieldsMap: map[string]*models.FieldMetadata{
			"effectivedate":            {APIName: "EffectiveDate"},
			"acv_amount_do_not_use__c": {APIName: "ACV_Amount_do_not_use__c"},
			"applicable_law__c":        {APIName: "Applicable_Law__c"},
			"pingcap_revrec_entity__c": {APIName: "PingCAP_RevRec_Entity__c"},
		}},
		"Contract": {APIName: "Contract", FieldsMap: map[string]*models.FieldMetadata{
			"applicable_law__c":             {APIName: "Applicable_Law__c"},
			"pingcap_contracting_entity__c": {APIName: "PingCAP_Contracting_Entity__c"},
		}},
	}}
	action := standardActionDef{
		Name:          "CreateOrder",
		Type:          constants.ActionTypeCreateRecord,
		ObjectAPIName: "Quote",
		TargetObject:  "Order",
		Config: map[string]interface{}{
			"workflow": map[string]interface{}{
				"fieldMappings": map[string]interface{}{
					"EffectiveDate":            "Service_Start_Date__c",
					"ACV_Amount_do_not_use__c": "ACV_Amount_do_not_use__c",
					"Applicable_Law__c":        "Contract.Applicable_Law__c",
					"PingCAP_RevRec_Entity__c": "Contract.PingCAP_Contracting_Entity__c",
				},
			},
		},
	}
	warnings := validateStandardActionFieldMappings(context.Background(), action, schema)
	assert.Empty(t, warnings)
}

func TestValidateStandardActionFieldMappings_UnknownRelatedSourceField(t *testing.T) {
	schema := &fakeSchemaLookup{schemas: map[string]*models.ObjectMetadata{
		"Quote": {APIName: "Quote", FieldsMap: map[string]*models.FieldMetadata{
			"ContractId": {APIName: "ContractId"},
		}},
		"Order": {APIName: "Order", FieldsMap: map[string]*models.FieldMetadata{
			"Applicable_Law__c": {APIName: "Applicable_Law__c"},
		}},
		"Contract": {APIName: "Contract", FieldsMap: map[string]*models.FieldMetadata{}},
	}}
	action := standardActionDef{
		Name:          "CreateOrder",
		Type:          constants.ActionTypeCreateRecord,
		ObjectAPIName: "Quote",
		TargetObject:  "Order",
		Config: map[string]interface{}{
			"workflow": map[string]interface{}{
				"fieldMappings": map[string]interface{}{
					"Applicable_Law__c": "Contract.Applicable_Law__c",
				},
			},
		},
	}
	warnings := validateStandardActionFieldMappings(context.Background(), action, schema)
	assert.Len(t, warnings, 1)
	assert.Contains(t, warnings[0], "Applicable_Law__c")
	assert.Contains(t, warnings[0], "Contract")
}

func TestValidateStandardActionFieldMappings_UnknownSourceField(t *testing.T) {
	schema := &fakeSchemaLookup{schemas: map[string]*models.ObjectMetadata{
		"Opportunity": {APIName: "Opportunity", FieldsMap: map[string]*models.FieldMetadata{
			"Name": {APIName: "Name"},
		}},
		"Quote": {APIName: "Quote", FieldsMap: map[string]*models.FieldMetadata{
			"Name": {APIName: "Name"},
		}},
	}}
	action := standardActionDef{
		Name:          "CreateQuote",
		Type:          constants.ActionTypeCreateRecord,
		ObjectAPIName: "Opportunity",
		TargetObject:  "Quote",
		Config: map[string]interface{}{
			"workflow": map[string]interface{}{
				"fieldMappings": map[string]interface{}{
					"Name": "Typo__c",
				},
			},
		},
	}
	warnings := validateStandardActionFieldMappings(context.Background(), action, schema)
	assert.Len(t, warnings, 1)
	assert.Contains(t, warnings[0], "Typo__c")
	assert.Contains(t, warnings[0], "Opportunity")
}

func TestValidateStandardActionFieldMappings_UnknownTargetField(t *testing.T) {
	schema := &fakeSchemaLookup{schemas: map[string]*models.ObjectMetadata{
		"Opportunity": {APIName: "Opportunity", FieldsMap: map[string]*models.FieldMetadata{
			"Name": {APIName: "Name"},
		}},
		"Quote": {APIName: "Quote", FieldsMap: map[string]*models.FieldMetadata{}},
	}}
	action := standardActionDef{
		Name:          "CreateQuote",
		Type:          constants.ActionTypeCreateRecord,
		ObjectAPIName: "Opportunity",
		TargetObject:  "Quote",
		Config: map[string]interface{}{
			"workflow": map[string]interface{}{
				"fieldMappings": map[string]interface{}{
					"NotOnQuote__c": "Name",
				},
			},
		},
	}
	warnings := validateStandardActionFieldMappings(context.Background(), action, schema)
	assert.Len(t, warnings, 1)
	assert.Contains(t, warnings[0], "NotOnQuote__c")
	assert.Contains(t, warnings[0], "Quote")
}

func TestValidateStandardActionFieldMappings_SkipsNonCreateRecord(t *testing.T) {
	schema := &fakeSchemaLookup{schemas: map[string]*models.ObjectMetadata{}}
	action := standardActionDef{
		Name: "Edit",
		Type: "Edit",
		Config: map[string]interface{}{
			"workflow": map[string]interface{}{
				"fieldMappings": map[string]interface{}{"X": "Y"},
			},
		},
	}
	warnings := validateStandardActionFieldMappings(context.Background(), action, schema)
	assert.Nil(t, warnings)
}

// TestValidateStandardActionFieldMappings_ChildCopyIgnored pins the
// Phase 2b contract: the schema-validation pass MUST NOT inspect
// workflow.childCopy entries. That subtree is a retired surface;
// validating its shape would suggest it's live (reviewer finding:
// fake metadata surface). The authoring-time rejection in
// actionvalidate.StrictChildCopyInConfig is the single authority for
// the surface's retired status.
func TestValidateStandardActionFieldMappings_ChildCopyIgnored(t *testing.T) {
	schema := &fakeSchemaLookup{schemas: map[string]*models.ObjectMetadata{
		"Opportunity":         {APIName: "Opportunity", FieldsMap: map[string]*models.FieldMetadata{"Id": {APIName: "Id"}}},
		"Quote":               {APIName: "Quote", FieldsMap: map[string]*models.FieldMetadata{"Id": {APIName: "Id"}}},
		"OpportunityLineItem": {APIName: "OpportunityLineItem", FieldsMap: map[string]*models.FieldMetadata{"Quantity": {APIName: "Quantity"}}},
		"quote_product__c":    {APIName: "quote_product__c", FieldsMap: map[string]*models.FieldMetadata{}},
	}}
	// Deliberately broken childCopy: targets a field that doesn't exist
	// on quote_product__c. If the validator still walked childCopy,
	// this would surface as a warning. Phase 2b says it must NOT —
	// shape validation of a retired surface is itself a bug (suggests
	// liveness).
	action := standardActionDef{
		Name:          "CreateQuote",
		Type:          constants.ActionTypeCreateRecord,
		ObjectAPIName: "Opportunity",
		TargetObject:  "Quote",
		Config: map[string]interface{}{
			"workflow": map[string]interface{}{
				"childCopy": []interface{}{
					map[string]interface{}{
						"sourceObject": "OpportunityLineItem",
						"targetObject": "quote_product__c",
						"fieldMappings": map[string]interface{}{
							"Quantity__c": "Quantity", // unknown target field
						},
					},
				},
			},
		},
	}
	warnings := validateStandardActionFieldMappings(context.Background(), action, schema)
	assert.Empty(t, warnings, "validator must NOT surface childCopy-shape warnings — the surface is retired")
}

// TestStandardActionsJSON_CreateQuotePresent validates the CreateQuote action
// definition shipped in standard_actions.json. The action now opens the Quote
// new-record form so required fields can be filled before save, while the
// standard field-mapping metadata remains available for tenant overlay merge.
func TestStandardActionsJSON_CreateQuotePresent(t *testing.T) {
	var actions []standardActionDef
	err := json.Unmarshal(standardActionsJSON, &actions)
	assert.NoError(t, err)

	var createQuote *standardActionDef
	for i := range actions {
		if actions[i].Name == "CreateQuote" {
			createQuote = &actions[i]
			break
		}
	}
	if !assert.NotNil(t, createQuote, "CreateQuote entry missing from standard_actions.json") {
		return
	}

	assert.Equal(t, "NavigateToNewRecord", createQuote.Type)
	assert.Equal(t, "Opportunity", createQuote.ObjectAPIName)
	assert.Equal(t, "Quote", createQuote.TargetObject)

	prefillMappings, ok := createQuote.Config["prefillMappings"].(map[string]interface{})
	if !assert.True(t, ok, "CreateQuote must have prefillMappings for new-record navigation") {
		return
	}
	assert.Equal(t, "Name", prefillMappings["Name"])
	assert.Equal(t, "AccountId", prefillMappings["AccountId"])
	assert.Equal(t, "Discount", prefillMappings["Discount"])

	// Verify workflow config is still present for tenant overlay merge.
	workflow, ok := createQuote.Config["workflow"].(map[string]interface{})
	if !assert.True(t, ok, "CreateQuote must have a workflow config") {
		return
	}

	// Field mappings: only SF-standard fields remain in standard_actions.json
	// per issue #1609. Tenant __c mappings (NetFees__c, Service_*, etc.) and
	// the entire childCopy (which targets tenant quote_product__c) are layered
	// in at apply time from tenant_actions.json — validated in
	// tenant_actions_test.go TestTenantActionsJSON_CreateQuoteOverlay.
	mappings, _ := workflow["fieldMappings"].(map[string]interface{})
	assert.Equal(t, "Name", mappings["Name"])
	assert.Equal(t, "AccountId", mappings["AccountId"])
	assert.Equal(t, "Discount", mappings["Discount"])
	for _, tenantKey := range []string{"NetFees__c", "Net_Fees__c", "Product_Family__c", "Service_Start_Date__c", "Service_End_Date__c", "Overall_Start_Date__c", "Overall_End_Date__c"} {
		_, hasTenant := mappings[tenantKey]
		assert.Falsef(t, hasTenant, "standard_actions.json CreateQuote must not carry tenant __c mapping %q", tenantKey)
	}

	// No childCopy in standard — the tenant overlay owns quote_product__c.
	_, hasChildCopy := workflow["childCopy"]
	assert.False(t, hasChildCopy, "standard_actions.json CreateQuote must not carry childCopy (targets tenant quote_product__c)")

	// Lookup field wires Quote back to Opportunity.
	assert.Equal(t, "OpportunityId", createQuote.Config["lookupField"])
}

// TestStandardActionsJSON_OpportunityEditProductsPresent validates the
// Opportunity EditProducts action — NavigateToRelatedList targeting the
// SF-standard OpportunityLineItem child. This is a standard action; the
// Quote-scoped EditProducts (targeting tenant quote_product__c) lives in
// tenant_actions.json and is validated separately.
func TestStandardActionsJSON_OpportunityEditProductsPresent(t *testing.T) {
	var actions []standardActionDef
	err := json.Unmarshal(standardActionsJSON, &actions)
	assert.NoError(t, err)

	var editProducts *standardActionDef
	for i := range actions {
		if actions[i].Name == "EditProducts" && actions[i].ObjectAPIName == "Opportunity" {
			editProducts = &actions[i]
			break
		}
	}
	if !assert.NotNil(t, editProducts, "Opportunity EditProducts entry missing from standard_actions.json") {
		return
	}

	assert.Equal(t, "Edit Products", editProducts.Label)
	assert.Equal(t, "NavigateToRelatedList", editProducts.Type)
	assert.Equal(t, "Opportunity", editProducts.ObjectAPIName)

	assert.Equal(t, "OpportunityLineItem", editProducts.Config["targetObject"])
	assert.Equal(t, "OpportunityId", editProducts.Config["lookupField"])
}

// Quote-scoped EditProducts (targets tenant quote_product__c) and
// UploadArchivedOF / UploadArchivedContract (tenant fullActions) are
// validated in tenant_metadata_test.go — per issue #1609, they live in
// tenant_actions.json and must not re-enter the standard seed.
// TestStandardActionsJSON_CreateOrderPresent is the regression guard for the
// Quote → Order creation action. It uses NavigateToNewRecord (not
// CreateRecord) so the existing RecordTypePicker and Order form run on the
// target route — parity with the "+ New" button on the Quote's Orders
// related list. prefillMappings carry Quote field values into the form as
// URL query params, consumed by ObjectView's createInitialData memo.
func TestStandardActionsJSON_CreateOrderPresent(t *testing.T) {
	var actions []standardActionDef
	err := json.Unmarshal(standardActionsJSON, &actions)
	assert.NoError(t, err)

	var createOrder *standardActionDef
	for i := range actions {
		if actions[i].Name == "CreateOrder" {
			createOrder = &actions[i]
			break
		}
	}
	if !assert.NotNil(t, createOrder, "CreateOrder entry missing from standard_actions.json") {
		return
	}

	assert.Equal(t, "NavigateToNewRecord", createOrder.Type)
	assert.Equal(t, "Create Order", createOrder.Label)
	assert.Equal(t, "Quote", createOrder.ObjectAPIName)
	assert.Equal(t, "Order", createOrder.TargetObject)

	config := createOrder.Config
	assert.Equal(t, "QuoteId", config["lookupField"])
	assert.Equal(t,
		"record.Status == 'Accepted' && record.AccountId != null",
		config["visibilityCondition"])

	prefill, ok := config["prefillMappings"].(map[string]interface{})
	if !assert.True(t, ok, "prefillMappings required") {
		return
	}
	// Standard prefill surface: SF-standard fields only per issue #1609.
	// Tenant __c mappings (Product_Family__c, Tax__c, PingCAP_*, etc.) are
	// layered in at apply time from tenant_actions.json — validated in
	// TestTenantActionsJSON_CreateOrderOverlay.
	expected := map[string]string{
		// Identity / lookups
		"AccountId":     "AccountId",
		"ContractId":    "ContractId",
		"OpportunityId": "OpportunityId",
		"Pricebook2Id":  "Pricebook2Id",
		// Currency + totals
		"CurrencyIsoCode": "CurrencyIsoCode",
		"TotalAmount":     "GrandTotal",
		// Description
		"Description": "Description",
		// Contact + ownership — mirror Salesforce behavior where the new Order
		// inherits the Quote's bill-to contact and owner rather than resetting
		// to the creating user.
		"BillToContactId": "ContactId",
		"OwnerId":         "OwnerId",
		// Billing address (dot-path from Account — Quote.Billing* is 0%
		// populated in production; Account.Billing* is ~72% populated).
		"BillingStreet":     "Account.BillingStreet",
		"BillingCity":       "Account.BillingCity",
		"BillingState":      "Account.BillingState",
		"BillingPostalCode": "Account.BillingPostalCode",
		"BillingCountry":    "Account.BillingCountry",
	}
	for targetField, expectedSource := range expected {
		assert.Equalf(t, expectedSource, prefill[targetField], "prefillMapping for %s", targetField)
	}
	assert.Equalf(t, len(expected), len(prefill),
		"prefillMappings size mismatch — expected %d entries, got %d; update this test when adding/removing mappings",
		len(expected), len(prefill))

	// Server-side workflow config must not be present — the action is
	// navigation, not record creation.
	_, hasWorkflow := config["workflow"]
	assert.False(t, hasWorkflow, "NavigateToNewRecord action must not carry a server-side workflow config")
}

// TestStandardActionsJSON_CreateContractPresent is the regression guard for the
// Account → Contract creation action. Pattern mirrors CreateOrder: a
// NavigateToNewRecord action that opens the target create form with
// prefillMappings seeded from the parent Account.
func TestStandardActionsJSON_CreateContractPresent(t *testing.T) {
	var actions []standardActionDef
	err := json.Unmarshal(standardActionsJSON, &actions)
	assert.NoError(t, err)

	var createContract *standardActionDef
	for i := range actions {
		if actions[i].Name == "CreateContract" {
			createContract = &actions[i]
			break
		}
	}
	if !assert.NotNil(t, createContract, "CreateContract entry missing from standard_actions.json") {
		return
	}

	assert.Equal(t, "NavigateToNewRecord", createContract.Type)
	assert.Equal(t, "Create Contract", createContract.Label)
	assert.Equal(t, "Account", createContract.ObjectAPIName)
	assert.Equal(t, "Contract", createContract.TargetObject)

	config := createContract.Config
	assert.Equal(t, "AccountId", config["lookupField"])

	prefill, ok := config["prefillMappings"].(map[string]interface{})
	if !assert.True(t, ok, "prefillMappings required") {
		return
	}
	// SF-standard prefill surface only per issue #1609. Tenant __c mappings
	// (PingCAP_Contracting_Entity__c, Region__c) layer in at apply time from
	// tenant_actions.json — validated in TestTenantActionsJSON_CreateContractOverlay.
	expected := map[string]string{
		"OwnerId":           "OwnerId",
		"CurrencyIsoCode":   "CurrencyIsoCode",
		"Description":       "Description",
		"BillingStreet":     "BillingStreet",
		"BillingCity":       "BillingCity",
		"BillingState":      "BillingState",
		"BillingPostalCode": "BillingPostalCode",
		"BillingCountry":    "BillingCountry",
	}
	for targetField, expectedSource := range expected {
		assert.Equalf(t, expectedSource, prefill[targetField], "prefillMapping for %s", targetField)
	}
	assert.Equalf(t, len(expected), len(prefill),
		"prefillMappings size mismatch — expected %d entries, got %d; update this test when adding/removing mappings",
		len(expected), len(prefill))

	_, hasWorkflow := config["workflow"]
	assert.False(t, hasWorkflow, "NavigateToNewRecord action must not carry a server-side workflow config")
}

// TestTenantSpecJSON_RequiredPicklistsHaveDefaults guards against the
// recurring "validation error on field 'X': required field(s) missing" bug
// class. Concrete cases: #1157 (Order.Status/StatusCode), this PR
// (Contract.Status/StatusCode and Opportunity.StageName).
//
// Root cause: when a picklist field ships required=true but no default,
// platform_tenant.go:200-203 hydrates DefaultValue=nil, and the
// create-time validator at sobject_records.go:437 refuses to skip the
// missing field (its skip clause fires only when DefaultValue is
// non-empty). Every user save then fails with "required field(s)
// missing: <name>".
//
// Scope: picklists only. Non-picklist required fields (Name, LastName,
// AccountId, etc.) legitimately need user input and must NOT be silently
// defaulted — auto-defaulting them would mask real validation bugs.
// Restricting to picklist=true keeps the guard focused on fields whose
// values come from a closed set already declared in the spec.
func TestTenantSpecJSON_RequiredPicklistsHaveDefaults(t *testing.T) {
	type specField struct {
		APIName  string          `json:"apiName"`
		Type     string          `json:"type"`
		Required bool            `json:"required"`
		Default  json.RawMessage `json:"default,omitempty"`
	}
	type specObject struct {
		APIName string      `json:"apiName"`
		Fields  []specField `json:"fields"`
	}
	specBytes, err := os.ReadFile("../tenants/pingcap/tenant_objects_spec.json")
	if !assert.NoError(t, err, "read tenant_objects_spec.json from tenant quarantine") {
		return
	}
	var objects []specObject
	err = json.Unmarshal(specBytes, &objects)
	if !assert.NoError(t, err, "tenant_objects_spec.json must parse") {
		return
	}

	for _, obj := range objects {
		for _, f := range obj.Fields {
			if f.Type != "picklist" || !f.Required {
				continue
			}
			assert.NotEmptyf(t, f.Default,
				"%s.%s is a required picklist with no default — create will fail "+
					"with 'required field(s) missing: %s' (see #1157)",
				obj.APIName, f.APIName, f.APIName)
		}
	}
}

// TestBuildStandardActionsUpsertSQL_PreservesAdminSoftDelete guards against the
// regression where the bootstrap UPSERT reset IsDeleted = 0 on every restart,
// undoing admin soft-deletes of shipped actions. The SQL must update shipped
// config columns (Label/Type/Icon/TargetObject/Config/LastModifiedDate) but
// must NOT touch IsDeleted.
func TestBuildStandardActionsUpsertSQL_PreservesAdminSoftDelete(t *testing.T) {
	sql := buildStandardActionsUpsertSQL([]string{"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"})

	// Must still refresh shipped config columns on duplicate-key.
	assert.Contains(t, sql, "ON DUPLICATE KEY UPDATE")
	assert.Contains(t, sql, "`Label` = VALUES(`Label`)")
	assert.Contains(t, sql, "`Config` = VALUES(`Config`)")
	assert.Contains(t, sql, "`LastModifiedDate` = VALUES(`LastModifiedDate`)")

	// Must NOT reset IsDeleted — admin soft-deletes must persist across restarts.
	updateClause := sql[strings.Index(sql, "ON DUPLICATE KEY UPDATE"):]
	assert.NotContains(t, updateClause, "IsDeleted",
		"bootstrap UPSERT must not touch IsDeleted; admin soft-deletes must persist")
}
