package rest

import (
	"bytes"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/pkg/auth"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// ---------------------------------------------------------------------------
// ChangeOwner — request-shape validation (pure handler logic, no DB needed)
// ---------------------------------------------------------------------------

func newTestHandler() *StandardActionsHandler {
	return NewStandardActionsHandler(nil, nil, nil, nil, nil, "")
}

func withTestUser(c *gin.Context) {
	c.Set(constants.ContextKeyUser, auth.UserSession{
		ID:    "user-test",
		Name:  "Test User",
		Email: "test@example.com",
	})
}

func TestChangeOwner_Unauthenticated(t *testing.T) {
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest(http.MethodPost, "/actions/standard/changeOwner",
		bytes.NewReader([]byte(`{"inputs":[{"recordId":"r1","objectApiName":"Lead","newOwnerId":"u1"}]}`)))
	c.Request.Header.Set("Content-Type", "application/json")

	assert.Panics(t, func() { newTestHandler().ChangeOwner(c) })
}

func TestChangeOwner_InvalidJSON(t *testing.T) {
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest(http.MethodPost, "/actions/standard/changeOwner",
		bytes.NewReader([]byte(`{not-json`)))
	c.Request.Header.Set("Content-Type", "application/json")
	withTestUser(c)

	newTestHandler().ChangeOwner(c)

	assert.Equal(t, http.StatusBadRequest, w.Code)
}

func TestChangeOwner_EmptyInputs(t *testing.T) {
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest(http.MethodPost, "/actions/standard/changeOwner",
		bytes.NewReader([]byte(`{"inputs":[]}`)))
	c.Request.Header.Set("Content-Type", "application/json")
	withTestUser(c)

	newTestHandler().ChangeOwner(c)

	assert.Equal(t, http.StatusBadRequest, w.Code)
}

// When individual inputs omit required fields the handler must return 200 OK
// with per-input error records — matching SF Invocable Action batch semantics
// used by ConvertLead.
func TestChangeOwner_PerInputValidationErrors(t *testing.T) {
	cases := []struct {
		name string
		body string
		msg  string
	}{
		{"missing_recordId", `{"inputs":[{"objectApiName":"Lead","newOwnerId":"u1"}]}`, "recordId"},
		{"missing_objectApiName", `{"inputs":[{"recordId":"r1","newOwnerId":"u1"}]}`, "objectApiName"},
		{"missing_newOwnerId", `{"inputs":[{"recordId":"r1","objectApiName":"Lead"}]}`, "newOwnerId"},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			w := httptest.NewRecorder()
			c, _ := gin.CreateTestContext(w)
			c.Request = httptest.NewRequest(http.MethodPost, "/actions/standard/changeOwner",
				bytes.NewReader([]byte(tc.body)))
			c.Request.Header.Set("Content-Type", "application/json")
			withTestUser(c)

			newTestHandler().ChangeOwner(c)

			assert.Equal(t, http.StatusOK, w.Code)

			var got []ChangeOwnerResponse
			err := json.Unmarshal(w.Body.Bytes(), &got)
			assert.NoError(t, err)
			assert.Len(t, got, 1)
			assert.False(t, got[0].IsSuccess)
			assert.Len(t, got[0].Errors, 1)
			errEntry, _ := got[0].Errors[0].(map[string]interface{})
			assert.Contains(t, errEntry["message"], tc.msg)
			// Required-field failures should map to FIELD_INTEGRITY_EXCEPTION
			// via sfErrorCode → provides SF-shaped statusCode.
			assert.Equal(t, "FIELD_INTEGRITY_EXCEPTION", errEntry["statusCode"])
		})
	}
}

