package automation

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/assert"

	"github.com/nexuscrm/backend/pkg/logging"
	"github.com/nexuscrm/shared/pkg/api"
	"github.com/nexuscrm/shared/pkg/models"
)

// sfPrefixQueryStub satisfies the subset of ports.QueryService exercised by
// sfKeyPrefixMatch: it matches on ObjectAPIName + first Id-criterion value and
// returns whatever records the test seeded. Unexercised methods return
// zero-values so we do not drag the rest of the interface into these focused
// tests (mirrors the stubQueryAdapter pattern already used for __r tests).
type sfPrefixQueryStub struct {
	records map[string][]models.SObject // key: "<object>|<id>"
	err     error
}

func (s *sfPrefixQueryStub) Query(ctx context.Context, req api.QueryRequest, user *models.UserSession) (*models.QueryResult, error) {
	if s.err != nil {
		return nil, s.err
	}
	idVal := ""
	for _, c := range req.Criteria {
		if c.Field == "Id" {
			idVal, _ = c.Val.(string)
			break
		}
	}
	recs := s.records[req.ObjectAPIName+"|"+idVal]
	return &models.QueryResult{Records: recs, TotalSize: len(recs)}, nil
}
func (s *sfPrefixQueryStub) QueryWithFilter(context.Context, string, string, *models.UserSession, string, string, int) (*models.QueryResult, error) {
	return nil, nil
}
func (s *sfPrefixQueryStub) RunAnalytics(context.Context, models.AnalyticsQuery, *models.UserSession) (interface{}, error) {
	return nil, nil
}
func (s *sfPrefixQueryStub) SearchSingleObject(context.Context, string, string, []string, []string, int, int, string, string, string, *models.UserSession) ([]models.SObject, error) {
	return nil, nil
}
func (s *sfPrefixQueryStub) GlobalSearch(context.Context, string, *models.UserSession) ([]models.SearchResult, error) {
	return nil, nil
}
func (s *sfPrefixQueryStub) Calculate(context.Context, string, models.SObject, *models.UserSession) (models.SObject, error) {
	return nil, nil
}
func (s *sfPrefixQueryStub) ExecuteRawSQL(context.Context, string, []interface{}, *models.UserSession) ([]models.SObject, error) {
	return nil, nil
}

func newSFPrefixExecutor(stub *sfPrefixQueryStub) *FlowExecutor {
	return &FlowExecutor{query: stub, logger: logging.Default}
}

func TestSFKeyPrefixMatch_KnownPrefixAndIDResolves(t *testing.T) {
	// $Record.WhatId → a UUID that points to an Opportunity. Flow author wrote
	// `StartsWith "006"` expecting SF semantics. Engine must resolve to true.
	oppID := "4a8db94d-2e54-478c-a7cb-c5dc01fd5d0f"
	stub := &sfPrefixQueryStub{
		records: map[string][]models.SObject{
			"Opportunity|" + oppID: {{"Id": oppID}},
		},
	}
	e := newSFPrefixExecutor(stub)
	matched, handled := e.sfKeyPrefixMatch(context.Background(), oppID, "006")
	assert.True(t, handled, "known SF prefix against id must be handled")
	assert.True(t, matched, "id that exists in Opportunity table must match 006 prefix")
}

func TestSFKeyPrefixMatch_KnownPrefixIDNotInTable(t *testing.T) {
	// UUID does not exist in Opportunity; flow should correctly see no match.
	stub := &sfPrefixQueryStub{records: map[string][]models.SObject{}}
	e := newSFPrefixExecutor(stub)
	matched, handled := e.sfKeyPrefixMatch(context.Background(), "not-an-opp-id", "006")
	assert.True(t, handled)
	assert.False(t, matched)
}

func TestSFKeyPrefixMatch_UnknownPrefixFallsThrough(t *testing.T) {
	stub := &sfPrefixQueryStub{}
	e := newSFPrefixExecutor(stub)
	_, handled := e.sfKeyPrefixMatch(context.Background(), "any-value", "ZZZ")
	assert.False(t, handled, "unknown prefix must fall through to literal match")
}

