package queryspec

import (
	"strings"
	"testing"
)

func TestPredicateSeparatesFormulaAndStructuredCriteria(t *testing.T) {
	filter := And(
		FormulaPredicate("Name = 'Acme'"),
		CriteriaPredicate([]QueryCriterion{{Field: "Industry", Op: "=", Val: "Tech"}}),
	)

	lowering, err := LowerPredicate(filter, PredicateLoweringPreferCriteria)
	if err != nil {
		t.Fatalf("LowerPredicate() error = %v", err)
	}
	if lowering.Expression != "(Name = 'Acme')" {
		t.Fatalf("LowerPredicate().Expression = %q, want formula-only residual", lowering.Expression)
	}
	criteria := lowering.Criteria
	if len(criteria) != 1 || criteria[0].Field != "Industry" || criteria[0].Val != "Tech" {
		t.Fatalf("LowerPredicate().Criteria = %#v", criteria)
	}
}

func TestNeutralQuerySpecKeepsSubqueryShape(t *testing.T) {
	spec := QuerySpec{
		ObjectAPIName: "Account",
		SelectFields:  []string{"Id", "Name"},
		Filter:        CriteriaPredicate([]QueryCriterion{{Field: "OwnerId", Op: "=", Val: "005-user"}}),
		OrderBy:       []SortCriterion{{Field: "CreatedDate", Direction: "ASC"}},
		GroupBy:       "OwnerId",
		Having:        FormulaPredicate("COUNT(Id) > 1"),
		SubQueries: []SubQuerySpec{{
			RelationshipName: "Contacts",
			SelectFields:     []string{"Id"},
			Filter:           FormulaPredicate("LastName != nil"),
			OrderBy:          []SortCriterion{{Field: "LastName", Direction: "DESC"}},
			Limit:            5,
		}},
	}

	if spec.ObjectAPIName != "Account" || predicateExpression(t, spec.Having) != "COUNT(Id) > 1" {
		t.Fatalf("spec basic fields = %#v", spec)
	}
	if len(spec.OrderBy) != 1 || spec.OrderBy[0].Field != "CreatedDate" {
		t.Fatalf("orderBy = %#v", spec.OrderBy)
	}
	if len(spec.SubQueries) != 1 || predicateExpression(t, spec.SubQueries[0].Filter) != "LastName != nil" || spec.SubQueries[0].OrderBy[0].Field != "LastName" {
		t.Fatalf("subQueries = %#v", spec.SubQueries)
	}
}

func TestReferenceHydrationModeIsClosed(t *testing.T) {
	for _, mode := range []ReferenceHydrationMode{ReferenceHydrationDefault, ReferenceHydrationExplicitOnly} {
		if !mode.Valid() {
			t.Fatalf("supported mode %v is invalid", mode)
		}
	}
	if ReferenceHydrationMode(255).Valid() {
		t.Fatal("unknown reference hydration mode is valid")
	}
}

func TestNeutralSearchSpecUsesPredicateTree(t *testing.T) {
	spec := SearchSpec{
		ObjectAPIName: "Account",
		Term:          "acme",
		SearchFields:  []string{"Name"},
		ReturnFields:  []string{"Id", "Name"},
		Filter:        CriteriaPredicate([]QueryCriterion{{Field: "OwnerId", Op: "=", Val: "005-user"}}),
		SortField:     "Name",
		SortDirection: "ASC",
		Limit:         10,
		Offset:        5,
	}

	lowering, err := LowerPredicate(spec.Filter, PredicateLoweringPreferCriteria)
	if err != nil {
		t.Fatalf("LowerPredicate() error = %v", err)
	}
	if lowering.Expression != "" {
		t.Fatalf("SearchSpec filter residual expression = %q, want structured criteria only", lowering.Expression)
	}
	criteria := lowering.Criteria
	if len(criteria) != 1 || criteria[0].Field != "OwnerId" || criteria[0].Val != "005-user" {
		t.Fatalf("SearchSpec criteria = %#v", criteria)
	}
}

