package automation

import (
	"context"
	"testing"

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

// ---------------------------------------------------------------------------
// executeDecision
// ---------------------------------------------------------------------------

func TestExecuteDecision(t *testing.T) {
	executor := newTestExecutor()
	ctx := context.Background()
	user := &models.UserSession{ID: "user-1", Name: "Test User"}

	tests := []struct {
		name         string
		interview    *models.FlowInterview
		decision     *models.FlowDecision
		expectedNext string
	}{
		{
			name: "first rule matches",
			interview: &models.FlowInterview{
				ContextData: map[string]interface{}{"status": "Active"},
			},
			decision: &models.FlowDecision{
				Name: "checkStatus",
				Rules: []models.FlowDecisionRule{
					{
						Name:           "isActive",
						ConditionLogic: "and",
						Conditions: []models.FlowCondition{
							{LeftValueReference: "status", Operator: "EqualTo", RightValue: "Active"},
						},
						Connector: &models.FlowConnector{TargetReference: "ActivePath"},
					},
				},
				DefaultConnector: &models.FlowConnector{TargetReference: "DefaultPath"},
			},
			expectedNext: "ActivePath",
		},
		{
			name: "second rule matches when first fails",
			interview: &models.FlowInterview{
				ContextData: map[string]interface{}{"status": "Pending"},
			},
			decision: &models.FlowDecision{
				Name: "checkStatus",
				Rules: []models.FlowDecisionRule{
					{
						Name:           "isActive",
						ConditionLogic: "and",
						Conditions: []models.FlowCondition{
							{LeftValueReference: "status", Operator: "EqualTo", RightValue: "Active"},
						},
						Connector: &models.FlowConnector{TargetReference: "ActivePath"},
					},
					{
						Name:           "isPending",
						ConditionLogic: "and",
						Conditions: []models.FlowCondition{
							{LeftValueReference: "status", Operator: "EqualTo", RightValue: "Pending"},
						},
						Connector: &models.FlowConnector{TargetReference: "PendingPath"},
					},
				},
				DefaultConnector: &models.FlowConnector{TargetReference: "DefaultPath"},
			},
			expectedNext: "PendingPath",
		},
		{
			name: "falls through to default",
			interview: &models.FlowInterview{
				ContextData: map[string]interface{}{"status": "Unknown"},
			},
			decision: &models.FlowDecision{
				Name: "checkStatus",
				Rules: []models.FlowDecisionRule{
					{
						Name:           "isActive",
						ConditionLogic: "and",
						Conditions: []models.FlowCondition{
							{LeftValueReference: "status", Operator: "EqualTo", RightValue: "Active"},
						},
						Connector: &models.FlowConnector{TargetReference: "ActivePath"},
					},
				},
				DefaultConnector: &models.FlowConnector{TargetReference: "DefaultPath"},
			},
			expectedNext: "DefaultPath",
		},
		{
			name: "no rules and no default returns empty",
			interview: &models.FlowInterview{
				ContextData: map[string]interface{}{},
			},
			decision: &models.FlowDecision{
				Name:  "emptyDecision",
				Rules: nil,
			},
			expectedNext: "",
		},
		{
			name: "matched rule without connector returns empty",
			interview: &models.FlowInterview{
				ContextData: map[string]interface{}{"status": "Active"},
			},
			decision: &models.FlowDecision{
				Name: "noConnectorRule",
				Rules: []models.FlowDecisionRule{
					{
						Name:           "isActive",
						ConditionLogic: "and",
						Conditions: []models.FlowCondition{
							{LeftValueReference: "status", Operator: "EqualTo", RightValue: "Active"},
						},
						Connector: nil, // no connector
					},
				},
			},
			expectedNext: "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			next, err := executor.executeDecision(ctx, tt.interview, tt.decision, user)
			assert.NoError(t, err)
			assert.Equal(t, tt.expectedNext, next)
		})
	}
}

