package dialectsfcrm

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"testing"

	soql "github.com/nexuscrm/dialect-sf-crm-soql"
	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

func TestQueryAllNeutralHandlerDispatchesSemiJoinAsCorrelatedMembership(t *testing.T) {
	query := &semiJoinQueryCapture{}

	resp, err := QueryAllNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Query: map[string][]string{
			"q": {"SELECT Id FROM Task WHERE WhatId IN (SELECT Id FROM Opportunity WHERE IsClosed = true)"},
		},
		User: &models.UserSession{ID: "005-reader"},
	}, dialect.Deps{Contracts: contract.Deps{Query: query}})
	if err != nil {
		t.Fatalf("QueryNeutralHandler error = %v", err)
	}
	if resp.Status != 200 {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	assertSemiJoinDispatchedAsCorrelatedMembership(t, query.calls, 0, true, false, 0)
}

func TestQueryMoreNeutralHandlerDispatchesSemiJoinAsCorrelatedMembership(t *testing.T) {
	query := &semiJoinQueryCapture{}
	locator := semiJoinLocator(t, queryLocatorData{
		V:       queryLocatorVersionSOQLCap,
		Q:       "SELECT Id FROM Task WHERE WhatId NOT IN (SELECT Id FROM Opportunity WHERE IsClosed = false)",
		Offset:  2000,
		Limit:   2000,
		ScanAll: true,
	})

	resp, err := QueryMoreNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Params:  map[string]string{"locator": locator},
		User:    &models.UserSession{ID: "005-reader"},
	}, dialect.Deps{Contracts: contract.Deps{Query: query}})
	if err != nil {
		t.Fatalf("QueryMoreNeutralHandler error = %v", err)
	}
	if resp.Status != 200 {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	assertSemiJoinDispatchedAsCorrelatedMembership(t, query.calls, 2000, true, true, 0)
}

func TestQueryNeutralHandlerPreservesExplicitSemiJoinInnerLimit(t *testing.T) {
	query := &semiJoinQueryCapture{}

	resp, err := QueryNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Query: map[string][]string{
			"q": {"SELECT Id FROM Task WHERE WhatId IN (SELECT Id FROM Opportunity WHERE IsClosed = true LIMIT 5)"},
		},
		User: &models.UserSession{ID: "005-reader"},
	}, dialect.Deps{Contracts: contract.Deps{Query: query}})
	if err != nil {
		t.Fatalf("QueryNeutralHandler error = %v", err)
	}
	if resp.Status != 200 {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	assertSemiJoinDispatchedAsCorrelatedMembership(t, query.calls, 0, false, false, 5)
}

func TestQueryNeutralHandlerDoesNotMaterializeLargeSemiJoinIDList(t *testing.T) {
	query := &semiJoinQueryCapture{innerRecordCount: 1001}

	resp, err := QueryNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Query: map[string][]string{
			"q": {"SELECT Id FROM Task WHERE WhatId IN (SELECT Id FROM Opportunity WHERE IsClosed = true)"},
		},
		User: &models.UserSession{ID: "005-reader"},
	}, dialect.Deps{Contracts: contract.Deps{Query: query}})
	if err != nil {
		t.Fatalf("QueryNeutralHandler error = %v", err)
	}
	if resp.Status != 200 {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	assertSemiJoinDispatchedAsCorrelatedMembership(t, query.calls, 0, false, false, 0)
}

func TestQueryNeutralHandlerDoesNotMaterializeLargeAntiJoinIDList(t *testing.T) {
	query := &semiJoinQueryCapture{innerRecordCount: 1001}

	resp, err := QueryNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Query: map[string][]string{
			"q": {"SELECT Id FROM Task WHERE WhatId NOT IN (SELECT Id FROM Opportunity WHERE IsClosed = false)"},
		},
		User: &models.UserSession{ID: "005-reader"},
	}, dialect.Deps{Contracts: contract.Deps{Query: query}})
	if err != nil {
		t.Fatalf("QueryNeutralHandler error = %v", err)
	}
	if resp.Status != 200 {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	assertSemiJoinDispatchedAsCorrelatedMembership(t, query.calls, 0, false, true, 0)
}

