package querycodec

import (
	"context"
	"encoding/json"
	"net/http"
	"net/url"
	"reflect"
	"strings"
	"testing"
)

func TestMatchOwnsSupportedQueryRouteGrammar(t *testing.T) {
	wantVersions := []string{"v59.0", "v60.0", "v61.0", "v62.0", "v63.0"}
	if got := SupportedVersions(); !reflect.DeepEqual(got, wantVersions) {
		t.Fatalf("SupportedVersions=%#v want=%#v", got, wantVersions)
	}
	versions := SupportedVersions()
	versions[0] = "changed"
	if SupportedVersions()[0] != "v59.0" {
		t.Fatal("SupportedVersions returned shared mutable storage")
	}
	wantSpecs := []RouteSpec{
		{Kind: RouteQuery, Method: http.MethodGet, RelativePath: "/query"},
		{Kind: RouteQueryAll, Method: http.MethodGet, RelativePath: "/queryAll"},
		{Kind: RouteMore, Method: http.MethodGet, RelativePath: "/query/:locator"},
		{Kind: RouteToolingQuery, Method: http.MethodGet, RelativePath: "/tooling/query"},
	}
	if got := RouteSpecs(); !reflect.DeepEqual(got, wantSpecs) {
		t.Fatalf("RouteSpecs=%#v want=%#v", got, wantSpecs)
	}
	got := RouteSpecs()
	got[0].RelativePath = "/changed"
	if RouteSpecs()[0].RelativePath != "/query" {
		t.Fatal("RouteSpecs returned shared mutable storage")
	}
	for _, path := range []string{
		"/services/data/v59.0/query",
		"/services/data/v63.0/queryAll",
		"/services/data/v63.0/query/locator",
		"/services/data/v63.0/tooling/query",
	} {
		if _, ok := Match(path); !ok {
			t.Fatalf("Match(%q) = false", path)
		}
	}
	for _, path := range []string{
		"/services/data/v58.0/query",
		"/services/data/v63.0/query/",
		"/services/data/v63.0/query/a/b",
		"/services/data/v63.0/search",
	} {
		if _, ok := Match(path); ok {
			t.Fatalf("Match(%q) = true", path)
		}
	}
}

func TestHandlerParsesAndProjectsOneQueryExecution(t *testing.T) {
	var calls int
	handler := mustHandler(t, Executor{
		Query: func(_ context.Context, request QueryRequest, aggregateLimit int) (Result, error) {
			calls++
			if request.ObjectAPIName != "Account" || request.ScanAll || request.Limit != 2000 || aggregateLimit != 0 {
				t.Fatalf("request = %#v aggregateLimit=%d", request, aggregateLimit)
			}
			if len(request.SelectFields) != 2 || request.SelectFields[0] != "Id" || request.SelectFields[1] != "Name" {
				t.Fatalf("selections = %#v", request.SelectFields)
			}
			return Result{TotalSize: 1, Done: true, Records: []Record{{ObjectType: "Account", Fields: map[string]any{"Id": "001A", "Name": "Acme"}}}}, nil
		},
		History: unavailableHistory,
	})
	route, _ := Match("/services/data/v63.0/query")
	response := handler.Handle(context.Background(), Request{Route: route, Query: url.Values{"q": {"SELECT Id, Name FROM Account"}}})
	if calls != 1 || response.Status != http.StatusOK {
		t.Fatalf("calls=%d status=%d body=%#v", calls, response.Status, response.Body)
	}
	want := `{"done":true,"records":[{"Id":"001A","Name":"Acme","attributes":{"type":"Account","url":"/services/data/v63.0/sobjects/Account/001A"}}],"totalSize":1}`
	if got := marshalBody(t, response.Body); got != want {
		t.Fatalf("body=%s\nwant=%s", got, want)
	}
}

func TestHandlerPreservesQueryAllAndCorrelatedMembership(t *testing.T) {
	var observed QueryRequest
	handler := mustHandler(t, Executor{
		Query: func(_ context.Context, request QueryRequest, _ int) (Result, error) {
			observed = request
			return Result{Done: true}, nil
		},
		History: unavailableHistory,
	})
	route, _ := Match("/services/data/v63.0/queryAll")
	query := "SELECT Id FROM Quote WHERE OpportunityId IN (SELECT Id FROM Opportunity WHERE Name = 'A')"
	response := handler.Handle(context.Background(), Request{Route: route, Query: url.Values{"q": {query}}})
	if response.Status != http.StatusOK || !observed.ScanAll || len(observed.SemiJoins) != 0 || observed.Filter.Kind != PredicateCorrelatedMembership {
		t.Fatalf("status=%d request=%#v", response.Status, observed)
	}
}