// ---------------------------------------------------------------------------
// executeAssignment
// ---------------------------------------------------------------------------

func TestExecuteAssignment(t *testing.T) {
	executor := newTestExecutor()
	ctx := context.Background()
	user := &models.UserSession{ID: "user-1"}

	tests := []struct {
		name         string
		contextData  map[string]interface{}
		items        []models.FlowAssignmentItem
		connector    *models.FlowConnector
		expectedNext string
		check        func(t *testing.T, ctx map[string]interface{})
	}{
		{
			name:        "Assign operator sets value",
			contextData: map[string]interface{}{},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "myVar", Operator: "Assign", Value: "hello"},
			},
			connector:    &models.FlowConnector{TargetReference: "Next"},
			expectedNext: "Next",
			check: func(t *testing.T, ctx map[string]interface{}) {
				assert.Equal(t, "hello", ctx["myVar"])
			},
		},
		{
			name:        "empty operator defaults to Assign",
			contextData: map[string]interface{}{},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "myVar", Operator: "", Value: "hello"},
			},
			connector:    nil,
			expectedNext: "",
			check: func(t *testing.T, ctx map[string]interface{}) {
				assert.Equal(t, "hello", ctx["myVar"])
			},
		},
		{
			name:        "Add to number",
			contextData: map[string]interface{}{"counter": float64(5)},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "counter", Operator: "Add", Value: float64(3)},
			},
			connector:    nil,
			expectedNext: "",
			check: func(t *testing.T, ctx map[string]interface{}) {
				assert.Equal(t, float64(8), ctx["counter"])
			},
		},
		{
			name:        "Add to collection",
			contextData: map[string]interface{}{"items": []interface{}{"a", "b"}},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "items", Operator: "Add", Value: "c"},
			},
			connector:    nil,
			expectedNext: "",
			check: func(t *testing.T, ctx map[string]interface{}) {
				items := ctx["items"].([]interface{})
				assert.Len(t, items, 3)
				assert.Equal(t, "c", items[2])
			},
		},
		{
			name:        "Add initializes nil as collection",
			contextData: map[string]interface{}{},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "newCollection", Operator: "Add", Value: "first"},
			},
			connector:    nil,
			expectedNext: "",
			check: func(t *testing.T, ctx map[string]interface{}) {
				items := ctx["newCollection"].([]interface{})
				assert.Len(t, items, 1)
				assert.Equal(t, "first", items[0])
			},
		},
		{
			name:        "AddAtStart prepends to collection",
			contextData: map[string]interface{}{"items": []interface{}{"b", "c"}},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "items", Operator: "AddAtStart", Value: "a"},
			},
			connector:    nil,
			expectedNext: "",
			check: func(t *testing.T, ctx map[string]interface{}) {
				items := ctx["items"].([]interface{})
				assert.Equal(t, []interface{}{"a", "b", "c"}, items)
			},
		},
		{
			name:        "AddAtStart initializes nil as collection",
			contextData: map[string]interface{}{},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "items", Operator: "AddAtStart", Value: "first"},
			},
			connector:    nil,
			expectedNext: "",
			check: func(t *testing.T, ctx map[string]interface{}) {
				items := ctx["items"].([]interface{})
				assert.Len(t, items, 1)
			},
		},
		{
			name:        "Subtract from number",
			contextData: map[string]interface{}{"counter": float64(10)},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "counter", Operator: "Subtract", Value: float64(3)},
			},
			connector:    nil,
			expectedNext: "",
			check: func(t *testing.T, ctx map[string]interface{}) {
				assert.Equal(t, float64(7), ctx["counter"])
			},
		},
		{
			name:        "RemoveFirst removes first occurrence",
			contextData: map[string]interface{}{"items": []interface{}{"a", "b", "a", "c"}},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "items", Operator: "RemoveFirst", Value: "a"},
			},
			connector:    nil,
			expectedNext: "",
			check: func(t *testing.T, ctx map[string]interface{}) {
				items := ctx["items"].([]interface{})
				assert.Equal(t, []interface{}{"b", "a", "c"}, items)
			},
		},
		{
			name:        "RemoveAll removes all occurrences",
			contextData: map[string]interface{}{"items": []interface{}{"a", "b", "a", "c", "a"}},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "items", Operator: "RemoveAll", Value: "a"},
			},
			connector:    nil,
			expectedNext: "",
			check: func(t *testing.T, ctx map[string]interface{}) {
				items := ctx["items"].([]interface{})
				assert.Equal(t, []interface{}{"b", "c"}, items)
			},
		},
		{
			name:        "RemoveAfterFirst keeps first, removes rest",
			contextData: map[string]interface{}{"items": []interface{}{"a", "b", "a", "c", "a"}},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "items", Operator: "RemoveAfterFirst", Value: "a"},
			},
			connector:    nil,
			expectedNext: "",
			check: func(t *testing.T, ctx map[string]interface{}) {
				items := ctx["items"].([]interface{})
				assert.Equal(t, []interface{}{"a", "b", "c"}, items)
			},
		},
		{
			name:        "RemoveBeforeFirst removes all before first occurrence",
			contextData: map[string]interface{}{"items": []interface{}{"x", "y", "a", "b", "a"}},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "items", Operator: "RemoveBeforeFirst", Value: "a"},
			},
			connector:    nil,
			expectedNext: "",
			check: func(t *testing.T, ctx map[string]interface{}) {
				items := ctx["items"].([]interface{})
				assert.Equal(t, []interface{}{"a", "b", "a"}, items)
			},
		},
		{
			name:        "multiple assignments in sequence",
			contextData: map[string]interface{}{},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "x", Operator: "Assign", Value: "hello"},
				{AssignToReference: "y", Operator: "Assign", Value: float64(42)},
			},
			connector:    &models.FlowConnector{TargetReference: "Done"},
			expectedNext: "Done",
			check: func(t *testing.T, ctx map[string]interface{}) {
				assert.Equal(t, "hello", ctx["x"])
				assert.Equal(t, float64(42), ctx["y"])
			},
		},
		{
			name: "Assign operator updates nested $Record field",
			contextData: map[string]interface{}{
				"$Record": models.SObject{
					"Portal_ID__c":      "portal-1",
					"Account_Status__c": nil,
				},
			},
			items: []models.FlowAssignmentItem{
				{AssignToReference: "$Record.Account_Status__c", Operator: "Assign", Value: "new_user"},
			},
			connector:    nil,
			expectedNext: "",
			check: func(t *testing.T, ctx map[string]interface{}) {
				record, ok := ctx["$Record"].(models.SObject)
				assert.True(t, ok)
				assert.Equal(t, "new_user", record["Account_Status__c"])
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			interview := &models.FlowInterview{ContextData: tt.contextData}
			assignment := &models.FlowAssignment{
				Name:      "testAssignment",
				Items:     tt.items,
				Connector: tt.connector,
			}

			next, err := executor.executeAssignment(ctx, interview, assignment, user)
			assert.NoError(t, err)
			assert.Equal(t, tt.expectedNext, next)
			tt.check(t, interview.ContextData)
		})
	}
}

