package soql

import (
	"context"
	"errors"
	"strings"
	"testing"

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

type stubResolver struct {
	ids   []string
	err   error
	calls int
}

func (s *stubResolver) ResolveSemiJoinIDs(ctx context.Context, inner *models.QueryRequest) ([]string, error) {
	s.calls++
	if s.err != nil {
		return nil, s.err
	}
	return s.ids, nil
}

// per-clause resolver: returns different IDs per call to support multi-clause tests.
type seqResolver struct {
	results [][]string
	idx     int
}

func (s *seqResolver) ResolveSemiJoinIDs(ctx context.Context, inner *models.QueryRequest) ([]string, error) {
	r := s.results[s.idx]
	s.idx++
	return r, nil
}

func TestResolveSemiJoins_NilOrEmptyIsNoOp(t *testing.T) {
	if err := ResolveSemiJoins(context.Background(), nil, nil); err != nil {
		t.Fatalf("nil req: want no-op, got %v", err)
	}
	req := &models.QueryRequest{ObjectAPIName: "Account", FilterExpr: "Name == 'X'"}
	if err := ResolveSemiJoins(context.Background(), req, nil); err != nil {
		t.Fatalf("empty SemiJoins: want no-op, got %v", err)
	}
	if req.FilterExpr != "Name == 'X'" {
		t.Fatalf("FilterExpr changed: %q", req.FilterExpr)
	}
}

func TestResolveSemiJoins_SubstitutesIDs(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("parse: %v", err)
	}
	res := &stubResolver{ids: []string{"006A", "006B"}}
	if err := ResolveSemiJoins(context.Background(), req, res); err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if len(req.SemiJoins) != 0 {
		t.Fatalf("SemiJoins not cleared after resolve: %v", req.SemiJoins)
	}
	if req.FilterExpr != "" {
		t.Fatalf("FilterExpr = %q, want parser path to keep legacy string empty", req.FilterExpr)
	}
	if req.Filter.Kind != queryspec.PredicateComparison {
		t.Fatalf("Filter.Kind = %q, want comparison predicate", req.Filter.Kind)
	}
	expr := predicateExpression(t, req.Filter)
	if !strings.Contains(expr, "['006A','006B']") || strings.Contains(expr, "__nx_semijoin_") {
		t.Fatalf("predicate filter not substituted: %q", expr)
	}
}

func TestResolveSemiJoins_EmptyIDs_INSubstitutesFalse(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity WHERE Name = 'Nope')`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	res := &stubResolver{ids: nil}
	if err := ResolveSemiJoins(context.Background(), req, res); err != nil {
		t.Fatalf("resolve: %v", err)
	}
	expr := predicateExpression(t, req.Filter)
	if !strings.Contains(expr, "false") {
		t.Fatalf("empty IN should substitute 'false', got: %q", expr)
	}
	if strings.Contains(expr, "OpportunityId in") {
		t.Fatalf("the IN binary expr should be replaced wholesale: %q", expr)
	}
}

func TestResolveSemiJoins_EmptyIDs_NotINSubstitutesTrue(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId NOT IN (SELECT Id FROM Opportunity WHERE Name = 'Nope')`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	res := &stubResolver{ids: nil}
	if err := ResolveSemiJoins(context.Background(), req, res); err != nil {
		t.Fatalf("resolve: %v", err)
	}
	expr := predicateExpression(t, req.Filter)
	if !strings.Contains(expr, "true") {
		t.Fatalf("empty NOT IN should substitute 'true', got: %q", expr)
	}
	if strings.Contains(expr, "OpportunityId not in") {
		t.Fatalf("the NOT IN binary expr should be replaced wholesale: %q", expr)
	}
}

func TestResolveSemiJoins_MultipleClauses(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity) AND AccountId IN (SELECT Id FROM Account)`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	res := &seqResolver{results: [][]string{{"006A"}, {"001X", "001Y"}}}
	if err := ResolveSemiJoins(context.Background(), req, res); err != nil {
		t.Fatalf("resolve: %v", err)
	}
	expr := predicateExpression(t, req.Filter)
	if !strings.Contains(expr, "['006A']") {
		t.Fatalf("first clause not substituted: %q", expr)
	}
	if !strings.Contains(expr, "['001X','001Y']") {
		t.Fatalf("second clause not substituted: %q", expr)
	}
}

func TestResolveSemiJoins_InnerErrorPropagates(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity)`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	res := &stubResolver{err: errors.New("boom")}
	err := ResolveSemiJoins(context.Background(), req, res)
	if err == nil {
		t.Fatal("want error from inner failure")
	}
	if !strings.Contains(err.Error(), "OpportunityId") {
		t.Fatalf("want error to name OuterField, got: %v", err)
	}
}