// Batch with multiple invalid inputs returns one result per input in order.
// Each entry reports its own validation failure — matching the per-input
// error semantics used by ConvertLead.
func TestChangeOwner_BatchPerInputErrors(t *testing.T) {
	body := `{"inputs":[
		{"recordId":"","objectApiName":"Lead","newOwnerId":"u1"},
		{"recordId":"r2","objectApiName":"Lead","newOwnerId":""}
	]}`
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest(http.MethodPost, "/actions/standard/changeOwner",
		bytes.NewReader([]byte(body)))
	c.Request.Header.Set("Content-Type", "application/json")
	withTestUser(c)

	newTestHandler().ChangeOwner(c)

	assert.Equal(t, http.StatusOK, w.Code)
	var got []ChangeOwnerResponse
	if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
		t.Fatalf("unmarshal response: %v", err)
	}
	assert.Len(t, got, 2)
	assert.False(t, got[0].IsSuccess)
	assert.False(t, got[1].IsSuccess)

	err0, _ := got[0].Errors[0].(map[string]interface{})
	assert.Contains(t, err0["message"], "recordId")
	err1, _ := got[1].Errors[0].(map[string]interface{})
	assert.Contains(t, err1["message"], "newOwnerId")
	assert.Equal(t, "r2", got[1].OutputValues["recordId"])
}

// ---------------------------------------------------------------------------
// Helpers: sfErrorCode / recordDisplayName / orFallback
// ---------------------------------------------------------------------------

func TestSFErrorCode_Mapping(t *testing.T) {
	cases := map[string]string{
		"permission denied: cannot transfer":         "INSUFFICIENT_ACCESS",
		"forbidden: blocked by sharing":              "INSUFFICIENT_ACCESS",
		"record Lead not found":                      "NOT_FOUND",
		"newOwnerId does not refer to an accessible": "NOT_FOUND",
		"newOwnerId refers to an inactive user":      "FIELD_INTEGRITY_EXCEPTION",
		"recordId is required":                       "FIELD_INTEGRITY_EXCEPTION",
		"invalid session":                            "FIELD_INTEGRITY_EXCEPTION",
		"db exploded unexpectedly":                   "UNKNOWN_EXCEPTION",
	}
	for msg, want := range cases {
		got := sfErrorCode(errors.New(msg))
		assert.Equal(t, want, got, msg)
	}
}

func TestRecordDisplayName_Fallbacks(t *testing.T) {
	assert.Equal(t, "Acme Corp", recordDisplayName(models.SObject{"Name": "Acme Corp"}))
	assert.Equal(t, "CASE-42", recordDisplayName(models.SObject{"CaseNumber": "CASE-42"}))
	assert.Equal(t, "Follow up", recordDisplayName(models.SObject{"Subject": "Follow up"}))
	assert.Equal(t, "Book title", recordDisplayName(models.SObject{"Title": "Book title"}))
	assert.Equal(t, "00Q001", recordDisplayName(models.SObject{"Id": "00Q001"}))
	assert.Equal(t, "(unknown)", recordDisplayName(models.SObject{}))
}

func TestOrFallback(t *testing.T) {
	assert.Equal(t, "primary", orFallback("primary", "fallback"))
	assert.Equal(t, "fallback", orFallback("", "fallback"))
}

// isTruthy must handle every representation TiDB/MySQL drivers produce
// for BOOLEAN columns — missing a case silently breaks the IsActive gate.
func TestIsTruthy(t *testing.T) {
	cases := []struct {
		name string
		in   interface{}
		want bool
	}{
		{"native true", true, true},
		{"native false", false, false},
		{"int64 one", int64(1), true},
		{"int64 zero", int64(0), false},
		{"int one", 1, true},
		{"int zero", 0, false},
		{"float64 one", float64(1), true},
		{"float64 zero", float64(0), false},
		{"bytes one", []byte("1"), true},
		{"bytes zero", []byte("0"), false},
		{"bytes false (regression: must not be truthy on 'f')", []byte("false"), false},
		{"bytes true", []byte("true"), true},
		{"string true", "true", true},
		{"string one", "1", true},
		{"string zero", "0", false},
		{"nil", nil, false},
		{"unknown struct", struct{}{}, false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			assert.Equal(t, tc.want, isTruthy(tc.in))
		})
	}
}