// TestExecuteAssignment_RecordDottedReference verifies that assigning to
// `$Record.FieldName` mutates the underlying $Record sObject map in place,
// matching Salesforce before-save flow semantics. This is the critical path
// for record-triggered auto-assignment flows (e.g. Lead region → Owner).
//
// Regression test for ywang-official/MetaDrivenPlatform4AI#550.
func TestExecuteAssignment_RecordDottedReference(t *testing.T) {
	executor := newTestExecutor()
	ctx := context.Background()
	user := &models.UserSession{ID: "user-1"}

	t.Run("$Record.OwnerId assignment mutates $Record sObject", func(t *testing.T) {
		// Simulate the flow_trigger_listener handoff: the input $Record is
		// a map reference shared with the caller's in-flight record.
		record := models.SObject{
			"Id":        "lead-1",
			"Region__c": "NA",
			"OwnerId":   nil,
		}
		interview := &models.FlowInterview{
			ContextData: map[string]interface{}{
				"$Record": record,
			},
		}
		assignment := &models.FlowAssignment{
			Name: "setOwnerNA",
			Items: []models.FlowAssignmentItem{
				{AssignToReference: "$Record.OwnerId", Operator: "Assign", Value: "005RC00000BN0iXYAT"},
			},
			Connector: &models.FlowConnector{TargetReference: "End"},
		}

		next, err := executor.executeAssignment(ctx, interview, assignment, user)
		assert.NoError(t, err)
		assert.Equal(t, "End", next)

		// The $Record sub-map (same reference as the caller's record) MUST
		// see the new OwnerId. A no-op flat key `ContextData["$Record.OwnerId"]`
		// would leave the caller's record unchanged — the bug in #550.
		assert.Equal(t, "005RC00000BN0iXYAT", record["OwnerId"],
			"assignment to $Record.OwnerId must mutate the underlying $Record map")
		assert.Equal(t, "005RC00000BN0iXYAT", interview.ContextData["$Record"].(models.SObject)["OwnerId"])

		// And the stale flat key must NOT be created — it would mask the bug
		// and confuse downstream readers that use resolveNestedReference.
		_, flatExists := interview.ContextData["$Record.OwnerId"]
		assert.False(t, flatExists, "dotted assignment must not create flat key")
	})

	t.Run("simple (non-dotted) reference still writes flat key", func(t *testing.T) {
		interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
		assignment := &models.FlowAssignment{
			Name: "setVar",
			Items: []models.FlowAssignmentItem{
				{AssignToReference: "myVar", Operator: "Assign", Value: "hello"},
			},
		}
		_, err := executor.executeAssignment(ctx, interview, assignment, user)
		assert.NoError(t, err)
		assert.Equal(t, "hello", interview.ContextData["myVar"])
	})

	t.Run("dotted assign with missing root is a no-op (no panic)", func(t *testing.T) {
		interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
		assignment := &models.FlowAssignment{
			Name: "setMissing",
			Items: []models.FlowAssignmentItem{
				{AssignToReference: "$Record.OwnerId", Operator: "Assign", Value: "005x"},
			},
		}
		_, err := executor.executeAssignment(ctx, interview, assignment, user)
		assert.NoError(t, err)
		_, flatExists := interview.ContextData["$Record.OwnerId"]
		assert.False(t, flatExists)
	})
}