func TestHandlerPaginationLocatorIsStableAndCallsOncePerPage(t *testing.T) {
	var offsets []int
	handler := mustHandler(t, Executor{
		Query: func(_ context.Context, request QueryRequest, _ int) (Result, error) {
			offsets = append(offsets, request.Offset)
			id := "001A"
			if request.Offset > 0 {
				id = "001B"
			}
			return Result{TotalSize: 2, Done: request.Offset > 0, Records: []Record{{ObjectType: "Account", Fields: map[string]any{"Id": id}}}}, nil
		},
		History: unavailableHistory,
	})
	route, _ := Match("/services/data/v63.0/query")
	first := handler.Handle(context.Background(), Request{Route: route, Query: url.Values{"q": {"SELECT Id FROM Account"}}, Header: http.Header{"Sforce-Query-Options": {"batchSize=200"}}})
	body := first.Body.(map[string]any)
	next, ok := body["nextRecordsUrl"].(string)
	if !ok {
		t.Fatalf("first body=%#v", body)
	}
	more, ok := Match(next)
	if !ok {
		t.Fatalf("next route did not match: %s", next)
	}
	second := handler.Handle(context.Background(), Request{Route: more})
	if second.Status != http.StatusOK || !reflect.DeepEqual(offsets, []int{0, 200}) {
		t.Fatalf("status=%d offsets=%v body=%#v", second.Status, offsets, second.Body)
	}
}

func TestHandlerFailuresAreFailClosedAndProductOwned(t *testing.T) {
	handler := mustHandler(t, Executor{
		Query: func(context.Context, QueryRequest, int) (Result, error) {
			return Result{}, &Failure{Category: FailureForbidden, Message: "Insufficient access"}
		},
		History: unavailableHistory,
	})
	route, _ := Match("/services/data/v63.0/query")
	response := handler.Handle(context.Background(), Request{Route: route, Query: url.Values{"q": {"SELECT Id FROM Secret"}}})
	if response.Status != http.StatusForbidden || marshalBody(t, response.Body) != `[{"errorCode":"FORBIDDEN","message":"Insufficient access"}]` {
		t.Fatalf("response=%#v body=%s", response, marshalBody(t, response.Body))
	}
	missing := handler.Handle(context.Background(), Request{Route: route})
	if missing.Status != http.StatusBadRequest || !strings.Contains(marshalBody(t, missing.Body), "MALFORMED_QUERY") {
		t.Fatalf("missing=%#v", missing)
	}
}

func TestHandlerPreservesExistingStructuredQueryDomainFailure(t *testing.T) {
	handler := mustHandler(t, Executor{
		Query: func(context.Context, QueryRequest, int) (Result, error) {
			return Result{}, &Failure{Category: FailureInvalidField, Message: "No such column", Fields: []string{"Missing__c"}}
		},
		History: unavailableHistory,
	})
	route, _ := Match("/services/data/v63.0/query")
	response := handler.Handle(context.Background(), Request{Route: route, Query: url.Values{"q": {"SELECT Missing__c FROM Account"}}})
	if response.Status != http.StatusBadRequest || marshalBody(t, response.Body) != `[{"errorCode":"INVALID_FIELD","message":"No such column","fields":["Missing__c"]}]` {
		t.Fatalf("response=%#v body=%s", response, marshalBody(t, response.Body))
	}
}

func TestCanceledContextDoesNotEnterExecutor(t *testing.T) {
	calls := 0
	handler := mustHandler(t, Executor{
		Query: func(context.Context, QueryRequest, int) (Result, error) {
			calls++
			return Result{}, nil
		},
		History: unavailableHistory,
	})
	route, _ := Match("/services/data/v63.0/query")
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	response := handler.Handle(ctx, Request{Route: route, Query: url.Values{"q": {"SELECT Id FROM Account"}}})
	if calls != 0 || response.Status != 499 || marshalBody(t, response.Body) != `[{"errorCode":"UNKNOWN_EXCEPTION","message":"Request canceled"}]` {
		t.Fatalf("calls=%d response=%#v", calls, response)
	}
}

