package dialectsfcrm

import (
	"bytes"
	"context"
	"errors"
	"log"
	"net/http"
	"strings"
	"sync"
	"testing"
	"time"

	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 TestCreateBulkQueryJobNeutralHandlerCreatesJobAndStoresQuery(t *testing.T) {
	queryBlocker := make(chan struct{})
	t.Cleanup(func() { close(queryBlocker) })
	jobs := newFakeBulkQueryJobs()
	query := &fakeBulkQueryQuery{block: queryBlocker, result: &contract.QueryResult{}}
	req := &dialect.Request{
		Version: "v60.0",
		User:    &models.UserSession{ID: "005000000000001AAA"},
		Body:    []byte(`{"operation":"queryAll","query":"SELECT Id FROM Account"}`),
	}

	resp, err := CreateBulkQueryJobNeutralHandler(context.Background(), req, dialect.Deps{
		Contracts: contract.Deps{BulkJobs: jobs, Query: query},
	})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	if resp.Status != http.StatusCreated {
		t.Fatalf("status = %d, want 201; body=%#v", resp.Status, resp.Body)
	}
	job, ok := resp.Body.(*models.BulkJob)
	if !ok {
		t.Fatalf("body = %T, want *models.BulkJob", resp.Body)
	}
	if job.Operation != models.OperationQueryAll || job.State != models.JobStateUploadComplete || job.JobType != "V2Query" {
		t.Fatalf("job core fields = %+v", job)
	}
	if job.ApiVersion != 60 || job.CreatedById != req.User.ID || job.ContentUrl != "SELECT Id FROM Account" {
		t.Fatalf("job wire fields = %+v", job)
	}
	if got := jobs.latest(job.ID); got == nil || got.ContentUrl != "SELECT Id FROM Account" {
		t.Fatalf("stored job = %+v, want query contentUrl", got)
	}
}

func TestCreateBulkQueryJobInternalErrorsAreSanitizedAndLogged(t *testing.T) {
	for _, tc := range []struct {
		name     string
		jobs     *fakeBulkQueryJobs
		wantLog  string
		wantLeak string
	}{
		{
			name:     "create fails",
			jobs:     &fakeBulkQueryJobs{records: make(map[string]*models.BulkJob), createErr: errors.New("insert failed password=create-secret SELECT * FROM _System_BulkJob")},
			wantLog:  "create-secret",
			wantLeak: "create-secret",
		},
		{
			name:     "store query fails",
			jobs:     &fakeBulkQueryJobs{records: make(map[string]*models.BulkJob), updateErr: errors.New("update failed password=update-secret UPDATE _System_BulkJob")},
			wantLog:  "update-secret",
			wantLeak: "update-secret",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			logOutput := captureBulkQueryLogs(t)
			query := &fakeBulkQueryQuery{result: &contract.QueryResult{}}
			resp, err := CreateBulkQueryJobNeutralHandler(context.Background(), &dialect.Request{
				Version: "v63.0",
				User:    &models.UserSession{ID: "005000000000001AAA"},
				Body:    []byte(`{"query":"SELECT Id FROM Account"}`),
			}, dialect.Deps{Contracts: contract.Deps{BulkJobs: tc.jobs, Query: query}})
			if err != nil {
				t.Fatalf("handler error: %v", err)
			}
			assertBulkQueryInternalResponse(t, resp, tc.wantLeak)
			if !strings.Contains(logOutput.String(), tc.wantLog) {
				t.Fatalf("log output = %q, want %q", logOutput.String(), tc.wantLog)
			}
			if query.calls != 0 {
				t.Fatalf("query calls = %d, want no async processing after failed create/update", query.calls)
			}
		})
	}
}