func TestAggregateQueryNeutralHandlerDoesNotMaterializeLargeSemiJoinIDList(t *testing.T) {
	query := &semiJoinQueryCapture{
		aggregateTaskResult: true,
		innerRecordCount:    1001,
	}

	resp, err := QueryNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Query: map[string][]string{
			"q": {"SELECT COUNT(Id) FROM Task WHERE WhatId IN (SELECT Id FROM Opportunity WHERE IsClosed = true)"},
		},
		User: &models.UserSession{ID: "005-reader"},
	}, dialect.Deps{Contracts: contract.Deps{Query: query}})
	if err != nil {
		t.Fatalf("QueryNeutralHandler error = %v", err)
	}
	if resp.Status != 200 {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	assertSemiJoinDispatchedAsCorrelatedMembership(t, query.calls, 0, false, false, 0)
	if got := query.calls[0].SelectFields; len(got) != 1 || got[0] != "COUNT(Id)" {
		t.Fatalf("outer SelectFields = %v, want [COUNT(Id)]", got)
	}
}

func TestHistoryQueryRejectsSemiJoinBeforeDispatch(t *testing.T) {
	resp, err := QueryNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Query: map[string][]string{
			"q": {"SELECT Id FROM AccountHistory WHERE AccountId IN (SELECT Id FROM Account WHERE Name = 'Acme')"},
		},
		User: &models.UserSession{ID: "005-reader"},
	}, dialect.Deps{Contracts: contract.Deps{Query: &semiJoinQueryCapture{}}})
	if err != nil {
		t.Fatalf("QueryNeutralHandler error = %v", err)
	}
	if resp.Status != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400; body=%#v", resp.Status, resp.Body)
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeMalformedQuery, historySemiJoinUnsupportedMessage))
	assertBody(t, "history semi-join query rejection", resp.Body, want, want)
}

func TestHistoryQueryMoreRejectsSemiJoinBeforeDispatch(t *testing.T) {
	locator := semiJoinLocator(t, queryLocatorData{
		V:      queryLocatorVersionSOQLCap,
		Q:      "SELECT Id FROM AccountHistory WHERE AccountId NOT IN (SELECT Id FROM Account WHERE Name = 'Acme')",
		Offset: 2000,
		Limit:  2000,
	})

	resp, err := QueryMoreNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Params:  map[string]string{"locator": locator},
		User:    &models.UserSession{ID: "005-reader"},
	}, dialect.Deps{Contracts: contract.Deps{Query: &semiJoinQueryCapture{}}})
	if err != nil {
		t.Fatalf("QueryMoreNeutralHandler error = %v", err)
	}
	if resp.Status != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400; body=%#v", resp.Status, resp.Body)
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeMalformedQuery, historySemiJoinUnsupportedMessage))
	assertBody(t, "history semi-join queryMore rejection", resp.Body, want, want)
}

func TestQueryRejectsInnerHistorySemiJoinBeforeDispatch(t *testing.T) {
	query := &semiJoinQueryCapture{}

	resp, err := QueryNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Query: map[string][]string{
			"q": {"SELECT Id FROM Account WHERE Id IN (SELECT AccountId FROM AccountHistory WHERE Field = 'Name')"},
		},
		User: &models.UserSession{ID: "005-reader"},
	}, dialect.Deps{Contracts: contract.Deps{Query: query}})
	if err != nil {
		t.Fatalf("QueryNeutralHandler error = %v", err)
	}
	if resp.Status != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400; body=%#v", resp.Status, resp.Body)
	}
	if len(query.calls) != 0 {
		t.Fatalf("query calls = %d, want no inner history dispatch; calls=%#v", len(query.calls), query.calls)
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeMalformedQuery, historySemiJoinUnsupportedMessage))
	assertBody(t, "inner history semi-join query rejection", resp.Body, want, want)
}