func TestResolveSemiJoins_LimitExceeded(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity)`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	huge := make([]string, MaxSemiJoinResultIDs+1)
	for i := range huge {
		huge[i] = "id"
	}
	res := &stubResolver{ids: huge}
	err := ResolveSemiJoins(context.Background(), req, res)
	if err == nil || !strings.Contains(err.Error(), "exceeding limit") {
		t.Fatalf("want limit-exceeded error, got: %v", err)
	}
}

func TestResolveSemiJoins_NoResolverButHasClauses(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity)`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	err := ResolveSemiJoins(context.Background(), req, nil)
	if err == nil {
		t.Fatal("want error when resolver is nil but clauses exist")
	}
}

func TestResolveSemiJoins_AllowsUserLiteralResemblingPlaceholder(t *testing.T) {
	cases := []string{
		`SELECT Id FROM Account WHERE Name = '__nx_semijoin_0__'`,
		`SELECT Id FROM Account WHERE Name IN ('__nx_semijoin_0__')`,
	}
	for _, q := range cases {
		t.Run(q, func(t *testing.T) {
			req, err := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
			if err != nil {
				t.Fatalf("parse: %v", err)
			}
			if len(req.SemiJoins) != 0 {
				t.Fatalf("user literal must not register a semi-join: %+v", req.SemiJoins)
			}
			if err := ResolveSemiJoins(context.Background(), req, nil); err != nil {
				t.Fatalf("user literal resembling placeholder should remain a literal, got: %v", err)
			}
		})
	}
}

func TestResolveSemiJoins_AllowsLegacyRawFilterExprCasing(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity)`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	req.FilterExpr = "OpportunityId IN " + SemiJoinPlaceholderBracketed(0)
	res := &stubResolver{ids: []string{"006A"}}

	if err := ResolveSemiJoins(context.Background(), req, res); err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if res.calls != 1 {
		t.Fatalf("resolver calls = %d, want 1", res.calls)
	}
	if !strings.Contains(req.FilterExpr, "['006A']") {
		t.Fatalf("raw FilterExpr was not substituted: %q", req.FilterExpr)
	}
}

func TestResolveSemiJoins_RejectsPlaceholderDriftBeforeDispatch(t *testing.T) {
	cases := []struct {
		name    string
		mutate  func(req *models.QueryRequest)
		wantSub string
	}{
		{
			name: "index",
			mutate: func(req *models.QueryRequest) {
				req.SemiJoins[0].Index = 1
			},
			wantSub: "index drift",
		},
		{
			name: "outer field",
			mutate: func(req *models.QueryRequest) {
				req.SemiJoins[0].OuterField = "AccountId"
			},
			wantSub: "placeholder",
		},
		{
			name: "operator",
			mutate: func(req *models.QueryRequest) {
				req.SemiJoins[0].Negate = true
			},
			wantSub: "placeholder",
		},
		{
			name: "legacy raw filter surface",
			mutate: func(req *models.QueryRequest) {
				req.FilterExpr = "AccountId in " + SemiJoinPlaceholderBracketed(0)
			},
			wantSub: "placeholder",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity)`
			req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
			tc.mutate(req)
			res := &stubResolver{ids: []string{"006A"}}

			err := ResolveSemiJoins(context.Background(), req, res)
			if err == nil {
				t.Fatal("want placeholder drift error")
			}
			if !strings.Contains(err.Error(), tc.wantSub) {
				t.Fatalf("error must name %q, got: %v", tc.wantSub, err)
			}
			if res.calls != 0 {
				t.Fatalf("resolver was dispatched even though placeholder drifted; calls=%d", res.calls)
			}
		})
	}
}

// Defense-in-depth assertion: a placeholder leaked into SubQueries[i].FilterExpr
// (e.g. by a future parser regression) must be detected, not silently passed on.
func TestResolveSemiJoins_ChildSubqueryPlaceholderTripsAssertion(t *testing.T) {
	req := &models.QueryRequest{
		ObjectAPIName: "Account",
		FilterExpr:    "Name == 'X'",
		SubQueries: []models.SubQueryRequest{
			{
				RelationshipName: "Contacts",
				FilterExpr:       "AccountId in ['" + SemiJoinPlaceholderToken(0) + "']",
			},
		},
	}
	err := ResolveSemiJoins(context.Background(), req, nil)
	if err == nil {
		t.Fatal("want error when child subquery FilterExpr still has placeholder")
	}
	if !strings.Contains(err.Error(), "SubQueries") {
		t.Fatalf("error must name SubQueries surface, got: %v", err)
	}
}