func TestBulkQueryInternalDependencyAndReadErrorsAreLogged(t *testing.T) {
	logOutput := captureBulkQueryLogs(t)
	resp, err := CreateBulkQueryJobNeutralHandler(context.Background(), &dialect.Request{
		User: &models.UserSession{ID: "005owner"},
		Body: []byte(`{"query":"SELECT Id FROM Account"}`),
	}, dialect.Deps{})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	assertBulkQueryInternalResponse(t, resp, "contracts unavailable")
	if !strings.Contains(logOutput.String(), "create query job contracts unavailable") {
		t.Fatalf("log output = %q, want missing contract context", logOutput.String())
	}

	logOutput.Reset()
	resp, err = GetBulkQueryResultsNeutralHandler(context.Background(), &dialect.Request{
		User:   &models.UserSession{ID: "005owner"},
		Params: map[string]string{"jobId": "750query"},
	}, dialect.Deps{})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	assertBulkQueryInternalResponse(t, resp, "contracts unavailable")
	if !strings.Contains(logOutput.String(), "get query results contracts unavailable") {
		t.Fatalf("log output = %q, want missing results contract context", logOutput.String())
	}

	logOutput.Reset()
	jobs := &fakeBulkQueryJobs{records: make(map[string]*models.BulkJob), getErr: errors.New("select failed password=get-secret FROM _System_BulkJob")}
	resp, err = GetBulkQueryJobNeutralHandler(context.Background(), &dialect.Request{
		User:   &models.UserSession{ID: "005owner"},
		Params: map[string]string{"jobId": "750query"},
	}, dialect.Deps{Contracts: contract.Deps{BulkJobs: jobs}})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	assertBulkQueryInternalResponse(t, resp, "get-secret")
	if !strings.Contains(logOutput.String(), "get-secret") {
		t.Fatalf("log output = %q, want get failure cause", logOutput.String())
	}
}