func TestSFKeyPrefixMatch_PrefixNot3CharsFallsThrough(t *testing.T) {
	stub := &sfPrefixQueryStub{}
	e := newSFPrefixExecutor(stub)
	_, handled := e.sfKeyPrefixMatch(context.Background(), "any", "00")
	assert.False(t, handled)
	_, handled = e.sfKeyPrefixMatch(context.Background(), "any", "0061")
	assert.False(t, handled)
}

func TestSFKeyPrefixMatch_LiteralMatchDefersToString(t *testing.T) {
	// When the left value genuinely starts with the SF prefix (e.g. someone
	// imported real SF ids), defer to literal StartsWith so behaviour is
	// preserved and we don't do a wasted DB round-trip.
	stub := &sfPrefixQueryStub{}
	e := newSFPrefixExecutor(stub)
	_, handled := e.sfKeyPrefixMatch(context.Background(), "006abc12345XYZAB", "006")
	assert.False(t, handled)
}

func TestSFKeyPrefixMatch_NonStringLeftValueFallsThrough(t *testing.T) {
	stub := &sfPrefixQueryStub{}
	e := newSFPrefixExecutor(stub)
	_, handled := e.sfKeyPrefixMatch(context.Background(), 42, "006")
	assert.False(t, handled)
	_, handled = e.sfKeyPrefixMatch(context.Background(), nil, "006")
	assert.False(t, handled)
}

func TestSFKeyPrefixMatch_QueryErrorFallsThrough(t *testing.T) {
	// DB error → don't assume "no match"; fall through to literal string so a
	// transient query failure does not flip a decision outcome.
	stub := &sfPrefixQueryStub{err: errors.New("db down")}
	e := newSFPrefixExecutor(stub)
	_, handled := e.sfKeyPrefixMatch(context.Background(), "some-id", "006")
	assert.False(t, handled)
}

// TestEvaluateConditionCtx_SFKeyPrefixStartsWith verifies the end-to-end
// integration: a Decision condition `$Record.WhatId StartsWith "006"` against
// a Task whose WhatId is a UUID pointing at a real Opportunity must evaluate
// to true — this is the precise scenario flagged in issue #1749.
func TestEvaluateConditionCtx_SFKeyPrefixStartsWith(t *testing.T) {
	oppID := "4a8db94d-2e54-478c-a7cb-c5dc01fd5d0f"
	stub := &sfPrefixQueryStub{
		records: map[string][]models.SObject{
			"Opportunity|" + oppID: {{"Id": oppID}},
		},
	}
	e := newSFPrefixExecutor(stub)
	interview := &models.FlowInterview{
		ContextData: map[string]interface{}{
			"_objectType": "Task",
			"$Record":     models.SObject{"Id": "task-1", "WhatId": oppID},
		},
	}
	cond := &models.FlowCondition{
		LeftValueReference: "$Record.WhatId",
		Operator:           "StartsWith",
		RightValue:         map[string]interface{}{"stringValue": "006"},
	}
	assert.True(t, e.evaluateConditionCtx(context.Background(), interview, nil, cond),
		"decision with SF prefix 006 must match Task whose WhatId resolves to Opportunity")
}

