package automation

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

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

// TestIssue2231_ScheduledTriggerExplicitDollarRecord_ZeroFilters_NullObject_PersistsUpdate
// reproduces the exact recordUpdate shape reported in #2231:
//
//	{
//	  "inputReference": "$Record",
//	  "object": null,
//	  "filters": [],
//	  "inputAssignments": [{ "field": "StageName", "value": { "stringValue": "Launched" } }]
//	}
//
// The interview's $Record has attributes.type stamped (mirrors what
// buildScheduledRunInputs does for Scheduled-trigger flows). The executor
// MUST resolve the target object from attributes.type and call
// persistence.Update — the production symptom is a silent no-op.
func TestIssue2231_ScheduledTriggerExplicitDollarRecord_ZeroFilters_NullObject_PersistsUpdate(t *testing.T) {
	mockPersist := new(MockPersistence)
	executor := &FlowExecutor{persistence: mockPersist}

	interview := &models.FlowInterview{
		ContextData: map[string]interface{}{
			"$Record": models.SObject{
				constants.FieldID: "opp-001",
				"attributes":      map[string]interface{}{"type": "Opportunity"},
				"StageName":       "Deal Closed",
			},
		},
	}

	update := &models.FlowRecordUpdate{
		Name:           "update_stage",
		Object:         "", // null in source JSON
		InputReference: "$Record",
		Filters:        []models.FlowRecordFilter{},
		InputAssignments: []models.FlowInputAssignment{
			{Field: "StageName", Value: map[string]interface{}{"stringValue": "Launched"}},
		},
		Connector: &models.FlowConnector{TargetReference: "End"},
	}

	mockPersist.On("Update", mock.Anything, "Opportunity", "opp-001", mock.Anything, mock.Anything).
		Return(nil)

	ctx := context.Background()
	next, err := executor.executeRecordUpdate(ctx, interview, update, nil)

	assert.NoError(t, err)
	assert.Equal(t, "End", next)
	mockPersist.AssertExpectations(t)
}

// TestIssue2231_FullScheduledFlow_FromMetadataJson_ExecutesUpdate exercises
// the production path end-to-end:
//
//  1. Real JSON metadata (the exact shape stored in the `flow.Metadata`
//     column for `Opportunity_Stage_To_Lunched`).
//  2. json.Unmarshal into models.FlowVersion (matches HydrateMetadata).
//  3. buildScheduledRunInputs against a mock query that returns one
//     "Deal Closed" Opportunity row.
//  4. executeFlowVersion across the resulting input set.
//
// The test asserts persistence.Update was called — i.e. the recordUpdate
// produced an observable mutation, not a silent no-op.
func TestIssue2231_FullScheduledFlow_FromMetadataJson_ExecutesUpdate(t *testing.T) {
	mockPersist := new(MockPersistence)
	mockQuery := new(MockQueryService)
	executor := newTransientExecutor(mockPersist)
	executor.query = mockQuery
	user := &models.UserSession{ID: "system"}

	// Mirrors the JSON_PRETTY of $.start, $.recordUpdates from the live
	// Opportunity_Stage_To_Lunched flow on localhost.
	metaJSON := `{
		"start": {
			"object": "Opportunity",
			"triggerType": "Scheduled",
			"connector": { "targetReference": "update_stage" },
			"schedule": { "frequency": "Daily", "startDate": "2025-05-26", "startTime": "00:00:00.000Z" },
			"filters": [
				{ "field": "StageName", "operator": "EqualTo", "value": { "stringValue": "Deal Closed" } }
			]
		},
		"recordUpdates": [
			{
				"name": "update_stage",
				"label": "update stage",
				"object": null,
				"inputReference": "$Record",
				"filters": [],
				"inputAssignments": [
					{
						"field": "StageName",
						"value": { "stringValue": "Launched" }
					}
				]
			}
		]
	}`

	var meta models.FlowMetadata
	if err := json.Unmarshal([]byte(metaJSON), &meta); err != nil {
		t.Fatalf("unmarshal flow metadata: %v", err)
	}

	flowVersion := &models.FlowVersion{
		Id:       "301RC00000oRxIsYAK",
		Metadata: meta,
	}

	// One "Deal Closed" Opportunity returned by the scheduled query.
	mockQuery.On("QueryWithFilter", mock.Anything, "Opportunity",
		"StageName = 'Deal Closed'", user, "", "", scheduledFlowQueryLimit).
		Return(&models.QueryResult{
			Records: []models.SObject{
				{constants.FieldID: "opp-001", "StageName": "Deal Closed"},
			},
		}, nil).Once()

	// Persistent interview path: one Insert (CreateInterview) + one Update
	// (CompleteInterview). The flowExecutor wraps these via FlowInterviewService.
	mockPersist.On("Insert", mock.Anything, "FlowInterview", mock.Anything, mock.Anything).
		Return(models.SObject{constants.FieldID: "interview-1"}, nil).Once()
	mockPersist.On("Update", mock.Anything, "FlowInterview", "interview-1", mock.Anything, mock.Anything).
		Return(nil)

	// The recordUpdate must reach this mock — silent no-op = test FAIL.
	mockPersist.On("Update", mock.Anything, "Opportunity", "opp-001", mock.Anything, mock.Anything).
		Return(nil).Once()

	inputSets, err := executor.buildScheduledRunInputs(context.Background(), flowVersion, user)
	if err != nil {
		t.Fatalf("buildScheduledRunInputs: %v", err)
	}
	assert.Len(t, inputSets, 1, "scheduled query should match one Deal Closed Opportunity")

	for _, inputs := range inputSets {
		result, err := executor.executeFlowVersion(context.Background(), flowVersion, "Opportunity_Stage_To_Lunched", inputs, user)
		assert.NoError(t, err)
		assert.True(t, result.IsSuccess, "flow execution should succeed")
	}

	// AssertExpectations verifies the Opportunity Update was actually invoked.
	mockPersist.AssertExpectations(t)
	mockQuery.AssertExpectations(t)
}