func TestGetBulkQueryJobNeutralHandlerProjectsStatusAndChecksAccess(t *testing.T) {
	created := time.Date(2026, 6, 12, 10, 30, 0, 0, time.UTC)
	modstamp := created.Add(2 * time.Minute)
	jobs := newFakeBulkQueryJobs()
	jobs.put(&models.BulkJob{
		ID:             "750query",
		Operation:      models.OperationQuery,
		Object:         "Account",
		State:          models.JobStateJobComplete,
		JobType:        "V2Query",
		CreatedById:    "005owner",
		CreatedDate:    created,
		SystemModstamp: modstamp,
	})

	resp, err := GetBulkQueryJobNeutralHandler(context.Background(), &dialect.Request{
		User:   &models.UserSession{ID: "005owner"},
		Params: map[string]string{"jobId": "750query"},
	}, dialect.Deps{Contracts: contract.Deps{BulkJobs: jobs}})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	body := resp.Body.(map[string]interface{})
	if body["id"] != "750query" || body["operation"] != models.OperationQuery || body["createdDate"] != created.Format(time.RFC3339) || body["systemModstamp"] != modstamp.Format(time.RFC3339) {
		t.Fatalf("status body = %#v", body)
	}
	if body["numberRecordsProcessed"] != 0 {
		t.Fatalf("numberRecordsProcessed = %#v, want 0", body["numberRecordsProcessed"])
	}

	resp, err = GetBulkQueryJobNeutralHandler(context.Background(), &dialect.Request{
		User:   &models.UserSession{ID: "005other"},
		Params: map[string]string{"jobId": "750query"},
	}, dialect.Deps{Contracts: contract.Deps{BulkJobs: jobs}})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	assertSFError(t, resp, http.StatusForbidden, wire.SFCodeForbidden)

	resp, err = GetBulkQueryJobNeutralHandler(context.Background(), &dialect.Request{
		User:   &models.UserSession{ID: "005other"},
		Params: map[string]string{"jobId": "750query"},
	}, dialect.Deps{Contracts: contract.Deps{BulkJobs: jobs, Policy: bulkQueryPolicy{allow: true}}})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("superuser status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
}

func TestGetBulkQueryResultsNeutralHandlerProjectsRowsAndHeaders(t *testing.T) {
	jobs := newFakeBulkQueryJobs()
	jobs.put(&models.BulkJob{
		ID:             "750query",
		Operation:      models.OperationQuery,
		Object:         "Contact",
		State:          models.JobStateJobComplete,
		JobType:        "V2Query",
		CreatedById:    "005owner",
		CreatedDate:    time.Now().UTC(),
		SystemModstamp: time.Now().UTC(),
		ContentUrl:     "SELECT Id, Account.Id FROM Contact",
	})
	query := &fakeBulkQueryQuery{result: &contract.QueryResult{Records: []contract.Record{{
		models.ObjectTypeMetadataKey: "Contact",
		"attributes":                 map[string]any{"type": "Contact"},
		"Id":                         "003000000000001AAA",
		"Account": contract.Record{
			models.ObjectTypeMetadataKey: "Account",
			"attributes":                 map[string]any{"type": "Account"},
			"Id":                         "001000000000001AAA",
		},
	}}}}

	resp, err := GetBulkQueryResultsNeutralHandler(context.Background(), &dialect.Request{
		User:   &models.UserSession{ID: "005owner"},
		Params: map[string]string{"jobId": "750query"},
		Query:  map[string][]string{"maxRecords": {"7"}, "locator": {"2"}},
	}, dialect.Deps{Contracts: contract.Deps{BulkJobs: jobs, Query: query}})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if resp.Headers["Sforce-Locator"] != "null" || resp.Headers["Sforce-NumberOfRecords"] != "1" {
		t.Fatalf("headers = %#v", resp.Headers)
	}
	if query.req.Limit != 7 || query.req.Offset != 2 {
		t.Fatalf("query spec paging = limit %d offset %d, want 7/2", query.req.Limit, query.req.Offset)
	}
	rows := resp.Body.([]map[string]interface{})
	if _, ok := rows[0][models.ObjectTypeMetadataKey]; ok {
		t.Fatalf("bulk query row leaked neutral object type: %#v", rows[0])
	}
	if _, ok := rows[0]["attributes"]; ok {
		t.Fatalf("bulk query row added attributes envelope: %#v", rows[0])
	}
	account, ok := rows[0]["Account"].(models.SObject)
	if !ok {
		t.Fatalf("nested Account = %T %#v, want models.SObject", rows[0]["Account"], rows[0]["Account"])
	}
	if _, ok := account[models.ObjectTypeMetadataKey]; ok {
		t.Fatalf("nested Account leaked neutral object type: %#v", account)
	}
	if _, ok := account["attributes"]; ok {
		t.Fatalf("nested Account added attributes envelope: %#v", account)
	}
}

func TestGetBulkQueryResultsHonorsQueryAllScanAll(t *testing.T) {
	jobs := newFakeBulkQueryJobs()
	jobs.put(&models.BulkJob{
		ID:             "750query",
		Operation:      models.OperationQueryAll,
		Object:         "Contact",
		State:          models.JobStateJobComplete,
		JobType:        "V2Query",
		CreatedById:    "005owner",
		CreatedDate:    time.Now().UTC(),
		SystemModstamp: time.Now().UTC(),
		ContentUrl:     "SELECT Id FROM Contact",
	})
	query := &fakeBulkQueryQuery{result: &contract.QueryResult{}}

	resp, err := GetBulkQueryResultsNeutralHandler(context.Background(), &dialect.Request{
		User:   &models.UserSession{ID: "005owner"},
		Params: map[string]string{"jobId": "750query"},
	}, dialect.Deps{Contracts: contract.Deps{BulkJobs: jobs, Query: query}})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if !query.req.ScanAll {
		t.Fatalf("query spec ScanAll = false, want true for queryAll bulk job")
	}
}

func TestGetBulkQueryResultsExecutionErrorsAreSanitized(t *testing.T) {
	for _, tc := range []struct {
		name string
		err  error
		leak string
	}{
		{
			name: "internal marker",
			err:  bulkQueryInternalTestError{message: "driver failed password=internal-secret SELECT * FROM Contact"},
			leak: "internal-secret",
		},
		{
			name: "unclassified execution failure",
			err:  errors.New("count query failed password=plain-secret SELECT COUNT(*) FROM Contact"),
			leak: "plain-secret",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			logOutput := captureBulkQueryLogs(t)
			jobs := newFakeBulkQueryJobs()
			jobs.put(&models.BulkJob{
				ID:             "750query",
				Operation:      models.OperationQuery,
				Object:         "Contact",
				State:          models.JobStateJobComplete,
				JobType:        "V2Query",
				CreatedById:    "005owner",
				CreatedDate:    time.Now().UTC(),
				SystemModstamp: time.Now().UTC(),
				ContentUrl:     "SELECT Id FROM Contact",
			})
			query := &fakeBulkQueryQuery{err: tc.err}

			resp, err := GetBulkQueryResultsNeutralHandler(context.Background(), &dialect.Request{
				User:   &models.UserSession{ID: "005owner"},
				Params: map[string]string{"jobId": "750query"},
			}, dialect.Deps{Contracts: contract.Deps{BulkJobs: jobs, Query: query}})
			if err != nil {
				t.Fatalf("handler error: %v", err)
			}
			assertBulkQueryInternalResponse(t, resp, tc.leak)
			if !strings.Contains(logOutput.String(), tc.leak) {
				t.Fatalf("log output = %q, want execution cause %q", logOutput.String(), tc.leak)
			}
		})
	}
}