func TestCloneSObject(t *testing.T) {
	src := models.SObject{"Id": "x", "Name": "y"}
	dst := cloneSObject(src)
	dst["Name"] = "MUTATED"
	assert.Equal(t, "y", src["Name"], "cloning must not mutate the source map")
	assert.Nil(t, cloneSObject(nil))
}

// ---------------------------------------------------------------------------
// isValidFieldAPIName
// ---------------------------------------------------------------------------

func TestIsValidFieldAPIName(t *testing.T) {
	cases := []struct {
		name  string
		input string
		want  bool
	}{
		{"standard field", "AccountId", true},
		{"custom field", "Foo__c", true},
		{"relationship", "Account__r", true},
		{"single letter", "X", true},
		{"underscore in middle", "Service_Start_Date__c", true},
		{"empty string", "", false},
		{"starts with number", "1Field", false},
		{"contains space", "My Field", false},
		{"contains quote", "Name'", false},
		{"contains equals", "Status=Active", false},
		{"SQL injection attempt", "Id; DROP TABLE", false},
		{"double suffix is valid (underscore chars in body)", "Foo__c__c", true},
		{"just underscore", "_Bad", false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			assert.Equal(t, tc.want, isValidFieldAPIName(tc.input), tc.input)
		})
	}
}

// ---------------------------------------------------------------------------
// decodeCreateRecordActionConfig
// ---------------------------------------------------------------------------

func TestDecodeCreateRecordActionConfig_Empty(t *testing.T) {
	config, err := decodeCreateRecordActionConfig(nil)
	assert.NoError(t, err)
	assert.NotNil(t, config)
	assert.Empty(t, config.LookupField)
	assert.Nil(t, config.Workflow)
}

func TestDecodeCreateRecordActionConfig_FullWorkflow(t *testing.T) {
	raw := map[string]interface{}{
		"lookupField":    "OpportunityId",
		"confirmTitle":   "Create Quote",
		"confirmMessage": "Do you confirm?",
		"workflow": map[string]interface{}{
			"defaultValues": map[string]interface{}{
				"Status": "Pending",
			},
			"fieldMappings": map[string]interface{}{
				"Name":      "Name",
				"AccountId": "AccountId",
			},
			// workflow.childCopy is intentionally included in the raw Config
			// to prove Phase 2's contract: the field has been removed from
			// createRecordWorkflowConfig, so json.Unmarshal silently drops
			// these keys. Legacy Config JSON with residue decodes cleanly;
			// the decoded config exposes no child-copy surface for any
			// caller to execute.
			"childCopy": []interface{}{
				map[string]interface{}{
					"sourceObject":      "OpportunityLineItem",
					"sourceLookupField": "OpportunityId",
					"targetObject":      "quote_product__c",
					"targetLookupField": "Quote__c",
				},
			},
		},
	}

	config, err := decodeCreateRecordActionConfig(raw)
	assert.NoError(t, err)
	assert.Equal(t, "OpportunityId", config.LookupField)
	assert.Equal(t, "Create Quote", config.ConfirmTitle)
	assert.NotNil(t, config.Workflow)
	assert.Equal(t, "Pending", config.Workflow.DefaultValues["Status"])
	assert.Equal(t, "Name", config.Workflow.FieldMappings["Name"])
}

func TestStandardActionsPack_CreateQuoteUsesNavigateToNewRecord(t *testing.T) {
	raw, err := os.ReadFile("../../controlplane/packs/standard_actions.json")
	require.NoError(t, err)

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

	var createQuote map[string]any
	for _, action := range actions {
		if action["name"] == "CreateQuote" {
			createQuote = action
			break
		}
	}

	require.NotNil(t, createQuote, "CreateQuote action must exist in the standard actions pack")
	assert.Equal(t, "NavigateToNewRecord", createQuote["type"])

	config, ok := createQuote["config"].(map[string]any)
	require.True(t, ok)
	assert.Equal(t, "OpportunityId", config["lookupField"])

	prefillMappings, ok := config["prefillMappings"].(map[string]any)
	require.True(t, ok)
	assert.Equal(t, "Name", prefillMappings["Name"])
	assert.Equal(t, "AccountId", prefillMappings["AccountId"])
	assert.Equal(t, "Discount", prefillMappings["Discount"])

	workflow, ok := config["workflow"].(map[string]any)
	require.True(t, ok)
	fieldMappings, ok := workflow["fieldMappings"].(map[string]any)
	require.True(t, ok)
	assert.Equal(t, "Name", fieldMappings["Name"])
	assert.Equal(t, "AccountId", fieldMappings["AccountId"])
	assert.Equal(t, "Discount", fieldMappings["Discount"])
}

