package automation

import (
	"context"
	"strings"
	"testing"

	"github.com/nexuscrm/shared/pkg/api"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

type countingQuery struct {
	calls  int
	result *models.QueryResult
}

func (q *countingQuery) QueryWithFilter(ctx context.Context, objectName string, filterExpr string, currentUser *models.UserSession, orderBy string, orderDirection string, limit int) (*models.QueryResult, error) {
	q.calls++
	if q.result != nil {
		return q.result, nil
	}
	return &models.QueryResult{}, nil
}

func (q *countingQuery) Query(ctx context.Context, req api.QueryRequest, user *models.UserSession) (*models.QueryResult, error) {
	return &models.QueryResult{}, nil
}

func (q *countingQuery) QueryWithSpec(context.Context, queryspec.QuerySpec, *models.UserSession) (*models.QueryResult, error) {
	q.calls++
	if q.result != nil {
		return q.result, nil
	}
	return &models.QueryResult{}, nil
}

func (q *countingQuery) RunAnalytics(ctx context.Context, analyticsQuery models.AnalyticsQuery, currentUser *models.UserSession) (interface{}, error) {
	return nil, nil
}

func (q *countingQuery) SearchSingleObject(ctx context.Context, objectName string, term string, searchFields []string, returnFields []string, limit int, offset int, orderBy string, orderDir string, filterExpr string, currentUser *models.UserSession) ([]models.SObject, error) {
	return nil, nil
}

func (q *countingQuery) SearchSuggestions(context.Context, string, string, []string, []string, int, *models.UserSession) ([]models.SObject, error) {
	return nil, nil
}

func (q *countingQuery) GlobalSearch(ctx context.Context, term string, currentUser *models.UserSession) ([]models.SearchResult, error) {
	return nil, nil
}

func (q *countingQuery) Calculate(ctx context.Context, objectName string, record models.SObject, user *models.UserSession) (models.SObject, error) {
	return record, nil
}

func (q *countingQuery) ExecuteRawSQL(ctx context.Context, sql string, params []interface{}, user *models.UserSession) ([]models.SObject, error) {
	return nil, nil
}

func TestBeforeSaveFlowHasSideEffects_FailClosedForRecordUpdateRoutes(t *testing.T) {
	tests := []struct {
		name string
		meta models.FlowMetadata
		want bool
	}{
		{
			name: "filtered empty input reference is side-effecting",
			meta: models.FlowMetadata{
				Start: &models.FlowStart{Object: "Lead"},
				RecordUpdates: []models.FlowRecordUpdate{{
					Name:   "UpdateSiblingLead",
					Object: "Lead",
					Filters: []models.FlowRecordFilter{{
						Field:    "Id",
						Operator: "EqualTo",
						Value:    stringValue("00QTARGET"),
					}},
				}},
			},
			want: true,
		},
		{
			name: "record target object mismatch is side-effecting",
			meta: models.FlowMetadata{
				Start: &models.FlowStart{Object: "Lead"},
				RecordUpdates: []models.FlowRecordUpdate{{
					Name:           "UpdateOtherObject",
					Object:         "Account",
					InputReference: "$Record",
				}},
			},
			want: true,
		},
		{
			name: "in-place trigger record update remains allowed",
			meta: models.FlowMetadata{
				Start: &models.FlowStart{Object: "Lead"},
				RecordUpdates: []models.FlowRecordUpdate{{
					Name:           "MutateTrigger",
					Object:         "Lead",
					InputReference: "$Record",
				}},
			},
			want: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := beforeSaveFlowHasSideEffects(&tt.meta); got != tt.want {
				t.Fatalf("beforeSaveFlowHasSideEffects() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestResolveBeforeSaveRecordUpdateEffect_RouteClassification(t *testing.T) {
	tests := []struct {
		name       string
		meta       *models.FlowMetadata
		update     *models.FlowRecordUpdate
		wantEffect BeforeSaveElementEffect
	}{
		{
			name: "matching $Record target is allowed",
			meta: &models.FlowMetadata{Start: &models.FlowStart{Object: "Lead"}},
			update: &models.FlowRecordUpdate{
				Name:           "MutateTrigger",
				Object:         "Lead",
				InputReference: "  $record  ",
			},
			wantEffect: BeforeSaveAllowedInMemoryTriggerRecordMutation,
		},
		{
			name: "filtered explicit-object same-type is side-effecting",
			meta: &models.FlowMetadata{Start: &models.FlowStart{Object: "Lead"}},
			update: &models.FlowRecordUpdate{
				Name:   "UpdateSiblingLead",
				Object: "Lead",
				Filters: []models.FlowRecordFilter{{
					Field:    "Id",
					Operator: "EqualTo",
					Value:    stringValue("00QTARGET"),
				}},
			},
			wantEffect: BeforeSaveSideEffecting,
		},
		{
			// issue #1119: implicit-$Record node with per-node filters is an
			// in-place trigger-record mutation conditional on the filter match.
			// Object is empty (implicit $Record targeting), so the executor
			// falls through the filtered-cross-object block and takes the
			// single-record in-place path. The filters gate the mutation; they
			// do not trigger a cross-object DB query.
			name: "implicit $Record update with per-node filters is allowed",
			meta: &models.FlowMetadata{Start: &models.FlowStart{Object: "Account"}},
			update: &models.FlowRecordUpdate{
				Name: "NodeA",
				// Object intentionally empty — implicit $Record targeting
				Filters: []models.FlowRecordFilter{{
					Field:    "Industry",
					Operator: "EqualTo",
					Value:    stringValue("Banking"),
				}},
			},
			wantEffect: BeforeSaveAllowedInMemoryTriggerRecordMutation,
		},
		{
			// Regression for Codex Review BLOCK: empty Object + "Id EqualTo
			// $Record.<lookupField>" filter must stay side-effecting. The executor
			// calls deriveObjectFromIdFilter for this pattern and, if field
			// metadata resolves a target type, performs cross-object DML — violating
			// the before-save no-side-effects contract. Fail-closed here since the
			// validator cannot run metadata lookups.
			name: "implicit-Id cross-object filter with empty Object is side-effecting",
			meta: &models.FlowMetadata{Start: &models.FlowStart{Object: "Lead"}},
			update: &models.FlowRecordUpdate{
				Name: "UpdateRelatedAccount",
				// Object intentionally empty (Process Builder pattern A/C)
				Filters: []models.FlowRecordFilter{{
					Field:    "Id",
					Operator: "EqualTo",
					Value:    map[string]interface{}{"elementReference": "$Record.AccountId"},
				}},
			},
			wantEffect: BeforeSaveSideEffecting,
		},
		{
			// Regression: explicit InputReference="$Record" keeps the executor on
			// the in-place path (executor_record_ops.go:319 requires InputReference=""
			// for the filtered cross-object route). Object matching startObject +
			// filters must be allowed even though Object is non-empty.
			name: "explicit $Record InputReference with filters is allowed",
			meta: &models.FlowMetadata{Start: &models.FlowStart{Object: "Lead"}},
			update: &models.FlowRecordUpdate{
				Name:           "FilteredInPlace",
				Object:         "Lead",
				InputReference: "$Record",
				Filters: []models.FlowRecordFilter{{
					Field:    "Status",
					Operator: "EqualTo",
					Value:    stringValue("Open"),
				}},
			},
			wantEffect: BeforeSaveAllowedInMemoryTriggerRecordMutation,
		},
		{
			// Regression: "Id EqualTo $Record.Id" is the record's own primary key
			// filter. deriveObjectFromIdFilter returns "" for "Id" (no ReferenceTo),
			// so the executor falls through to the in-place path. Validator must not
			// block this as cross-object.
			name: "Id EqualTo $Record.Id filter is allowed in-place",
			meta: &models.FlowMetadata{Start: &models.FlowStart{Object: "Lead"}},
			update: &models.FlowRecordUpdate{
				Name: "SelfIdFilter",
				Filters: []models.FlowRecordFilter{{
					Field:    "Id",
					Operator: "EqualTo",
					Value:    map[string]interface{}{"elementReference": "$Record.Id"},
				}},
			},
			wantEffect: BeforeSaveAllowedInMemoryTriggerRecordMutation,
		},
		{
			name: "object mismatch is side-effecting",
			meta: &models.FlowMetadata{Start: &models.FlowStart{Object: "Lead"}},
			update: &models.FlowRecordUpdate{
				Name:           "UpdateOtherObject",
				Object:         "Account",
				InputReference: "$Record",
			},
			wantEffect: BeforeSaveSideEffecting,
		},
		{
			name: "explicit non trigger input reference is side-effecting",
			meta: &models.FlowMetadata{Start: &models.FlowStart{Object: "Lead"}},
			update: &models.FlowRecordUpdate{
				Name:           "UpdateTargetAccount",
				Object:         "Account",
				InputReference: "TargetRecord",
			},
			wantEffect: BeforeSaveSideEffecting,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := ResolveBeforeSaveRecordUpdateEffect(tt.meta, tt.update).Effect; got != tt.wantEffect {
				t.Fatalf("ResolveBeforeSaveRecordUpdateEffect().Effect = %q, want %q", got, tt.wantEffect)
			}
		})
	}
}

func TestValidateBeforeSaveFlowEffects_AllowsAssignmentsAndCustomErrorButRejectsSideEffects(t *testing.T) {
	allowed := &models.FlowMetadata{
		Start: &models.FlowStart{Object: "Lead"},
		Assignments: []models.FlowAssignment{{
			Name: "MutateLeadCompany",
			Items: []models.FlowAssignmentItem{{
				AssignToReference: "$Record.Company",
				Operator:          "Assign",
				Value:             stringValue("Flow Mutated Company"),
			}},
		}},
		CustomErrors: []models.FlowCustomError{{
			Name: "RejectConvert",
			CustomErrorMessages: []models.FlowCustomErrorMessage{{
				ErrorMessage: "Lead must be Qualified before convert",
			}},
		}},
	}
	if err := ValidateBeforeSaveFlowEffects(allowed); err != nil {
		t.Fatalf("ValidateBeforeSaveFlowEffects(allowed) error = %v", err)
	}

	blocked := &models.FlowMetadata{
		Start: &models.FlowStart{Object: "Lead"},
		RecordCreates: []models.FlowRecordCreate{{
			Name:   "CreateAccount",
			Object: "Account",
		}},
	}
	err := ValidateBeforeSaveFlowEffects(blocked)
	if err == nil {
		t.Fatal("ValidateBeforeSaveFlowEffects(blocked) error = nil, want side-effecting rejection")
	}
	if !strings.Contains(err.Error(), beforeSaveSideEffectingMessage) {
		t.Fatalf("ValidateBeforeSaveFlowEffects(blocked) error = %q, want before-save side-effecting message", err.Error())
	}
}

func TestExecuteFlowVersion_BeforeSaveFilteredRecordUpdateRejected(t *testing.T) {
	flowMeta := models.FlowMetadata{
		Start: &models.FlowStart{
			Object:            "Lead",
			TriggerType:       "RecordBeforeSave",
			RecordTriggerType: "Update",
			Connector:         &models.FlowConnector{TargetReference: "UpdateSiblingLead"},
		},
		RecordUpdates: []models.FlowRecordUpdate{{
			Name:   "UpdateSiblingLead",
			Object: "Lead",
			Filters: []models.FlowRecordFilter{{
				Field:    "Id",
				Operator: "EqualTo",
				Value:    stringValue("00QTARGET"),
			}},
			InputAssignments: []models.FlowInputAssignment{
				{Field: "Company", Value: stringValue("side-effect")},
			},
		}},
	}

	exec, rp := previewExecutor(t, flowMeta)
	query := &countingQuery{
		result: &models.QueryResult{
			Records: []models.SObject{{constants.FieldID: "00QTARGET"}},
		},
	}
	exec.query = query

	result, err := exec.ExecuteFlow(WithTransientFlowInterviews(WithBeforeSaveFlow(context.Background())), "BeforeSaveFilteredUpdate", map[string]interface{}{
		"$Record": models.SObject{
			constants.FieldID: "00QSOURCE",
			"attributes":      map[string]interface{}{"type": "Lead"},
		},
	}, newPreviewUser())
	if err != nil {
		t.Fatalf("ExecuteFlow() error = %v", err)
	}
	if result == nil {
		t.Fatal("ExecuteFlow() result = nil")
	}
	if result.IsSuccess {
		t.Fatalf("ExecuteFlow() IsSuccess = true, want false for invalid before-save filtered RecordUpdate")
	}
	if len(result.Errors) == 0 {
		t.Fatalf("ExecuteFlow() errors = %#v, want INVALID_FLOW payload", result)
	}
	if got := result.Errors[0].StatusCode; got != statusCodeInvalidFlow {
		t.Fatalf("ExecuteFlow() first status code = %q, want %q", got, statusCodeInvalidFlow)
	}
	if query.calls != 0 {
		t.Fatalf("QueryWithFilter() calls = %d, want 0 after preflight rejection", query.calls)
	}
	if rp.updateCalls != 0 {
		t.Fatalf("persistence update calls = %d, want 0 after preflight rejection", rp.updateCalls)
	}
}

func TestExecuteRecordUpdate_BeforeSaveFilteredRecordUpdateRejectedBeforeQueryOrUpdate(t *testing.T) {
	query := &countingQuery{
		result: &models.QueryResult{
			Records: []models.SObject{{constants.FieldID: "00QTARGET"}},
		},
	}
	rp := &recordingPersistence{}
	exec := &FlowExecutor{
		persistence: rp,
		query:       query,
	}

	interview := &models.FlowInterview{
		Id: "before-save-filtered-update",
		ContextData: map[string]interface{}{
			"$Record": models.SObject{
				constants.FieldID: "00QSOURCE",
				"attributes":      map[string]interface{}{"type": "Lead"},
			},
		},
	}
	update := &models.FlowRecordUpdate{
		Name:   "UpdateSiblingLead",
		Object: "Lead",
		Filters: []models.FlowRecordFilter{{
			Field:    "Id",
			Operator: "EqualTo",
			Value:    stringValue("00QTARGET"),
		}},
		InputAssignments: []models.FlowInputAssignment{
			{Field: "Company", Value: stringValue("side-effect")},
		},
	}

	nextElem, err := exec.executeRecordUpdate(WithBeforeSaveFlow(context.Background()), interview, nil, update, newPreviewUser())
	if err == nil {
		t.Fatalf("executeRecordUpdate() err = nil, nextElem=%q; want invalid before-save side-effecting error", nextElem)
	}
	if !strings.Contains(err.Error(), "RecordBeforeSave") {
		t.Fatalf("executeRecordUpdate() err = %q, want before-save invalid-flow context", err.Error())
	}
	if query.calls != 0 {
		t.Fatalf("QueryWithFilter() calls = %d, want 0 when before-save assertion rejects filtered update", query.calls)
	}
	if rp.updateCalls != 0 {
		t.Fatalf("persistence update calls = %d, want 0 when before-save assertion rejects filtered update", rp.updateCalls)
	}
}

func TestExecuteRecordUpdate_BeforeSaveExplicitTargetRejectedBeforePersistence(t *testing.T) {
	rp := &recordingPersistence{}
	exec := &FlowExecutor{
		persistence: rp,
	}

	interview := &models.FlowInterview{
		Id: "before-save-explicit-target",
		ContextData: map[string]interface{}{
			"$Record": models.SObject{
				constants.FieldID: "00QSOURCE",
				"attributes":      map[string]interface{}{"type": "Lead"},
			},
			"TargetRecord": models.SObject{
				constants.FieldID: "001TARGET",
				"attributes":      map[string]interface{}{"type": "Account"},
			},
		},
	}
	update := &models.FlowRecordUpdate{
		Name:           "UpdateTargetAccount",
		Object:         "Account",
		InputReference: "TargetRecord",
		InputAssignments: []models.FlowInputAssignment{
			{Field: "Name", Value: stringValue("side-effect")},
		},
	}

	nextElem, err := exec.executeRecordUpdate(WithBeforeSaveFlow(context.Background()), interview, nil, update, newPreviewUser())
	if err == nil {
		t.Fatalf("executeRecordUpdate() err = nil, nextElem=%q; want explicit non-$Record target to be rejected before-save", nextElem)
	}
	if rp.updateCalls != 0 {
		t.Fatalf("persistence update calls = %d, want 0 when explicit target is rejected before-save", rp.updateCalls)
	}
}

func TestExecuteRecordUpdate_FilteredRecordUpdate_NonBeforeSaveUsesPersistenceRoute(t *testing.T) {
	query := &countingQuery{
		result: &models.QueryResult{
			Records: []models.SObject{{constants.FieldID: "00QTARGET"}},
		},
	}
	rp := &recordingPersistence{}
	exec := &FlowExecutor{
		persistence: rp,
		query:       query,
	}

	interview := &models.FlowInterview{
		Id: "after-save-filtered-update",
		ContextData: map[string]interface{}{
			"$Record": models.SObject{
				constants.FieldID: "00QSOURCE",
				"attributes":      map[string]interface{}{"type": "Lead"},
			},
		},
	}
	update := &models.FlowRecordUpdate{
		Name:   "UpdateSiblingLead",
		Object: "Lead",
		Filters: []models.FlowRecordFilter{{
			Field:    "Id",
			Operator: "EqualTo",
			Value:    stringValue("00QTARGET"),
		}},
		InputAssignments: []models.FlowInputAssignment{
			{Field: "Company", Value: stringValue("allowed-after-save")},
		},
	}

	nextElem, err := exec.executeRecordUpdate(context.Background(), interview, nil, update, newPreviewUser())
	if err != nil {
		t.Fatalf("executeRecordUpdate() error = %v", err)
	}
	if nextElem != "" {
		t.Fatalf("executeRecordUpdate() nextElem = %q, want empty without connector", nextElem)
	}
	if query.calls != 1 {
		t.Fatalf("candidate query calls = %d, want 1 on normal filtered update route", query.calls)
	}
	if rp.updateCalls != 1 {
		t.Fatalf("persistence update calls = %d, want 1 on normal filtered update route", rp.updateCalls)
	}
}