func TestCorrelatedMembershipPredicateCarriesNeutralShape(t *testing.T) {
	inner := FieldPredicate("StageName", "=", "Closed Won")

	predicate := CorrelatedMembershipPredicate(CorrelatedMembership{
		ID:                 "semi_0",
		OuterField:         "WhatId",
		Negate:             true,
		InnerObjectAPIName: "Opportunity",
		InnerSelectField:   "Id",
		InnerFilter:        inner,
		InnerLimit:         25,
		InnerScanAll:       true,
	})

	if predicate.Kind != PredicateCorrelatedMembership || predicate.CorrelatedMembership == nil {
		t.Fatalf("predicate = %#v, want correlated-membership predicate", predicate)
	}
	got := predicate.CorrelatedMembership
	if got.ID != "semi_0" || got.OuterField != "WhatId" || !got.Negate {
		t.Fatalf("outer membership = %#v, want negated semi_0 on WhatId", got)
	}
	if got.InnerObjectAPIName != "Opportunity" || got.InnerSelectField != "Id" {
		t.Fatalf("inner membership = %#v, want Opportunity.Id", got)
	}
	if got.InnerLimit != 25 || !got.InnerScanAll {
		t.Fatalf("inner scope = limit %d scanAll %v, want 25/true", got.InnerLimit, got.InnerScanAll)
	}
	if got.InnerFilter.Kind != PredicateFieldComparison || got.InnerFilter.Field != "StageName" {
		t.Fatalf("inner filter = %#v, want StageName field predicate", got.InnerFilter)
	}
}

func TestCorrelatedMembershipPredicateRejectsIncompleteShape(t *testing.T) {
	if predicate := CorrelatedMembershipPredicate(CorrelatedMembership{
		OuterField:         "WhatId",
		InnerObjectAPIName: "Opportunity",
	}); predicate.Kind != PredicateNone {
		t.Fatalf("predicate = %#v, want empty predicate without inner selected field", predicate)
	}
}

func TestLowerPredicateRejectsCorrelatedMembershipUntilExecutorOwnsIt(t *testing.T) {
	predicate := CorrelatedMembershipPredicate(CorrelatedMembership{
		ID:                 "semi_0",
		OuterField:         "WhatId",
		InnerObjectAPIName: "Opportunity",
		InnerSelectField:   "Id",
	})

	for _, mode := range []PredicateLoweringMode{PredicateLoweringPreferCriteria, PredicateLoweringExpressionOnly} {
		if _, err := LowerPredicate(predicate, mode); err == nil {
			t.Fatalf("LowerPredicate(%s) error = nil, want unsupported correlated-membership rejection", mode)
		}
	}
}

func TestUnsupportedPredicateFailsClosedWithReason(t *testing.T) {
	predicate := UnsupportedPredicate("deferred predicate execution")
	if predicate.Kind != PredicateUnsupported {
		t.Fatalf("predicate = %#v, want unsupported predicate", predicate)
	}

	for _, mode := range []PredicateLoweringMode{PredicateLoweringPreferCriteria, PredicateLoweringExpressionOnly} {
		if _, err := LowerPredicate(predicate, mode); err == nil || !strings.Contains(err.Error(), "deferred predicate execution") {
			t.Fatalf("LowerPredicate(%s) error = %v, want unsupported reason", mode, err)
		}
	}
}

func TestLowerPredicateExpressionOnlyProjectsStructuredComparisons(t *testing.T) {
	predicate := And(
		FieldPredicate("OwnerId", "=", "005-user"),
		FieldPredicate("IsActive", "=", true),
		FieldPredicate("Rank", ">", 3),
		FieldPredicate("ParentId", "=", nil),
	)

	lowering, err := LowerPredicate(predicate, PredicateLoweringExpressionOnly)
	if err != nil {
		t.Fatalf("LowerPredicate() error = %v", err)
	}
	want := "(OwnerId = '005-user') AND (IsActive = true) AND (Rank > 3) AND (ParentId = nil)"
	if lowering.Expression != want {
		t.Fatalf("LowerPredicate().Expression = %q, want %q", lowering.Expression, want)
	}

	preferred, err := LowerPredicate(predicate, PredicateLoweringPreferCriteria)
	if err != nil {
		t.Fatalf("LowerPredicate(prefer criteria) error = %v", err)
	}
	if preferred.Expression != "(ParentId = nil)" || len(preferred.Criteria) != 3 {
		t.Fatalf("LowerPredicate(prefer criteria) = %#v, want three criteria plus nil residual", preferred)
	}
}

func TestLowerPredicateRendersAndCarriesListValues(t *testing.T) {
	predicate := FieldPredicate("OwnerId", "IN", []string{"005-user"})

	lowering, err := LowerPredicate(predicate, PredicateLoweringExpressionOnly)
	if err != nil {
		t.Fatalf("LowerPredicate(expression only) error = %v", err)
	}
	if lowering.Expression != "OwnerId IN ['005-user']" {
		t.Fatalf("LowerPredicate(expression only).Expression = %q", lowering.Expression)
	}

	preferred, err := LowerPredicate(predicate, PredicateLoweringPreferCriteria)
	if err != nil {
		t.Fatalf("LowerPredicate(prefer criteria) error = %v", err)
	}
	values, ok := preferred.Criteria[0].Val.([]interface{})
	if !ok || len(values) != 1 || values[0] != "005-user" {
		t.Fatalf("LowerPredicate(prefer criteria).Criteria = %#v", preferred.Criteria)
	}
}