// ---------------------------------------------------------------------------
// applyDefaultValues
// ---------------------------------------------------------------------------

func TestApplyDefaultValues(t *testing.T) {
	target := make(models.SObject)
	workflow := &createRecordWorkflowConfig{
		DefaultValues: map[string]interface{}{
			"Status":   "Pending",
			"Priority": "High",
			"IsActive": true,
		},
	}
	applyDefaultValues(target, workflow)

	assert.Equal(t, "Pending", target["Status"])
	assert.Equal(t, "High", target["Priority"])
	assert.Equal(t, true, target["IsActive"])
}

func TestApplyDefaultValues_NilWorkflow(t *testing.T) {
	target := make(models.SObject)
	applyDefaultValues(target, nil)
	assert.Empty(t, target)
}

func TestApplyDefaultValues_TodaySentinel(t *testing.T) {
	target := make(models.SObject)
	workflow := &createRecordWorkflowConfig{
		DefaultValues: map[string]interface{}{
			"EffectiveDate": "__TODAY__",
			"Status":        "Draft",
		},
	}
	before := time.Now().UTC().Format("2006-01-02")
	applyDefaultValues(target, workflow)
	after := time.Now().UTC().Format("2006-01-02")

	assert.Equal(t, "Draft", target["Status"])
	got := target["EffectiveDate"]
	assert.Contains(t, []string{before, after}, got, "__TODAY__ sentinel should substitute today's UTC date")
}

// ---------------------------------------------------------------------------