func TestResolveSemiJoins_IDWithSingleQuote(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity)`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	res := &stubResolver{ids: []string{"a'b"}}
	if err := ResolveSemiJoins(context.Background(), req, res); err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if expr := predicateExpression(t, req.Filter); !strings.Contains(expr, `['a\'b']`) {
		t.Fatalf("single quote not escaped: %q", expr)
	}
}

// Inner projections that aren't IDs may legitimately contain backslashes;
// expr-lang treats `\` as an escape lead-in, so a raw `\` would consume the
// following character. Verify backslash is escaped before single-quote.
func TestResolveSemiJoins_IDWithBackslash(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity)`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	res := &stubResolver{ids: []string{`a\b`, `c\'d`}}
	if err := ResolveSemiJoins(context.Background(), req, res); err != nil {
		t.Fatalf("resolve: %v", err)
	}
	// `a\b` -> `a\\b`; `c\'d` -> `c\\\'d` (backslash first, then quote)
	if expr := predicateExpression(t, req.Filter); !strings.Contains(expr, `['a\\b','c\\\'d']`) {
		t.Fatalf("backslash not escaped before quote: %q", expr)
	}
}

// Outer ScanAll (queryAll endpoint) must propagate to every inner query so
// soft-deleted rows are visible in semi-join inner sets too.
func TestResolveSemiJoins_PropagatesScanAll(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity)`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	req.ScanAll = true
	captured := false
	res := &funcResolver{fn: func(inner *models.QueryRequest) ([]string, error) {
		captured = inner.ScanAll
		return []string{"006A"}, nil
	}}
	if err := ResolveSemiJoins(context.Background(), req, res); err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if !captured {
		t.Fatal("ScanAll not propagated from outer to inner query")
	}
}

// When the SOQL author omitted LIMIT, the inner query must request more rows
// than MaxSemiJoinResultIDs so the cap check sees overflow rather than the
// persistence layer silently truncating at DefaultSOSLLimit (500).
func TestResolveSemiJoins_DefaultsInnerLimitAboveCap(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity)`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	var capturedLimit int
	res := &funcResolver{fn: func(inner *models.QueryRequest) ([]string, error) {
		capturedLimit = inner.Limit
		return []string{"006A"}, nil
	}}
	if err := ResolveSemiJoins(context.Background(), req, res); err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if capturedLimit != MaxSemiJoinResultIDs+1 {
		t.Fatalf("inner Limit = %d, want %d (cap+1 to detect overflow)", capturedLimit, MaxSemiJoinResultIDs+1)
	}
}