func TestLowerPredicateRendersLikeComparisonsAsInternalResidualCalls(t *testing.T) {
	predicate := And(
		FieldPredicate("IsActive", "=", true),
		Or(
			ComparisonPredicate(FieldOperand("Body"), "LIKE", LiteralOperand("%Record.Link%")),
			ComparisonPredicate(FieldOperand("HtmlValue"), "NOT LIKE", LiteralOperand("prefix-%@example.com")),
		),
	)

	lowering, err := LowerPredicate(predicate, PredicateLoweringPreferCriteria)
	if err != nil {
		t.Fatalf("LowerPredicate() error = %v", err)
	}
	if len(lowering.Criteria) != 1 || lowering.Criteria[0] != (QueryCriterion{Field: "IsActive", Op: "=", Val: true}) {
		t.Fatalf("LowerPredicate().Criteria = %#v", lowering.Criteria)
	}
	want := "((__NCRM_QUERY_LIKE__(Body, '%Record.Link%')) OR (not (__NCRM_QUERY_LIKE__(HtmlValue, 'prefix-%@example.com'))))"
	if lowering.Expression != want {
		t.Fatalf("LowerPredicate().Expression = %q, want %q", lowering.Expression, want)
	}
}

func TestLowerPredicateKeepsLikeSyntaxForExpressionOnlyExecution(t *testing.T) {
	predicate := Or(
		ComparisonPredicate(FieldOperand("Body"), "LIKE", LiteralOperand("%Record.Link%")),
		ComparisonPredicate(FieldOperand("HtmlValue"), "NOT LIKE", LiteralOperand("prefix-%@example.com")),
	)

	lowering, err := LowerPredicate(predicate, PredicateLoweringExpressionOnly)
	if err != nil {
		t.Fatalf("LowerPredicate() error = %v", err)
	}
	want := "(Body LIKE '%Record.Link%') OR (HtmlValue NOT LIKE 'prefix-%@example.com')"
	if lowering.Expression != want {
		t.Fatalf("LowerPredicate().Expression = %q, want %q", lowering.Expression, want)
	}
}

func TestExactNumericLiteralRendersUnquotedAndUsesStringCriteria(t *testing.T) {
	predicate := ComparisonPredicate(FieldOperand("Amount"), ">", LiteralOperand(NumericLiteral("12345678901234567890.1200")))

	expression, err := LowerPredicate(predicate, PredicateLoweringExpressionOnly)
	if err != nil {
		t.Fatalf("LowerPredicate(expression) error = %v", err)
	}
	if expression.Expression != "Amount > 12345678901234567890.1200" {
		t.Fatalf("expression = %q", expression.Expression)
	}

	criteria, err := LowerPredicate(predicate, PredicateLoweringPreferCriteria)
	if err != nil {
		t.Fatalf("LowerPredicate(criteria) error = %v", err)
	}
	if len(criteria.Criteria) != 1 || criteria.Criteria[0].Val != "12345678901234567890.1200" {
		t.Fatalf("criteria = %#v", criteria.Criteria)
	}

	invalid := ComparisonPredicate(FieldOperand("Amount"), ">", LiteralOperand(NumericLiteral("1 OR true")))
	if _, err := LowerPredicate(invalid, PredicateLoweringExpressionOnly); err == nil {
		t.Fatal("invalid exact numeric literal was rendered")
	}
}

func TestLowerPredicateExpressionOnlyEscapesBackslashBeforeQuote(t *testing.T) {
	lowering, err := LowerPredicate(FieldPredicate("Name", "=", `C:\Temp's`), PredicateLoweringExpressionOnly)
	if err != nil {
		t.Fatalf("LowerPredicate() error = %v", err)
	}
	want := `Name = 'C:\\Temp\'s'`
	if lowering.Expression != want {
		t.Fatalf("LowerPredicate().Expression = %q, want %q", lowering.Expression, want)
	}
}

func TestGlobalSearchSpecIsTransportFree(t *testing.T) {
	spec := GlobalSearchSpec{Term: "acme"}
	if spec.Term != "acme" {
		t.Fatalf("GlobalSearchSpec term = %q", spec.Term)
	}
}

func predicateExpression(t *testing.T, predicate Predicate) string {
	t.Helper()
	lowering, err := LowerPredicate(predicate, PredicateLoweringExpressionOnly)
	if err != nil {
		t.Fatalf("LowerPredicate(expression only) error = %v", err)
	}
	return lowering.Expression
}