func TestCreateOrderWorkflowDefaultsAndMappings(t *testing.T) {
	source := models.SObject{
		"AccountId":                "account-1",
		"ContractId":               "contract-1",
		"OpportunityId":            "opportunity-1",
		"Pricebook2Id":             "pricebook-1",
		"Description":              "source description",
		"Service_Start_Date__c":    "2026-05-01",
		"Service_End_Date__c":      "2027-04-30",
		"Overall_Start_Date__c":    "2026-05-01",
		"Overall_End_Date__c":      "2027-04-30",
		"CurrencyIsoCode":          "USD",
		"Product_Family__c":        "TiDB Cloud",
		"Payment_Term__c":          "30",
		"Payment_Type__c":          "Monthly in Arrears",
		"Tax__c":                   0.0,
		"NetFees__c":               420000.0,
		"ACV_Amount_do_not_use__c": 0.0,
		"Partner_Name__c":          "partner-1",
		"GrandTotal":               12345.67,
	}
	target := make(models.SObject)
	workflow := &createRecordWorkflowConfig{
		DefaultValues: map[string]interface{}{
			"Archived__c":                           false,
			"EffectiveDate":                         todaySentinel,
			"RecordTypeId":                          "012xx000000rtAAA",
			"Status":                                "Draft",
			"Type":                                  "New",
			"Order_is_Terminated__c":                false,
			"Order_to_be_deleted__c":                false,
			"Order_Expiration_Alert__c":             false,
			"Contain_Earlier_Termination_Clause__c": false,
			"Is_Stripe__c":                          false,
			"OP_Paid_via_Marketplace__c":            false,
			"Cash_Collected_exc_tax_Amount__c":      0,
			"Cash_Collected_USD_exc_tax__c":         0,
			"Payable_Amount_USD_exc_tax__c":         0,
		},
		FieldMappings: map[string]interface{}{
			"AccountId":                "AccountId",
			"ContractId":               "ContractId",
			"OpportunityId":            "OpportunityId",
			"Pricebook2Id":             "Pricebook2Id",
			"Description":              "Description",
			"EffectiveDate":            "Service_Start_Date__c",
			"EndDate":                  "Service_End_Date__c",
			"Order_End_Date__c":        "Service_End_Date__c",
			"Overall_Start_Date__c":    "Overall_Start_Date__c",
			"Overall_End_Date__c":      "Overall_End_Date__c",
			"CurrencyIsoCode":          "CurrencyIsoCode",
			"Product_Family__c":        "Product_Family__c",
			"Payment_Term__c":          "Payment_Term__c",
			"Payment_Type__c":          "Payment_Type__c",
			"Tax__c":                   "Tax__c",
			"Net_Fees__c":              "NetFees__c",
			"ACV_Amount_do_not_use__c": "ACV_Amount_do_not_use__c",
			"Partner_Name__c":          "Partner_Name__c",
			"TotalAmount":              "GrandTotal",
		},
	}

	applyDefaultValues(target, workflow)
	applyConfiguredFieldMappingsWithResolver(target, workflow, source, nil)

	assert.Equal(t, false, target["Archived__c"])
	assert.Equal(t, "2026-05-01", target["EffectiveDate"])
	assert.Equal(t, "012xx000000rtAAA", target["RecordTypeId"])
	assert.Equal(t, "Draft", target["Status"])
	assert.Equal(t, "New", target["Type"])
	assert.Equal(t, "account-1", target["AccountId"])
	assert.Equal(t, "contract-1", target["ContractId"])
	assert.Equal(t, "opportunity-1", target["OpportunityId"])
	assert.Equal(t, "pricebook-1", target["Pricebook2Id"])
	assert.Equal(t, "source description", target["Description"])
	assert.Equal(t, "2027-04-30", target["EndDate"])
	assert.Equal(t, "2027-04-30", target["Order_End_Date__c"])
	assert.Equal(t, "2026-05-01", target["Overall_Start_Date__c"])
	assert.Equal(t, "2027-04-30", target["Overall_End_Date__c"])
	assert.Equal(t, "USD", target["CurrencyIsoCode"])
	assert.Equal(t, "TiDB Cloud", target["Product_Family__c"])
	assert.Equal(t, "30", target["Payment_Term__c"])
	assert.Equal(t, "Monthly in Arrears", target["Payment_Type__c"])
	assert.Equal(t, 0.0, target["Tax__c"])
	assert.Equal(t, 420000.0, target["Net_Fees__c"])
	assert.Equal(t, 0.0, target["ACV_Amount_do_not_use__c"])
	assert.Equal(t, "partner-1", target["Partner_Name__c"])
	assert.Equal(t, 12345.67, target["TotalAmount"])
	assert.Equal(t, false, target["Order_is_Terminated__c"])
	assert.Equal(t, false, target["Order_to_be_deleted__c"])
	assert.Equal(t, false, target["Order_Expiration_Alert__c"])
	assert.Equal(t, false, target["Contain_Earlier_Termination_Clause__c"])
	assert.Equal(t, false, target["Is_Stripe__c"])
	assert.Equal(t, false, target["OP_Paid_via_Marketplace__c"])
	assert.Equal(t, 0, target["Cash_Collected_exc_tax_Amount__c"])
	assert.Equal(t, 0, target["Cash_Collected_USD_exc_tax__c"])
	assert.Equal(t, 0, target["Payable_Amount_USD_exc_tax__c"])
}