func TestBulkQuerySpecPreservesLegacyBulkSurface(t *testing.T) {
	parsed, err := soql.ParseSOQL("SELECT Id, (SELECT Id FROM Contacts WHERE LastName = 'Smith') FROM Account WHERE Name = 'Acme' GROUP BY OwnerId HAVING COUNT(Id) > 1 ORDER BY Name DESC LIMIT 10 OFFSET 3")
	if err != nil {
		t.Fatalf("ParseSOQL: %v", err)
	}

	spec := bulkQuerySpecFromParsedSOQL(parsed, 5, 2, false)

	rendered := predicateExpression(t, spec.Filter)
	if spec.ObjectAPIName != "Account" || rendered == "" || spec.SortField != "Name" || spec.SortDirection != "DESC" || spec.Limit != 5 || spec.Offset != 2 {
		t.Fatalf("bulk query spec preserved fields = %#v", spec)
	}
	if len(spec.SelectFields) != 0 || spec.GroupBy != "" || spec.Having.Kind != queryspec.PredicateNone || len(spec.SubQueries) != 0 {
		t.Fatalf("bulk query spec widened legacy surface: %#v", spec)
	}
}

func TestBulkQuerySpecPreservesMultiColumnOrderBy(t *testing.T) {
	parsed, err := soql.ParseSOQL("SELECT Id FROM Account ORDER BY Industry ASC, AnnualRevenue DESC, Id ASC")
	if err != nil {
		t.Fatalf("ParseSOQL: %v", err)
	}

	spec := bulkQuerySpecFromParsedSOQL(parsed, 5, 2, false)

	want := []struct{ field, dir string }{
		{"Industry", "ASC"},
		{"AnnualRevenue", "DESC"},
		{"Id", "ASC"},
	}
	if len(spec.OrderBy) != len(want) {
		t.Fatalf("expected %d ORDER BY terms, got %d (%#v)", len(want), len(spec.OrderBy), spec.OrderBy)
	}
	for i, w := range want {
		if spec.OrderBy[i].Field != w.field || spec.OrderBy[i].Direction != w.dir {
			t.Errorf("term %d = {%s %s}, want {%s %s}", i, spec.OrderBy[i].Field, spec.OrderBy[i].Direction, w.field, w.dir)
		}
	}
	if spec.SortField != "Industry" || spec.SortDirection != "ASC" {
		t.Errorf("first-column SortField/SortDirection not preserved: %q/%q", spec.SortField, spec.SortDirection)
	}
	if len(spec.SelectFields) != 0 || spec.GroupBy != "" || len(spec.SubQueries) != 0 {
		t.Errorf("bulk slim surface changed: %#v", spec)
	}
}

func TestBulkQueryMissingJobAndContractsFailClosed(t *testing.T) {
	jobs := newFakeBulkQueryJobs()
	resp, err := GetBulkQueryJobNeutralHandler(context.Background(), &dialect.Request{
		User:   &models.UserSession{ID: "005owner"},
		Params: map[string]string{"jobId": "missing"},
	}, dialect.Deps{Contracts: contract.Deps{BulkJobs: jobs}})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	assertSFError(t, resp, http.StatusNotFound, wire.SFCodeNotFound)

	resp, err = CreateBulkQueryJobNeutralHandler(context.Background(), &dialect.Request{
		User: &models.UserSession{ID: "005owner"},
		Body: []byte(`{"query":"SELECT Id FROM Account"}`),
	}, dialect.Deps{})
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	assertSFError(t, resp, http.StatusInternalServerError, wire.SFCodeInternalError)
}