func TestQueryMoreRejectsInnerHistorySemiJoinBeforeDispatch(t *testing.T) {
	query := &semiJoinQueryCapture{}
	locator := semiJoinLocator(t, queryLocatorData{
		V:      queryLocatorVersionSOQLCap,
		Q:      "SELECT Id FROM Account WHERE Id NOT IN (SELECT AccountId FROM AccountHistory WHERE Field = 'Name')",
		Offset: 2000,
		Limit:  2000,
	})

	resp, err := QueryMoreNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Params:  map[string]string{"locator": locator},
		User:    &models.UserSession{ID: "005-reader"},
	}, dialect.Deps{Contracts: contract.Deps{Query: query}})
	if err != nil {
		t.Fatalf("QueryMoreNeutralHandler error = %v", err)
	}
	if resp.Status != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400; body=%#v", resp.Status, resp.Body)
	}
	if len(query.calls) != 0 {
		t.Fatalf("query calls = %d, want no inner history dispatch; calls=%#v", len(query.calls), query.calls)
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeMalformedQuery, historySemiJoinUnsupportedMessage))
	assertBody(t, "inner history semi-join queryMore rejection", resp.Body, want, want)
}

type semiJoinQueryCapture struct {
	calls               []queryspec.QuerySpec
	aggregateTaskResult bool
	innerRecordCount    int
}

func (q *semiJoinQueryCapture) Query(_ context.Context, spec queryspec.QuerySpec, _ *models.UserSession) (*contract.QueryResult, error) {
	q.calls = append(q.calls, spec)
	switch spec.ObjectAPIName {
	case "Opportunity":
		count := q.innerRecordCount
		if count <= 0 {
			count = 1
		}
		records := make([]contract.Record, count)
		for i := range records {
			records[i] = contract.Record{"Id": fmt.Sprintf("006-inner-%04d", i)}
		}
		return &contract.QueryResult{
			TotalSize: count,
			Done:      true,
			Records:   records,
		}, nil
	case "Task":
		if q.aggregateTaskResult {
			return &contract.QueryResult{
				TotalSize:   1,
				Done:        true,
				IsAggregate: true,
				Records:     []contract.Record{{"expr0": int64(7)}},
			}, nil
		}
		return &contract.QueryResult{
			TotalSize: 1,
			Done:      true,
			Records:   []contract.Record{{"Id": "00T-result"}},
		}, nil
	default:
		return nil, fmt.Errorf("unexpected query dispatch for %s", spec.ObjectAPIName)
	}
}

func (q *semiJoinQueryCapture) SearchWithSpec(context.Context, queryspec.SearchSpec, *models.UserSession) ([]contract.Record, error) {
	return nil, errors.New("unexpected SearchWithSpec call")
}

func (q *semiJoinQueryCapture) GlobalSearchWithSpec(context.Context, queryspec.GlobalSearchSpec, *models.UserSession) ([]contract.SearchResult, error) {
	return nil, errors.New("unexpected GlobalSearchWithSpec call")
}

func (q *semiJoinQueryCapture) QueryHistory(context.Context, contract.HistoryQueryRequest, *models.UserSession) (*contract.HistoryQueryResult, error) {
	return nil, errors.New("unexpected QueryHistory call")
}

func assertSemiJoinDispatchedAsCorrelatedMembership(t *testing.T, calls []queryspec.QuerySpec, outerOffset int, wantScanAll bool, negate bool, wantInnerLimit int) {
	t.Helper()
	if len(calls) != 1 {
		t.Fatalf("query calls = %d, want one outer correlated-membership dispatch; calls=%#v", len(calls), calls)
	}
	outer := calls[0]
	if outer.ObjectAPIName != "Task" || outer.ScanAll != wantScanAll || outer.Offset != outerOffset {
		t.Fatalf("outer dispatch = %#v, want Task scanAll=%v offset %d", outer, wantScanAll, outerOffset)
	}
	if outer.Filter.Kind != queryspec.PredicateCorrelatedMembership || outer.Filter.CorrelatedMembership == nil {
		t.Fatalf("outer filter = %#v, want correlated-membership predicate", outer.Filter)
	}
	assertNoMaterializedSemiJoinIDList(t, outer.Filter)
	membership := outer.Filter.CorrelatedMembership
	if membership.OuterField != "WhatId" || membership.InnerObjectAPIName != "Opportunity" || membership.InnerSelectField != "Id" || membership.Negate != negate {
		t.Fatalf("membership = %#v, want negate=%v Task.WhatId -> Opportunity.Id", membership, negate)
	}
	if membership.InnerScanAll != wantScanAll {
		t.Fatalf("membership InnerScanAll = %v, want %v", membership.InnerScanAll, wantScanAll)
	}
	if membership.InnerLimit != wantInnerLimit {
		t.Fatalf("membership InnerLimit = %d, want %d", membership.InnerLimit, wantInnerLimit)
	}
	innerExpr := predicateExpression(t, membership.InnerFilter)
	if innerExpr == "" {
		t.Fatalf("membership inner filter empty, want parsed inner WHERE predicate")
	}
	if _, err := queryspec.LowerPredicate(outer.Filter, queryspec.PredicateLoweringExpressionOnly); err == nil {
		t.Fatalf("outer correlated-membership predicate lowered to expression, want executor-only shape")
	}
}