// Author-supplied LIMIT in inner SOQL must be respected, not overridden.
func TestResolveSemiJoins_ExplicitInnerLimitPreserved(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity LIMIT 5)`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	var capturedLimit int
	res := &funcResolver{fn: func(inner *models.QueryRequest) ([]string, error) {
		capturedLimit = inner.Limit
		return []string{"006A"}, nil
	}}
	if err := ResolveSemiJoins(context.Background(), req, res); err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if capturedLimit != 5 {
		t.Fatalf("inner Limit = %d, want 5 (author's LIMIT preserved)", capturedLimit)
	}
}

type funcResolver struct {
	fn func(inner *models.QueryRequest) ([]string, error)
}

func (f *funcResolver) ResolveSemiJoinIDs(ctx context.Context, inner *models.QueryRequest) ([]string, error) {
	return f.fn(inner)
}

// Substrate contract: any inner.QueryRequest field outside the resolver's
// known-good set must trip assertInnerQueryContract and surface as an
// explicit InternalError instead of being silently dispatched. This is
// the structural backstop that makes parser/resolver drift fail-loud.
//
// Each subtest constructs a SemiJoinClause with one out-of-contract field
// set on inner, calls ResolveSemiJoins, and verifies the assertion fires
// before the resolver dispatches.
func TestResolveSemiJoins_InnerContractAssertions(t *testing.T) {
	cases := []struct {
		name    string
		mutate  func(inner *models.QueryRequest)
		wantSub string
	}{
		{
			name:    "OrderBy array rejected",
			mutate:  func(in *models.QueryRequest) { in.OrderBy = []models.SortCriterion{{Field: "Id", Direction: "ASC"}} },
			wantSub: "ORDER BY",
		},
		{
			name:    "SortField rejected",
			mutate:  func(in *models.QueryRequest) { in.SortField = "Id" },
			wantSub: "ORDER BY",
		},
		{
			name:    "GroupBy rejected",
			mutate:  func(in *models.QueryRequest) { in.GroupBy = "Type" },
			wantSub: "GROUP BY",
		},
		{
			name:    "Having rejected",
			mutate:  func(in *models.QueryRequest) { in.Having = "COUNT(Id) > 1" },
			wantSub: "GROUP BY",
		},
		{
			name: "SubQueries rejected",
			mutate: func(in *models.QueryRequest) {
				in.SubQueries = []models.SubQueryRequest{{RelationshipName: "Contacts"}}
			},
			wantSub: "parent-child",
		},
		{
			name: "Criteria rejected",
			mutate: func(in *models.QueryRequest) {
				in.Criteria = []models.QueryCriterion{{Field: "Id", Op: "=", Val: "x"}}
			},
			wantSub: "Criteria",
		},
		{
			name:    "Offset rejected",
			mutate:  func(in *models.QueryRequest) { in.Offset = 5 },
			wantSub: "OFFSET",
		},
		{
			name:    "MRUUserID rejected",
			mutate:  func(in *models.QueryRequest) { in.MRUUserID = "005A" },
			wantSub: "MRU",
		},
		{
			name:    "SelectFields !=1 rejected (zero)",
			mutate:  func(in *models.QueryRequest) { in.SelectFields = nil },
			wantSub: "SelectFields",
		},
		{
			name:    "SelectFields !=1 rejected (two)",
			mutate:  func(in *models.QueryRequest) { in.SelectFields = []string{"Id", "Name"} },
			wantSub: "SelectFields",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			inner := &models.QueryRequest{
				ObjectAPIName: "Opportunity",
				SelectFields:  []string{"Id"},
				FilterExpr:    "Name == 'X'",
			}
			tc.mutate(inner)
			req := &models.QueryRequest{
				ObjectAPIName: "Quote",
				FilterExpr:    "OpportunityId in ['" + SemiJoinPlaceholderToken(0) + "']",
				SemiJoins: []models.SemiJoinClause{{
					OuterField: "OpportunityId",
					InnerQuery: inner,
					Index:      0,
				}},
			}
			res := &stubResolver{ids: []string{"006A"}}
			err := ResolveSemiJoins(context.Background(), req, res)
			if err == nil {
				t.Fatal("want contract assertion to fire")
			}
			if !strings.Contains(err.Error(), tc.wantSub) {
				t.Fatalf("error must name %q in message; got: %v", tc.wantSub, err)
			}
			if res.calls != 0 {
				t.Fatalf("resolver was dispatched even though contract failed; calls=%d", res.calls)
			}
		})
	}
}

// Inner-query failures must preserve the original error type/chain so the
// REST handler's RespondError can classify PermissionError → 403,
// NotFoundError → 404, InternalError → 500. The previous resolver wrapped
// every inner error as a *ValidationError, collapsing the v63 contract to
// 400 MALFORMED_QUERY for every inner failure.
type sentinelInnerErr struct{ msg string }

func (s *sentinelInnerErr) Error() string { return s.msg }

func TestResolveSemiJoins_InnerErrorChainPreserved(t *testing.T) {
	q := `SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity)`
	req, _ := ParseSOQLWith(q, ParseOpts{AllowSemiJoin: true})
	inner := &sentinelInnerErr{msg: "permission denied on Opportunity"}
	res := &stubResolver{err: inner}

	err := ResolveSemiJoins(context.Background(), req, res)
	if err == nil {
		t.Fatal("want error from inner failure")
	}
	if !strings.Contains(err.Error(), "OpportunityId") {
		t.Fatalf("error must name OuterField, got: %v", err)
	}
	// errors.As must walk the chain back to the original inner error so the
	// handler can classify it. Before the %w fix, the resolver wrapped into
	// *ValidationError and the chain was severed.
	var found *sentinelInnerErr
	if !errors.As(err, &found) {
		t.Fatalf("inner error chain not preserved; want errors.As to reach *sentinelInnerErr, got: %v", err)
	}
	if found.msg != inner.msg {
		t.Fatalf("recovered inner error msg = %q, want %q", found.msg, inner.msg)
	}
}