type fakeBulkQueryJobs struct {
	mu        sync.Mutex
	records   map[string]*models.BulkJob
	createErr error
	getErr    error
	updateErr error
}

func newFakeBulkQueryJobs() *fakeBulkQueryJobs {
	return &fakeBulkQueryJobs{records: make(map[string]*models.BulkJob)}
}

func (s *fakeBulkQueryJobs) CreateJob(_ context.Context, job *models.BulkJob) error {
	if s.createErr != nil {
		return s.createErr
	}
	s.put(job)
	return nil
}

func (s *fakeBulkQueryJobs) GetJob(_ context.Context, jobID string) (*models.BulkJob, error) {
	if s.getErr != nil {
		return nil, s.getErr
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	return cloneBulkJob(s.records[jobID]), nil
}

func (s *fakeBulkQueryJobs) UpdateJob(_ context.Context, job *models.BulkJob) error {
	if s.updateErr != nil {
		return s.updateErr
	}
	s.put(job)
	return nil
}

func (s *fakeBulkQueryJobs) put(job *models.BulkJob) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.records[job.ID] = cloneBulkJob(job)
}

func (s *fakeBulkQueryJobs) latest(jobID string) *models.BulkJob {
	s.mu.Lock()
	defer s.mu.Unlock()
	return cloneBulkJob(s.records[jobID])
}

func cloneBulkJob(job *models.BulkJob) *models.BulkJob {
	if job == nil {
		return nil
	}
	copied := *job
	return &copied
}

type fakeBulkQueryQuery struct {
	block  <-chan struct{}
	result *contract.QueryResult
	err    error
	req    queryspec.QuerySpec
	calls  int
}

func (q *fakeBulkQueryQuery) Query(_ context.Context, req queryspec.QuerySpec, _ *models.UserSession) (*contract.QueryResult, error) {
	q.calls++
	q.req = req
	if q.block != nil {
		<-q.block
	}
	return q.result, q.err
}

func (q *fakeBulkQueryQuery) SearchWithSpec(context.Context, queryspec.SearchSpec, *models.UserSession) ([]contract.Record, error) {
	return nil, nil
}

func (q *fakeBulkQueryQuery) GlobalSearchWithSpec(context.Context, queryspec.GlobalSearchSpec, *models.UserSession) ([]contract.SearchResult, error) {
	return nil, nil
}

func (q *fakeBulkQueryQuery) QueryHistory(context.Context, contract.HistoryQueryRequest, *models.UserSession) (*contract.HistoryQueryResult, error) {
	return nil, nil
}

type bulkQueryPolicy struct {
	allow bool
}

func (p bulkQueryPolicy) Access(context.Context, *models.UserSession, string, contract.AccessRequest) (*contract.EffectiveObjectAccess, error) {
	return nil, nil
}

func (p bulkQueryPolicy) HasCapability(context.Context, *models.UserSession, contract.Capability) bool {
	return p.allow
}

func captureBulkQueryLogs(t *testing.T) *bytes.Buffer {
	t.Helper()
	var output bytes.Buffer
	oldWriter := log.Writer()
	oldFlags := log.Flags()
	log.SetOutput(&output)
	log.SetFlags(0)
	t.Cleanup(func() {
		log.SetOutput(oldWriter)
		log.SetFlags(oldFlags)
	})
	return &output
}

func assertBulkQueryInternalResponse(t *testing.T, resp *dialect.Response, forbiddenSubstrings ...string) {
	t.Helper()
	if resp.Status != http.StatusInternalServerError {
		t.Fatalf("status = %d, want 500; body=%#v", resp.Status, resp.Body)
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeInternalError, "An internal server error occurred. Contact your system administrator."))
	assertBody(t, "bulk query internal", resp.Body, want, want)
	raw := mustJSON(t, resp.Body)
	for _, leaked := range forbiddenSubstrings {
		if strings.Contains(raw, leaked) {
			t.Fatalf("internal response leaked %q: %s", leaked, raw)
		}
	}
}

type bulkQueryInternalTestError struct {
	message string
}

func (e bulkQueryInternalTestError) Error() string {
	return e.message
}

func (e bulkQueryInternalTestError) InternalCondition() bool {
	return true
}
