package soql

import (
	"strings"
	"testing"
)

// Substrate invariant: ParseSOQL is strict by default. Any SOQL caller that
// has not opted into semi-join handling must fail-closed at parse time
// instead of receiving a request that carries an unresolved placeholder
// in FilterExpr (which would silently return wrong rows when dispatched).
//
// This test exists because there is no good place inside the resolver
// or any single handler to enforce "every ParseSOQL caller wires the
// resolver step." Pushing the gate into ParseSOQL itself makes the
// invariant local: a non-opted-in caller cannot, by construction, see
// a request with SemiJoins != nil.
func TestParseSOQL_StrictByDefaultRejectsSemiJoin(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity WHERE Name = 'Acme')`

	if _, err := ParseSOQL(q); err == nil {
		t.Fatal("ParseSOQL must reject semi-join by default; got nil error")
	} else if !strings.Contains(err.Error(), "semi-join") {
		t.Fatalf("error must name semi-join; got: %v", err)
	}

	// Symmetric anti-join shape.
	qNot := `SELECT Id FROM Account WHERE Id NOT IN (SELECT AccountId FROM Contact)`
	if _, err := ParseSOQL(qNot); err == nil {
		t.Fatal("ParseSOQL must reject NOT IN semi-join by default; got nil error")
	}
}

// Opt-in path keeps semi-join syntax accepted. Handling SemiJoins is the
// caller's contract once SemiJoins is non-empty.
func TestParseSOQLWith_AllowSemiJoinAccepts(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity)`

	req, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	if err != nil {
		t.Fatalf("opt-in parse: %v", err)
	}
	if len(req.SemiJoins) != 1 {
		t.Fatalf("want 1 SemiJoin, got %d", len(req.SemiJoins))
	}
}

// Strict default must NOT regress non-semi-join SOQL — every caller that
// doesn't opt in still needs to parse normal queries. This is the
// regression check for the substrate change.
func TestParseSOQL_StrictDefaultStillParsesPlainQueries(t *testing.T) {
	cases := []string{
		`SELECT Id FROM Account`,
		`SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 10`,
		`SELECT Id FROM Account WHERE Id IN ('001A', '001B')`, // literal IN-list, not semi-join
		`SELECT Id FROM Account WHERE Id NOT IN ('001A')`,
		`SELECT Id, COUNT(Id) FROM Account GROUP BY Id HAVING COUNT(Id) > 1`,
		`SELECT Id, (SELECT Id FROM Contacts) FROM Account`, // parent-child subquery
	}
	for _, q := range cases {
		t.Run(q, func(t *testing.T) {
			if _, err := ParseSOQL(q); err != nil {
				t.Fatalf("strict-default parse must accept non-semi-join SOQL %q: %v", q, err)
			}
		})
	}
}