func assertNoMaterializedSemiJoinIDList(t *testing.T, predicate queryspec.Predicate) {
	t.Helper()
	if predicateContainsSemiJoinPlaceholder(predicate) {
		t.Fatalf("outer filter still contains SOQL semi-join placeholder: %#v", predicate)
	}
	if listSize := largestOperandList(predicate); listSize > 1000 {
		t.Fatalf("outer filter contains materialized ID list with %d values, want correlated membership", listSize)
	}
}

func predicateContainsSemiJoinPlaceholder(predicate queryspec.Predicate) bool {
	switch predicate.Kind {
	case queryspec.PredicateComparison:
		return operandContainsSemiJoinPlaceholder(predicate.Left) ||
			operandContainsSemiJoinPlaceholder(predicate.Right) ||
			operandContainsSemiJoinPlaceholder(predicate.Operand)
	case queryspec.PredicateExpression:
		return operandContainsSemiJoinPlaceholder(predicate.Operand)
	case queryspec.PredicateAnd, queryspec.PredicateOr, queryspec.PredicateNot:
		for _, child := range predicate.Children {
			if predicateContainsSemiJoinPlaceholder(child) {
				return true
			}
		}
	case queryspec.PredicateCorrelatedMembership:
		return predicate.CorrelatedMembership != nil &&
			predicateContainsSemiJoinPlaceholder(predicate.CorrelatedMembership.InnerFilter)
	}
	return false
}

func operandContainsSemiJoinPlaceholder(operand queryspec.Operand) bool {
	if operand.Raw != "" && strings.Contains(operand.Raw, soql.SemiJoinPlaceholderPrefix) {
		return true
	}
	if value, ok := operand.Value.(string); ok && strings.Contains(value, soql.SemiJoinPlaceholderPrefix) {
		return true
	}
	for _, element := range operand.Elements {
		if operandContainsSemiJoinPlaceholder(element) {
			return true
		}
	}
	for _, arg := range operand.Args {
		if operandContainsSemiJoinPlaceholder(arg) {
			return true
		}
	}
	return false
}

func largestOperandList(predicate queryspec.Predicate) int {
	size := 0
	for _, operand := range []queryspec.Operand{predicate.Left, predicate.Right, predicate.Operand} {
		if operandSize := largestOperandListInOperand(operand); operandSize > size {
			size = operandSize
		}
	}
	for _, child := range predicate.Children {
		if childSize := largestOperandList(child); childSize > size {
			size = childSize
		}
	}
	if predicate.CorrelatedMembership != nil {
		if innerSize := largestOperandList(predicate.CorrelatedMembership.InnerFilter); innerSize > size {
			size = innerSize
		}
	}
	return size
}

func largestOperandListInOperand(operand queryspec.Operand) int {
	size := 0
	if operand.Kind == queryspec.OperandList {
		size = len(operand.Elements)
	}
	for _, element := range operand.Elements {
		if elementSize := largestOperandListInOperand(element); elementSize > size {
			size = elementSize
		}
	}
	for _, arg := range operand.Args {
		if argSize := largestOperandListInOperand(arg); argSize > size {
			size = argSize
		}
	}
	return size
}

func semiJoinLocator(t *testing.T, data queryLocatorData) string {
	t.Helper()
	raw, err := json.Marshal(data)
	if err != nil {
		t.Fatalf("marshal locator: %v", err)
	}
	return base64.URLEncoding.EncodeToString(raw)
}