func TestRuntimeFailureProjectionKeepsCurrentCRMGolden(t *testing.T) {
	tests := []struct {
		name     string
		failure  FailureCategory
		status   int
		envelope string
	}{
		{name: "response size", failure: FailureResponseTooLarge, status: http.StatusBadRequest, envelope: `[{"errorCode":"LIMIT_EXCEEDED","message":"Query response exceeds the supported size limit"}]`},
		{name: "admission", failure: FailureExhausted, status: http.StatusTooManyRequests, envelope: `[{"errorCode":"LIMIT_EXCEEDED","message":"Request limit exceeded"}]`},
		{name: "canceled", failure: FailureCanceled, status: 499, envelope: `[{"errorCode":"UNKNOWN_EXCEPTION","message":"Request canceled"}]`},
		{name: "deadline", failure: FailureDeadline, status: http.StatusGatewayTimeout, envelope: `[{"errorCode":"UNKNOWN_EXCEPTION","message":"Request deadline exceeded"}]`},
		{name: "unavailable", failure: FailureUnavailable, status: http.StatusServiceUnavailable, envelope: `[{"errorCode":"UNKNOWN_EXCEPTION","message":"Service temporarily unavailable"}]`},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			response := projectFailure(&Failure{Category: test.failure})
			if response.Status != test.status || marshalBody(t, response.Body) != test.envelope {
				t.Fatalf("response=%#v body=%s", response, marshalBody(t, response.Body))
			}
		})
	}
}

func TestProjectionPreservesTypedZeroFalseEmptyNullAndNestedRecords(t *testing.T) {
	falseValue := false
	zero := int64(0)
	handler := mustHandler(t, Executor{
		Query: func(context.Context, QueryRequest, int) (Result, error) {
			return Result{TotalSize: 1, Done: true, Records: []Record{{ObjectType: "Account", Fields: map[string]any{
				"Id": "001A", "Active": falseValue, "Count": zero, "Name": "", "Description": nil,
				"Owner": Record{ObjectType: "User", Fields: map[string]any{"Name": "Ada"}},
				// The neutral Runtime transport exposes lists as []any. The parsed
				// subquery, not the concrete empty-slice type, owns the envelope.
				"Contacts": []any{},
			}}}}, nil
		},
		History: unavailableHistory,
	})
	route, _ := Match("/services/data/v63.0/query")
	response := handler.Handle(context.Background(), Request{Route: route, Query: url.Values{"q": {"SELECT Id, Active, Count, Name, Description, Owner.Name, (SELECT Id FROM Contacts) FROM Account"}}})
	if response.Status != http.StatusOK {
		t.Fatalf("response=%#v", response)
	}
	body := marshalBody(t, response.Body)
	for _, fact := range []string{`"Active":false`, `"Count":0`, `"Name":""`, `"Description":null`, `"Owner":{"Name":"Ada","attributes":{"type":"User"}}`, `"Contacts":{"done":true,"records":[],"totalSize":0}`} {
		if !strings.Contains(body, fact) {
			t.Fatalf("body=%s missing %s", body, fact)
		}
	}
}

func TestHistoryUsesTheSameParserAndOneHistoryExecution(t *testing.T) {
	var calls int
	handler := mustHandler(t, Executor{
		Query: func(context.Context, QueryRequest, int) (Result, error) { return Result{}, nil },
		History: func(_ context.Context, request HistoryRequest) (HistoryResult, error) {
			calls++
			if request.ParentObjectAPIName != "Account" || request.ParentID != "001A" || len(request.SelectFields) != 4 {
				t.Fatalf("request=%#v", request)
			}
			old := "Old"
			return HistoryResult{TotalSize: 1, Done: true, Rows: []HistoryRecord{{ID: "017A", ParentID: "001A", Field: "Name", OldValue: &old, DataType: "string", CreatedDate: "2026-07-19T00:00:00Z", CreatedByID: "005A"}}}, nil
		},
	})
	route, _ := Match("/services/data/v63.0/query")
	query := "SELECT Id, Field, OldValue, NewValue FROM AccountHistory WHERE AccountId = '001A'"
	response := handler.Handle(context.Background(), Request{Route: route, Query: url.Values{"q": {query}}})
	if response.Status != http.StatusOK || calls != 1 {
		t.Fatalf("status=%d calls=%d body=%#v", response.Status, calls, response.Body)
	}
	if body := marshalBody(t, response.Body); !strings.Contains(body, `"OldValue":"Old"`) || !strings.Contains(body, `"NewValue":null`) {
		t.Fatalf("body=%s", body)
	}
}

func mustHandler(t *testing.T, executor Executor) *Handler {
	t.Helper()
	handler, err := NewHandler(executor)
	if err != nil {
		t.Fatal(err)
	}
	return handler
}

func unavailableHistory(context.Context, HistoryRequest) (HistoryResult, error) {
	return HistoryResult{}, &Failure{Category: FailureUnavailable}
}

func marshalBody(t *testing.T, body any) string {
	t.Helper()
	data, err := json.Marshal(body)
	if err != nil {
		t.Fatal(err)
	}
	return string(data)
}