func TestCreateOrderWorkflowEffectiveDateFallsBackToToday(t *testing.T) {
	source := models.SObject{
		"Service_End_Date__c": "2027-04-30",
	}
	target := make(models.SObject)
	workflow := &createRecordWorkflowConfig{
		DefaultValues: map[string]interface{}{
			"EffectiveDate": todaySentinel,
		},
		FieldMappings: map[string]interface{}{
			"EffectiveDate": "Service_Start_Date__c",
		},
	}
	before := time.Now().UTC().Format("2006-01-02")

	applyDefaultValues(target, workflow)
	applyConfiguredFieldMappingsWithResolver(target, workflow, source, nil)
	after := time.Now().UTC().Format("2006-01-02")

	assert.Contains(t, []string{before, after}, target["EffectiveDate"])
}

func TestApplyConfiguredFieldMappings_OneHopRelatedFields(t *testing.T) {
	source := models.SObject{"ContractId": "contract-1"}
	target := make(models.SObject)
	workflow := &createRecordWorkflowConfig{
		FieldMappings: map[string]interface{}{
			"Applicable_Law__c":        "Contract.Applicable_Law__c",
			"Owned_DM__c":              "Contract.Owned_DM__c",
			"OwnerId":                  "Contract.OwnerId",
			"PingCAP_RevRec_Entity__c": "Contract.PingCAP_Contracting_Entity__c",
		},
	}
	resolver := func(mapping string) (interface{}, bool) {
		values := map[string]interface{}{
			"Contract.Applicable_Law__c":             "Japan",
			"Contract.Owned_DM__c":                   "dm-1",
			"Contract.OwnerId":                       "owner-1",
			"Contract.PingCAP_Contracting_Entity__c": "PingCAP Kabushiki-Kaisha",
		}
		value, ok := values[mapping]
		return value, ok
	}

	applyConfiguredFieldMappingsWithResolver(target, workflow, source, resolver)

	assert.Equal(t, "Japan", target["Applicable_Law__c"])
	assert.Equal(t, "dm-1", target["Owned_DM__c"])
	assert.Equal(t, "owner-1", target["OwnerId"])
	assert.Equal(t, "PingCAP Kabushiki-Kaisha", target["PingCAP_RevRec_Entity__c"])
}

func TestApplyConfiguredFieldMappings_MissingRelatedFieldDoesNotBlock(t *testing.T) {
	source := models.SObject{"ContractId": "missing-contract"}
	target := models.SObject{"Status": "Draft"}
	workflow := &createRecordWorkflowConfig{
		FieldMappings: map[string]interface{}{
			"Applicable_Law__c": "Contract.Applicable_Law__c",
		},
	}

	applyConfiguredFieldMappingsWithResolver(target, workflow, source, func(string) (interface{}, bool) {
		return nil, false
	})

	assert.Equal(t, "Draft", target["Status"])
	_, ok := target["Applicable_Law__c"]
	assert.False(t, ok)
}

// ---------------------------------------------------------------------------
// applyConfiguredFieldMappingsWithResolver + applyFieldMapping
// ---------------------------------------------------------------------------

func TestApplyConfiguredFieldMappings_StringMappings(t *testing.T) {
	source := models.SObject{
		"Name":      "Acme Deal",
		"AccountId": "001-abc",
	}
	target := make(models.SObject)
	workflow := &createRecordWorkflowConfig{
		FieldMappings: map[string]interface{}{
			"Name":      "Name",
			"AccountId": "AccountId",
		},
	}
	applyConfiguredFieldMappingsWithResolver(target, workflow, source, nil)

	assert.Equal(t, "Acme Deal", target["Name"])
	assert.Equal(t, "001-abc", target["AccountId"])
}

func TestApplyFieldMapping_StringMapping_MissingSource(t *testing.T) {
	source := models.SObject{"Name": "Acme"}
	target := make(models.SObject)
	applyFieldMapping(target, "AccountId", "AccountId", source, nil)

	_, exists := target["AccountId"]
	assert.False(t, exists, "missing source field should not set target")
}

func TestApplyFieldMapping_StringMapping_NilValue(t *testing.T) {
	source := models.SObject{"Status": nil}
	target := make(models.SObject)
	applyFieldMapping(target, "Status", "Status", source, nil)

	_, exists := target["Status"]
	assert.False(t, exists, "nil source value should not set target")
}

