package soql

import (
	"strings"
	"testing"
)

func TestSemiJoin_BasicIN(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity WHERE Name = 'Acme')`
	req, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err != nil {
		t.Fatalf("ParseSOQL: %v", err)
	}
	if len(req.SemiJoins) != 1 {
		t.Fatalf("want 1 semi-join, got %d", len(req.SemiJoins))
	}
	c := req.SemiJoins[0]
	if c.OuterField != "OpportunityId" {
		t.Fatalf("OuterField = %q, want OpportunityId", c.OuterField)
	}
	if c.Negate {
		t.Fatalf("Negate = true, want false")
	}
	if c.InnerQuery.ObjectAPIName != "Opportunity" {
		t.Fatalf("inner ObjectAPIName = %q, want Opportunity", c.InnerQuery.ObjectAPIName)
	}
	if len(c.InnerQuery.SelectFields) != 1 || c.InnerQuery.SelectFields[0] != "Id" {
		t.Fatalf("inner SelectFields = %v, want [Id]", c.InnerQuery.SelectFields)
	}
	innerExpr := predicateExpression(t, c.InnerQuery.Filter)
	if !strings.Contains(innerExpr, "Name") {
		t.Fatalf("inner predicate missing Name: %q", innerExpr)
	}
	outerExpr := predicateExpression(t, req.Filter)
	if !strings.Contains(outerExpr, SemiJoinPlaceholderToken(0)) {
		t.Fatalf("outer predicate missing placeholder: %q", outerExpr)
	}
}

func TestSemiJoin_NOT_IN(t *testing.T) {
	q := `SELECT Id FROM Account WHERE Id NOT IN (SELECT AccountId FROM Contact WHERE Active__c = true)`
	req, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err != nil {
		t.Fatalf("ParseSOQL: %v", err)
	}
	if len(req.SemiJoins) != 1 {
		t.Fatalf("want 1 semi-join, got %d", len(req.SemiJoins))
	}
	if !req.SemiJoins[0].Negate {
		t.Fatalf("Negate = false, want true")
	}
	if expr := predicateExpression(t, req.Filter); !strings.Contains(expr, "not in") {
		t.Fatalf("outer predicate missing 'not in': %q", expr)
	}
}

func TestSemiJoin_InnerLimit(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity LIMIT 5)`
	req, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err != nil {
		t.Fatalf("ParseSOQL: %v", err)
	}
	if req.SemiJoins[0].InnerQuery.Limit != 5 {
		t.Fatalf("inner Limit = %d, want 5", req.SemiJoins[0].InnerQuery.Limit)
	}
}

func TestSemiJoin_MultiFieldRejected(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id, Name FROM Opportunity)`
	_, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err == nil {
		t.Fatal("want error for multi-field semi-join inner SELECT")
	}
	if !strings.Contains(err.Error(), "exactly one field") {
		t.Fatalf("want 'exactly one field' error, got: %v", err)
	}
}

func TestSemiJoin_NestedRejected(t *testing.T) {
	q := `SELECT Id FROM A WHERE Id IN (SELECT Id FROM B WHERE Id IN (SELECT Id FROM C))`
	_, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err == nil {
		t.Fatal("want error for nested semi-join")
	}
	if !strings.Contains(err.Error(), "nested semi-join") {
		t.Fatalf("want 'nested semi-join' error, got: %v", err)
	}
}

func TestSemiJoin_PlaceholderTokenIndexing(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity) AND AccountId IN (SELECT Id FROM Account)`
	req, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err != nil {
		t.Fatalf("ParseSOQL: %v", err)
	}
	if len(req.SemiJoins) != 2 {
		t.Fatalf("want 2 semi-joins, got %d", len(req.SemiJoins))
	}
	if req.SemiJoins[0].Index != 0 || req.SemiJoins[1].Index != 1 {
		t.Fatalf("indices = [%d %d], want [0 1]", req.SemiJoins[0].Index, req.SemiJoins[1].Index)
	}
	expr := predicateExpression(t, req.Filter)
	if !strings.Contains(expr, SemiJoinPlaceholderToken(0)) ||
		!strings.Contains(expr, SemiJoinPlaceholderToken(1)) {
		t.Fatalf("predicate missing both placeholder tokens: %q", expr)
	}
}