// ---------------------------------------------------------------------------
// executeLoop
// ---------------------------------------------------------------------------

func TestExecuteLoop(t *testing.T) {
	executor := newTestExecutor()
	ctx := context.Background()
	user := &models.UserSession{ID: "user-1"}

	t.Run("iterates over collection", func(t *testing.T) {
		interview := &models.FlowInterview{
			ContextData: map[string]interface{}{
				"myItems": []interface{}{"a", "b", "c"},
			},
		}
		loop := &models.FlowLoop{
			Name:                       "myLoop",
			CollectionReference:        "myItems",
			AssignNextValueToReference: "currentItem",
			NextValueConnector:         &models.FlowConnector{TargetReference: "LoopBody"},
			NoMoreValuesConnector:      &models.FlowConnector{TargetReference: "AfterLoop"},
		}

		// First iteration
		next, err := executor.executeLoop(ctx, interview, loop, user)
		assert.NoError(t, err)
		assert.Equal(t, "LoopBody", next)
		assert.Equal(t, "a", interview.ContextData["currentItem"])

		// Second iteration
		next, err = executor.executeLoop(ctx, interview, loop, user)
		assert.NoError(t, err)
		assert.Equal(t, "LoopBody", next)
		assert.Equal(t, "b", interview.ContextData["currentItem"])

		// Third iteration
		next, err = executor.executeLoop(ctx, interview, loop, user)
		assert.NoError(t, err)
		assert.Equal(t, "LoopBody", next)
		assert.Equal(t, "c", interview.ContextData["currentItem"])

		// No more items
		next, err = executor.executeLoop(ctx, interview, loop, user)
		assert.NoError(t, err)
		assert.Equal(t, "AfterLoop", next)
	})

	t.Run("empty collection exits immediately", func(t *testing.T) {
		interview := &models.FlowInterview{
			ContextData: map[string]interface{}{
				"myItems": []interface{}{},
			},
		}
		loop := &models.FlowLoop{
			Name:                  "myLoop",
			CollectionReference:   "myItems",
			NoMoreValuesConnector: &models.FlowConnector{TargetReference: "AfterLoop"},
		}

		next, err := executor.executeLoop(ctx, interview, loop, user)
		assert.NoError(t, err)
		assert.Equal(t, "AfterLoop", next)
	})

	t.Run("missing collection exits immediately", func(t *testing.T) {
		interview := &models.FlowInterview{
			ContextData: map[string]interface{}{},
		}
		loop := &models.FlowLoop{
			Name:                  "myLoop",
			CollectionReference:   "nonExistent",
			NoMoreValuesConnector: &models.FlowConnector{TargetReference: "AfterLoop"},
		}

		next, err := executor.executeLoop(ctx, interview, loop, user)
		assert.NoError(t, err)
		assert.Equal(t, "AfterLoop", next)
	})

	t.Run("SObject collection", func(t *testing.T) {
		interview := &models.FlowInterview{
			ContextData: map[string]interface{}{
				"records": []models.SObject{
					{"Id": "001", "Name": "First"},
					{"Id": "002", "Name": "Second"},
				},
			},
		}
		loop := &models.FlowLoop{
			Name:                       "recordLoop",
			CollectionReference:        "records",
			AssignNextValueToReference: "currentRecord",
			NextValueConnector:         &models.FlowConnector{TargetReference: "LoopBody"},
			NoMoreValuesConnector:      &models.FlowConnector{TargetReference: "AfterLoop"},
		}

		next, err := executor.executeLoop(ctx, interview, loop, user)
		assert.NoError(t, err)
		assert.Equal(t, "LoopBody", next)
	})

	t.Run("map slice collection", func(t *testing.T) {
		interview := &models.FlowInterview{
			ContextData: map[string]interface{}{
				"maps": []map[string]interface{}{
					{"key": "value1"},
					{"key": "value2"},
				},
			},
		}
		loop := &models.FlowLoop{
			Name:                       "mapLoop",
			CollectionReference:        "maps",
			AssignNextValueToReference: "current",
			NextValueConnector:         &models.FlowConnector{TargetReference: "LoopBody"},
			NoMoreValuesConnector:      &models.FlowConnector{TargetReference: "AfterLoop"},
		}

		next, err := executor.executeLoop(ctx, interview, loop, user)
		assert.NoError(t, err)
		assert.Equal(t, "LoopBody", next)
	})

	t.Run("unsupported collection type exits", func(t *testing.T) {
		interview := &models.FlowInterview{
			ContextData: map[string]interface{}{
				"scalar": "just-a-string",
			},
		}
		loop := &models.FlowLoop{
			Name:                  "badLoop",
			CollectionReference:   "scalar",
			NoMoreValuesConnector: &models.FlowConnector{TargetReference: "AfterLoop"},
		}

		next, err := executor.executeLoop(ctx, interview, loop, user)
		assert.NoError(t, err)
		assert.Equal(t, "AfterLoop", next)
	})

	t.Run("descending iteration order reverses collection", func(t *testing.T) {
		interview := &models.FlowInterview{
			ContextData: map[string]interface{}{
				"items": []interface{}{"a", "b", "c"},
			},
		}
		loop := &models.FlowLoop{
			Name:                       "descLoop",
			CollectionReference:        "items",
			IterationOrder:             "Desc",
			AssignNextValueToReference: "current",
			NextValueConnector:         &models.FlowConnector{TargetReference: "LoopBody"},
			NoMoreValuesConnector:      &models.FlowConnector{TargetReference: "AfterLoop"},
		}

		// First iteration should get "c" (last element, since reversed)
		next, err := executor.executeLoop(ctx, interview, loop, user)
		assert.NoError(t, err)
		assert.Equal(t, "LoopBody", next)
		assert.Equal(t, "c", interview.ContextData["current"])

		// Second should get "b"
		next, err = executor.executeLoop(ctx, interview, loop, user)
		assert.NoError(t, err)
		assert.Equal(t, "b", interview.ContextData["current"])

		// Third should get "a"
		next, err = executor.executeLoop(ctx, interview, loop, user)
		assert.NoError(t, err)
		assert.Equal(t, "a", interview.ContextData["current"])
	})
}