// TestCheckEntryConditionsCtx_SFKeyPrefixStartFilter verifies that a Start
// filter `WhatId StartsWith "006"` (used by Task_Hellowin_Feedback_Is_Resuired)
// resolves correctly when the Task's WhatId is a UUID pointing at an
// Opportunity. Before the Start-filter path was routed through
// sfKeyPrefixMatch, this check would fail silently and the flow would never
// dispatch on UUID-style IDs.
func TestCheckEntryConditionsCtx_SFKeyPrefixStartFilter(t *testing.T) {
	oppID := "11111111-1111-1111-1111-111111111111"
	stub := &sfPrefixQueryStub{
		records: map[string][]models.SObject{
			"Opportunity|" + oppID: {{"Id": oppID}},
		},
	}
	e := newSFPrefixExecutor(stub)
	flowMeta := &models.FlowMetadata{
		Start: &models.FlowStart{
			Object: "Task",
			Filters: []models.FlowRecordFilter{
				{
					Field:    "WhatId",
					Operator: "StartsWith",
					Value:    map[string]interface{}{"stringValue": "006"},
				},
			},
			FilterLogic: "and",
		},
	}
	ctxData := map[string]interface{}{
		"$Record": models.SObject{"Id": "task-1", "WhatId": oppID},
	}
	assert.True(t, e.checkEntryConditionsCtx(context.Background(), flowMeta, ctxData),
		"Start filter with SF prefix 006 must match Task whose WhatId resolves to Opportunity")

	// Negative: WhatId does not point to an Opportunity → filter must reject.
	contactID := "22222222-2222-2222-2222-222222222222"
	stub.records["Contact|"+contactID] = []models.SObject{{"Id": contactID}}
	ctxDataNeg := map[string]interface{}{
		"$Record": models.SObject{"Id": "task-2", "WhatId": contactID},
	}
	assert.False(t, e.checkEntryConditionsCtx(context.Background(), flowMeta, ctxDataNeg),
		"Start filter must reject when WhatId resolves to a Contact rather than Opportunity")
}

// TestCheckEntryConditions_NonCtxPreservesLegacy confirms that callers who
// continue to go through the non-ctx entry point keep the literal-string
// StartsWith semantics — i.e. we have not silently regressed any test shim
// or external caller that does not pass a context.
func TestCheckEntryConditions_NonCtxPreservesLegacy(t *testing.T) {
	stub := &sfPrefixQueryStub{}
	e := newSFPrefixExecutor(stub)
	flowMeta := &models.FlowMetadata{
		Start: &models.FlowStart{
			Object: "Task",
			Filters: []models.FlowRecordFilter{
				{
					Field:    "WhatId",
					Operator: "StartsWith",
					Value:    map[string]interface{}{"stringValue": "006"},
				},
			},
			FilterLogic: "and",
		},
	}
	// Non-ctx path, UUID does not start with "006" literally → must return false
	// (legacy behaviour; prefix resolution is opt-in via the Ctx variant).
	ctxData := map[string]interface{}{
		"$Record": models.SObject{"Id": "task-1", "WhatId": "4a8db94d-2e54-478c-a7cb-c5dc01fd5d0f"},
	}
	assert.False(t, e.checkEntryConditions(flowMeta, ctxData),
		"legacy non-ctx path must keep literal StartsWith semantics (no DB lookup, no regression)")
}

func TestEvaluateConditionCtx_SFKeyPrefixDoesNotStartWith(t *testing.T) {
	// Same UUID points to a Contact; "006" (Opportunity) should NOT match.
	contactID := "4a8db94d-2e54-478c-a7cb-c5dc01fd5d0f"
	stub := &sfPrefixQueryStub{
		records: map[string][]models.SObject{
			"Contact|" + contactID: {{"Id": contactID}},
		},
	}
	e := newSFPrefixExecutor(stub)
	interview := &models.FlowInterview{
		ContextData: map[string]interface{}{
			"_objectType": "Task",
			"$Record":     models.SObject{"Id": "task-1", "WhatId": contactID},
		},
	}
	cond := &models.FlowCondition{
		LeftValueReference: "$Record.WhatId",
		Operator:           "StartsWith",
		RightValue:         map[string]interface{}{"stringValue": "006"},
	}
	assert.False(t, e.evaluateConditionCtx(context.Background(), interview, nil, cond))

	condNeg := &models.FlowCondition{
		LeftValueReference: "$Record.WhatId",
		Operator:           "DoesNotStartWith",
		RightValue:         map[string]interface{}{"stringValue": "006"},
	}
	assert.True(t, e.evaluateConditionCtx(context.Background(), interview, nil, condNeg),
		"DoesNotStartWith must be the negation of StartsWith under prefix resolution")
}