// Regression: existing literal IN list path must still work after the
// semi-join branch was added in parseInfixExpression.
func TestSemiJoin_LiteralINUnchanged(t *testing.T) {
	q := `SELECT Id FROM Account WHERE Id IN ('a', 'b', 'c')`
	req, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err != nil {
		t.Fatalf("ParseSOQL: %v", err)
	}
	if len(req.SemiJoins) != 0 {
		t.Fatalf("literal IN must not register a semi-join, got %d", len(req.SemiJoins))
	}
	expr := predicateExpression(t, req.Filter)
	if !strings.Contains(expr, "'a'") || !strings.Contains(expr, "'b'") || !strings.Contains(expr, "'c'") {
		t.Fatalf("literal IN list missing or rewritten: %q", expr)
	}
}

// Regression: empty literal IN list must still produce empty ListLiteral.
func TestSemiJoin_EmptyINUnchanged(t *testing.T) {
	q := `SELECT Id FROM Account WHERE Id IN ()`
	req, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err != nil {
		t.Fatalf("ParseSOQL: %v", err)
	}
	if len(req.SemiJoins) != 0 {
		t.Fatalf("empty IN must not register a semi-join, got %d", len(req.SemiJoins))
	}
}

// Semi-join inside a parent-child subquery's WHERE must be rejected at parse
// time. The resolver only substitutes placeholders in the top-level WHERE
// predicate — allowing this would silently leak the parser placeholder into
// child subquery filtering and produce wrong child rows with no error.
func TestSemiJoin_InsideChildSubqueryRejected(t *testing.T) {
	q := `SELECT Id, (SELECT Id FROM Contacts WHERE AccountId IN (SELECT Id FROM Account WHERE Name = 'X')) FROM Account`
	_, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err == nil {
		t.Fatal("want error for semi-join inside parent-child subquery WHERE")
	}
	if !strings.Contains(err.Error(), "parent-child subqueries") {
		t.Fatalf("want 'parent-child subqueries' error, got: %v", err)
	}
}

// Semi-join inside a HAVING clause must be rejected at parse time for the
// same reason — placeholder lands in HAVING and the resolver only substitutes
// top-level WHERE semi-joins.
func TestSemiJoin_InsideHavingRejected(t *testing.T) {
	q := `SELECT Id, COUNT(Id) FROM Account GROUP BY Id HAVING Id IN (SELECT Id FROM Account WHERE Name = 'X')`
	_, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err == nil {
		t.Fatal("want error for semi-join inside HAVING clause")
	}
	if !strings.Contains(err.Error(), "HAVING") {
		t.Fatalf("want 'HAVING' error, got: %v", err)
	}
}

// Inner ORDER BY is rejected at parse time. The persistence layer only
// honors single-column SortField/SortDirection (query_repository.go),
// so an OrderBy array would be silently dropped — combined with LIMIT
// this produces wrong outer rows. Failing closed at parse keeps the
// inner-query contract narrow and explicit; the resolver-side
// assertInnerQueryContract is the runtime backstop.
func TestSemiJoin_InnerOrderByRejected(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity ORDER BY CreatedDate DESC LIMIT 10)`
	_, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err == nil {
		t.Fatal("want error for inner ORDER BY")
	}
	if !strings.Contains(err.Error(), "ORDER BY") {
		t.Fatalf("want ORDER BY error, got: %v", err)
	}
}

// A regular semi-join in the outer WHERE must continue to parse even when the
// query also has a parent-child subquery in the SELECT field list.
func TestSemiJoin_OuterWhereWithChildSubqueryInSelect(t *testing.T) {
	q := `SELECT Id, (SELECT Id FROM Contacts) FROM Account WHERE Id IN (SELECT AccountId FROM Opportunity WHERE Name = 'Acme')`
	req, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err != nil {
		t.Fatalf("ParseSOQL: %v", err)
	}
	if len(req.SemiJoins) != 1 {
		t.Fatalf("want 1 outer semi-join, got %d", len(req.SemiJoins))
	}
	if len(req.SubQueries) != 1 {
		t.Fatalf("want 1 child subquery, got %d", len(req.SubQueries))
	}
}