// TestExecuteLoop_LoopDepthLifecycle asserts that the transient `_loopDepth`
// sentinel is incremented on first entry, preserved through body iterations,
// and decremented on exit so resolveNestedReferenceCtx can gate the N+1 warn
// correctly (see design doc GAP 6 / Phase 2.1).
func TestExecuteLoop_LoopDepthLifecycle(t *testing.T) {
	executor := newTestExecutor()
	ctx := context.Background()
	user := &models.UserSession{ID: "user-1"}

	interview := &models.FlowInterview{
		ContextData: map[string]interface{}{
			"myItems": []interface{}{"a", "b"},
		},
	}
	loop := &models.FlowLoop{
		Name:                       "myLoop",
		CollectionReference:        "myItems",
		AssignNextValueToReference: "currentItem",
		NextValueConnector:         &models.FlowConnector{TargetReference: "LoopBody"},
		NoMoreValuesConnector:      &models.FlowConnector{TargetReference: "AfterLoop"},
	}

	// Baseline: not yet in a loop.
	assert.Equal(t, 0, loopDepth(interview.ContextData))

	// First iteration — depth goes to 1.
	_, err := executor.executeLoop(ctx, interview, loop, user)
	assert.NoError(t, err)
	assert.Equal(t, 1, loopDepth(interview.ContextData))

	// Second iteration — depth stays at 1 (still same loop).
	_, err = executor.executeLoop(ctx, interview, loop, user)
	assert.NoError(t, err)
	assert.Equal(t, 1, loopDepth(interview.ContextData))

	// Exit iteration — depth returns to 0.
	next, err := executor.executeLoop(ctx, interview, loop, user)
	assert.NoError(t, err)
	assert.Equal(t, "AfterLoop", next)
	assert.Equal(t, 0, loopDepth(interview.ContextData))
}

// TestExecuteLoop_EmptyCollectionDoesNotDecrementBelowZero guards against the
// "missing collection on first call" edge case — depth must not underflow when
// the loop exits without ever having incremented.
func TestExecuteLoop_EmptyCollectionDoesNotDecrementBelowZero(t *testing.T) {
	executor := newTestExecutor()
	ctx := context.Background()
	user := &models.UserSession{ID: "user-1"}

	interview := &models.FlowInterview{
		ContextData: map[string]interface{}{
			"myItems": []interface{}{}, // empty collection
		},
	}
	loop := &models.FlowLoop{
		Name:                       "myLoop",
		CollectionReference:        "myItems",
		AssignNextValueToReference: "currentItem",
		NextValueConnector:         &models.FlowConnector{TargetReference: "LoopBody"},
		NoMoreValuesConnector:      &models.FlowConnector{TargetReference: "AfterLoop"},
	}

	next, err := executor.executeLoop(ctx, interview, loop, user)
	assert.NoError(t, err)
	assert.Equal(t, "AfterLoop", next)
	assert.Equal(t, 0, loopDepth(interview.ContextData))
}
