package automation

import (
	"encoding/json"
	"reflect"
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/shared/pkg/models"
	connector "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
)

// The captured persistence update is the boundary under test: enrichment may
// complete one observed transform, but must never turn into a snapshot write.
func TestConnectorTransformSourceWritesOnlyObservedMappings(t *testing.T) {
	mappings := []connector.ConnectorFieldMappingInput{
		{SourceField: "child", TargetField: "Composite", TransformExpression: `$Record.parent & "|" & $Record.child`},
		{SourceField: "parent", TargetField: "Parent"},
		{SourceField: "status", TargetField: "Status"},
		{SourceField: "owner", TargetField: "OwnerId"},
		{SourceField: "unrelated", TargetField: "Unrelated", TransformExpression: `UPPER($Record.unrelated)`},
		{SourceField: "constant", TargetField: "Constant", TransformExpression: `"default"`},
	}
	for _, tc := range []struct {
		name                 string
		observed, supplement models.SObject
		coalesced, optOut    bool
		want                 models.SObject
	}{
		{name: "child event uses parent only as context", observed: models.SObject{"child": "C"}, supplement: models.SObject{"parent": "P", "child": "stale", "status": "new", "owner": "other", "unrelated": "replace"}, coalesced: true, want: models.SObject{"Composite": "P|C"}},
		{name: "sparse action has the same target boundary", observed: models.SObject{"child": "C"}, supplement: models.SObject{"parent": "P", "status": "new"}, want: models.SObject{"Composite": "P|C"}},
		{name: "observed parent wins over context", observed: models.SObject{"child": "C", "parent": "event"}, supplement: models.SObject{"parent": "stale", "child": "stale"}, coalesced: true, want: models.SObject{"Composite": "event|C", "Parent": "event"}},
		{name: "case folded observed dependency wins", observed: models.SObject{"CHILD": "C", "PARENT": "event"}, supplement: models.SObject{"parent": "stale"}, coalesced: true, want: models.SObject{"Composite": "event|C", "Parent": "event"}},
		{name: "missing dependency remains skipped", observed: models.SObject{"child": "C"}, supplement: models.SObject{"status": "new"}, coalesced: true},
		{name: "context alone selects nothing", observed: models.SObject{}, supplement: models.SObject{"parent": "P", "child": "C", "status": "new", "owner": "other", "unrelated": "replace", "constant": "x"}, coalesced: true},
		{name: "blank event is not replaced", observed: models.SObject{"child": ""}, supplement: models.SObject{"parent": "P", "child": "stale"}, coalesced: true, want: models.SObject{"Composite": "P|"}},
		{name: "opt out preserves sparse skip", observed: models.SObject{"child": "C"}, supplement: models.SObject{"parent": "P"}, coalesced: true, optOut: true},
		{name: "unrelated event cannot trigger context transform", observed: models.SObject{"status": "event"}, supplement: models.SObject{"parent": "P", "child": "C", "owner": "other"}, coalesced: true, want: models.SObject{"Status": "event"}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			beforeObserved, _ := json.Marshal(tc.observed)
			beforeSupplement, _ := json.Marshal(tc.supplement)
			capture := &captureUpdate{}
			executor := &FlowExecutor{persistence: &stubPersistenceCapture{capture: capture}, connectorMappingResolverFn: stubResolver(mappings), formulaEngine: formula.NewEngine()}
			params := map[string]interface{}{"source": "connector_a", "sourceObject": "remote", "targetObject": "Contact", "actionType": "sync", "direction": "inbound", "targetId": "record-1", "applyExtraUpdates": false, "sourceRecord": map[string]interface{}{"elementReference": "$SourceRecord"}}
			if !tc.optOut {
				params["transformSourceRecord"] = map[string]interface{}{"elementReference": "$TransformSourceRecord"}
			}
			interview := buildInterviewWithSourceRecord(tc.observed, nil)
			interview.ContextData["$TransformSourceRecord"] = tc.supplement
			if tc.coalesced {
				params["coalescedPropertyChanges"] = true
				params["coalescedPayloads"] = map[string]interface{}{"elementReference": "$CoalescedPayloads"}
				payloads := []interface{}{}
				for field, value := range tc.observed {
					payloads = append(payloads, models.SObject{"payload": models.SObject{"propertyName": field, "propertyValue": value, "occurredAt": int64(300)}})
				}
				interview.ContextData["$CoalescedPayloads"] = payloads
				// A full sourceRecord was already passed by coalesced callers. It must
				// remain ignored when the new explicit context input is omitted.
				interview.ContextData["$SourceRecord"] = tc.supplement
			} else {
				params["filterMappingsBySourcePresence"] = true
			}
			_, err := executor.executeApplyConnectorFieldMappings(connectorMappingContext("connector_a"), interview, buildConnectorMappingAction(params), nil)
			if err != nil {
				t.Fatal(err)
			}
			if len(tc.want) == 0 {
				if len(capture.calls) != 0 {
					t.Fatalf("unselected input wrote %#v", capture.calls)
				}
			} else if len(capture.calls) != 1 || !reflect.DeepEqual(capture.calls[0].updates, tc.want) {
				t.Fatalf("updates=%#v, want only %#v", capture.calls, tc.want)
			}
			afterObserved, _ := json.Marshal(tc.observed)
			afterSupplement, _ := json.Marshal(tc.supplement)
			if string(beforeObserved) != string(afterObserved) || string(beforeSupplement) != string(afterSupplement) {
				t.Fatal("action mutated its input records")
			}
		})
	}
}

func TestConnectorTransformSourceRejectsInvalidInputBeforeWrite(t *testing.T) {
	for _, tc := range []struct {
		name              string
		input             interface{}
		sparse, duplicate bool
		message           string
	}{
		{name: "null", input: nil, sparse: true, message: "must resolve to a record"},
		{name: "scalar", input: map[string]interface{}{"stringValue": "not a record"}, sparse: true, message: "must resolve to a record"},
		{name: "unresolved", input: map[string]interface{}{"elementReference": "Missing"}, sparse: true, message: "must resolve to a record"},
		{name: "not sparse", input: map[string]interface{}{"elementReference": "$SourceRecord"}, message: "requires sparse mapping selection"},
		{name: "duplicate", input: map[string]interface{}{"elementReference": "$SourceRecord"}, sparse: true, duplicate: true, message: "duplicate transformSourceRecord"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			capture := &captureUpdate{}
			executor := &FlowExecutor{persistence: &stubPersistenceCapture{capture: capture}, connectorMappingResolverFn: stubResolver(nil)}
			action := buildConnectorMappingAction(map[string]interface{}{"source": "connector_a", "sourceObject": "remote", "targetObject": "Contact", "actionType": "sync", "direction": "inbound", "targetId": "record-1", "applyExtraUpdates": false, "filterMappingsBySourcePresence": tc.sparse, "transformSourceRecord": tc.input})
			if tc.duplicate {
				action.InputParameters = append(action.InputParameters, models.FlowActionParameter{Name: "transformSourceRecord", Value: tc.input})
			}
			_, err := executor.executeApplyConnectorFieldMappings(connectorMappingContext("connector_a"), buildInterviewWithSourceRecord(models.SObject{}, nil), action, nil)
			if err == nil || !strings.Contains(err.Error(), tc.message) {
				t.Fatalf("error=%v, want %s", err, tc.message)
			}
			if len(capture.calls) != 0 {
				t.Fatal("invalid context reached persistence")
			}
		})
	}
}