func TestApplyFieldMapping_ObjectMapping_WithSourceField(t *testing.T) {
	source := models.SObject{"Amount": 1500.00}
	target := make(models.SObject)
	mapping := map[string]interface{}{
		"sourceField":  "Amount",
		"defaultValue": 0.0,
	}
	applyFieldMapping(target, "TotalAmount", mapping, source, nil)

	assert.Equal(t, 1500.00, target["TotalAmount"])
}

func TestApplyFieldMapping_ObjectMapping_FallsBackToDefault(t *testing.T) {
	source := models.SObject{} // no Amount field
	target := make(models.SObject)
	mapping := map[string]interface{}{
		"sourceField":  "Amount",
		"defaultValue": 0.0,
	}
	applyFieldMapping(target, "TotalAmount", mapping, source, nil)

	assert.Equal(t, 0.0, target["TotalAmount"])
}

func TestApplyFieldMapping_ObjectMapping_NilSourceFallsBackToDefault(t *testing.T) {
	source := models.SObject{"Amount": nil}
	target := make(models.SObject)
	mapping := map[string]interface{}{
		"sourceField":  "Amount",
		"defaultValue": "N/A",
	}
	applyFieldMapping(target, "TotalAmount", mapping, source, nil)

	assert.Equal(t, "N/A", target["TotalAmount"])
}

func TestApplyFieldMapping_ObjectMapping_NoDefaultNoSource(t *testing.T) {
	source := models.SObject{}
	target := make(models.SObject)
	mapping := map[string]interface{}{
		"sourceField": "Amount",
	}
	applyFieldMapping(target, "TotalAmount", mapping, source, nil)

	_, exists := target["TotalAmount"]
	assert.False(t, exists, "no source and no default should not set target")
}

func TestApplyConfiguredFieldMappings_NilWorkflow(t *testing.T) {
	target := make(models.SObject)
	applyConfiguredFieldMappingsWithResolver(target, nil, models.SObject{"Name": "test"}, nil)
	assert.Empty(t, target)
}

func TestApplyConfiguredFieldMappings_MixedMappingTypes(t *testing.T) {
	source := models.SObject{
		"Name":              "Big Deal",
		"Product_Family__c": "TiDB Cloud",
	}
	target := make(models.SObject)
	workflow := &createRecordWorkflowConfig{
		FieldMappings: map[string]interface{}{
			"Name":              "Name",
			"Product_Family__c": "Product_Family__c",
			"Discount__c": map[string]interface{}{
				"sourceField":  "Discount__c",
				"defaultValue": 0.0,
			},
		},
	}
	applyConfiguredFieldMappingsWithResolver(target, workflow, source, nil)

	assert.Equal(t, "Big Deal", target["Name"])
	assert.Equal(t, "TiDB Cloud", target["Product_Family__c"])
	assert.Equal(t, 0.0, target["Discount__c"], "missing source falls back to default")
}

// Part D: tests for defaultConvertOpportunityFields, applyForecastCategoryFallback,
// and opportunitySchemaHasForecastCategoryDefault were deleted alongside
// the legacy StandardActionsHandler.ConvertLead method they exercised.
// The equivalent guarantees are now enforced by the metadata standard-action path:
//
//   - "Prospecting" default for StageName → schema DefaultValue on
//     Opportunity.StageName (standard_objects_spec.json). Pinned by
//     TestApplyDefaults_SchemaDefaultFiresWhenAbsent_SkipsWhenNil in
//     bulk_ops_unit_test.go.
//   - CloseDate default → _System_ActionFieldDefault row seeded from
//     standard_actions.json. Pinned by TestShippedPack_ConvertLeadActionPinsContract
//     in standard_actions_test.go.
//   - "Pipeline" ForecastCategory fallback → deleted as a concept; tenants
//     that need a ForecastCategory default set it as a schema DefaultValue
//     OR seed an _System_ActionFieldDefault row (the same admin-config
//     pattern proven by the Admin Config Test).
