package sfcrmappservice

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/pingcap-inc/nexuscrm-api-host/runtimegrpc"
	querysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	accesssemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordaccessv1"
	mutationsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordmutationv1"
)

func TestCompositeBatchDispatchesThroughIndependentService(t *testing.T) {
	runtime := &fakeRuntime{response: accesssemantic.Response{
		Results:  []accesssemantic.Result{{RecordID: "ISS-1", Access: accesssemantic.Access{Read: true}}},
		Receipts: validReceipts(),
	}}
	runtime.ready.Store(true)
	request := httptest.NewRequest(http.MethodPost, "/services/data/v63.0/composite/batch", strings.NewReader(`{
		"batchRequests":[{
			"method":"POST",
			"url":"ui-api/record-access",
			"referenceId":"access",
			"body":{"recordIds":["ISS-1"]}
		}]
	}`))
	request.Header.Set("Authorization", "Bearer user-secret")
	recorder := httptest.NewRecorder()

	newTestService(t, runtime).ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || runtime.calls.Load() != 1 {
		t.Fatalf("status=%d Runtime calls=%d body=%s", recorder.Code, runtime.calls.Load(), recorder.Body.String())
	}
	var response genericCompositeBatchResponse
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatal(err)
	}
	if response.HasErrors || len(response.Results) != 1 || response.Results[0].StatusCode != http.StatusOK || response.Results[0].ReferenceID != "access" {
		t.Fatalf("response=%+v", response)
	}
	result, ok := response.Results[0].Result.(map[string]any)
	if !ok || result["recordAccess"] == nil {
		t.Fatalf("result=%#v", response.Results[0].Result)
	}
}

func TestCompositeDependentRequestResolvesReferencesAndStopsOnError(t *testing.T) {
	runtime := &fakeRuntime{response: accessSemanticResponseForComposite()}
	runtime.ready.Store(true)
	request := httptest.NewRequest(http.MethodPost, "/services/data/v62.0/composite", strings.NewReader(`{
		"allOrNone":true,
		"compositeRequest":[
			{"method":"POST","url":"ui-api/record-access","referenceId":"access","body":{"recordIds":["ISS-1"]}},
			{"method":"POST","url":"/services/data/v62.0/composite","referenceId":"loop"},
			{"method":"POST","url":"ui-api/record-access","referenceId":"not-run","body":{"recordIds":["ISS-2"]}}
		]
	}`))
	request.Header.Set("Authorization", "Bearer user-secret")
	recorder := httptest.NewRecorder()

	newTestService(t, runtime).ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || runtime.calls.Load() != 1 {
		t.Fatalf("status=%d Runtime calls=%d body=%s", recorder.Code, runtime.calls.Load(), recorder.Body.String())
	}
	var response genericCompositeResponse
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatal(err)
	}
	if len(response.CompositeResponse) != 2 || response.CompositeResponse[1].HTTPStatusCode != http.StatusBadRequest {
		t.Fatalf("response=%+v", response)
	}
	resolved := resolveCompositeReferences("/records/@{access.recordAccess.ISS-1.hasReadAccess}", map[string]any{
		"access": map[string]any{"recordAccess": map[string]any{"ISS-1": map[string]any{"hasReadAccess": true}}},
	})
	if resolved != "/records/true" {
		t.Fatalf("resolved=%q", resolved)
	}
}

func TestCompositeBodyReferenceEscapesJSONStringContent(t *testing.T) {
	value := "quoted \"line\"\nnext\\path"
	resolved := resolveCompositeBodyReferences(
		`{"value":"@{source.value}"}`,
		map[string]any{"source": map[string]any{"value": value}},
	)
	if !json.Valid([]byte(resolved)) {
		t.Fatalf("resolved body is invalid JSON: %s", resolved)
	}
	var body map[string]string
	if err := json.Unmarshal([]byte(resolved), &body); err != nil {
		t.Fatal(err)
	}
	if body["value"] != value {
		t.Fatalf("resolved value=%q", body["value"])
	}
}

func TestCompositeRejectsExternalTargetAndRequiresOneBearer(t *testing.T) {
	runtime := &fakeRuntime{response: accessSemanticResponseForComposite()}
	runtime.ready.Store(true)
	service := newTestService(t, runtime)

	request := httptest.NewRequest(http.MethodPost, "/services/data/v63.0/composite/batch", strings.NewReader(`{
		"batchRequests":[{"method":"GET","url":"https://legacy.example/services/data/v63.0/query?q=x","referenceId":"external"}]
	}`))
	request.Header.Set("Authorization", "Bearer user-secret")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusOK || runtime.calls.Load() != 0 || !strings.Contains(recorder.Body.String(), `"hasErrors":true`) {
		t.Fatalf("external status=%d Runtime calls=%d body=%s", recorder.Code, runtime.calls.Load(), recorder.Body.String())
	}

	request = httptest.NewRequest(http.MethodPost, "/services/data/v63.0/composite", strings.NewReader(`{"compositeRequest":[]}`))
	recorder = httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusUnauthorized || runtime.calls.Load() != 0 {
		t.Fatalf("unauthorized status=%d Runtime calls=%d body=%s", recorder.Code, runtime.calls.Load(), recorder.Body.String())
	}
}

func TestCompositeRejectsNormalizedRecursionAndBearerOverride(t *testing.T) {
	runtime := &fakeRuntime{response: accessSemanticResponseForComposite()}
	runtime.ready.Store(true)
	service := newTestService(t, runtime)

	for _, test := range []struct {
		name string
		body string
		want string
	}{
		{
			name: "relative composite target",
			body: `{"batchRequests":[{"method":"POST","url":"composite","referenceId":"loop","body":{}}]}`,
			want: "cannot target composite endpoints",
		},
		{
			name: "subrequest bearer override",
			body: `{"batchRequests":[{"method":"POST","url":"ui-api/record-access","referenceId":"access","httpHeaders":{"Authorization":"Bearer attacker"},"body":{"recordIds":["ISS-1"]}}]}`,
			want: "cannot override authorization",
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			request := httptest.NewRequest(
				http.MethodPost,
				"/services/data/v63.0/composite/batch",
				strings.NewReader(test.body),
			)
			request.Header.Set("Authorization", "Bearer user-secret")
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, request)

			if recorder.Code != http.StatusOK || runtime.calls.Load() != 0 ||
				!strings.Contains(recorder.Body.String(), test.want) {
				t.Fatalf("status=%d Runtime calls=%d body=%s", recorder.Code, runtime.calls.Load(), recorder.Body.String())
			}
		})
	}
}

func TestCompositePropagatesParentDeadlineToRuntime(t *testing.T) {
	observed := make(chan time.Time, 1)
	service, err := NewService(ServiceConfig{
		RuntimeEvaluate: func(ctx context.Context, _ string, _ accesssemantic.Request) (accesssemantic.Response, error) {
			deadline, ok := ctx.Deadline()
			if !ok {
				t.Fatal("Runtime context has no deadline")
			}
			observed <- deadline
			return accessSemanticResponseForComposite(), nil
		},
		RuntimeQuery: func(context.Context, string, querysemantic.Request) (querysemantic.Response, error) {
			return querysemantic.Response{}, nil
		},
		RuntimeHistoryQuery: func(context.Context, string, querysemantic.HistoryRequest) (querysemantic.HistoryResponse, error) {
			return querysemantic.HistoryResponse{}, nil
		},
		RuntimeReady: func() bool { return true },
		RuntimeMetrics: func() runtimegrpc.RuntimeClientMetrics {
			return runtimegrpc.RuntimeClientMetrics{}
		},
		RequestTimeout:  time.Second,
		MaxRequestBytes: 64 * 1024,
	})
	if err != nil {
		t.Fatal(err)
	}
	parentDeadline := time.Now().Add(200 * time.Millisecond)
	ctx, cancel := context.WithDeadline(context.Background(), parentDeadline)
	defer cancel()
	request := httptest.NewRequest(http.MethodPost, "/services/data/v63.0/composite/batch", strings.NewReader(`{
		"batchRequests":[{"method":"POST","url":"ui-api/record-access","body":{"recordIds":["ISS-1"]}}]
	}`)).WithContext(ctx)
	request.Header.Set("Authorization", "Bearer user-secret")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)

	select {
	case got := <-observed:
		if !got.Equal(parentDeadline) {
			t.Fatalf("Runtime deadline=%s, want parent deadline %s", got, parentDeadline)
		}
	default:
		t.Fatal("Runtime did not observe composite parent deadline")
	}
}

func TestCompositeOwnsOneAggregateDeadlineAcrossSubrequests(t *testing.T) {
	observed := make(chan time.Time, 2)
	service, err := NewService(ServiceConfig{
		RuntimeEvaluate: func(ctx context.Context, _ string, _ accesssemantic.Request) (accesssemantic.Response, error) {
			deadline, ok := ctx.Deadline()
			if !ok {
				t.Fatal("Runtime context has no deadline")
			}
			observed <- deadline
			return accessSemanticResponseForComposite(), nil
		},
		RuntimeQuery: func(context.Context, string, querysemantic.Request) (querysemantic.Response, error) {
			return querysemantic.Response{}, nil
		},
		RuntimeHistoryQuery: func(context.Context, string, querysemantic.HistoryRequest) (querysemantic.HistoryResponse, error) {
			return querysemantic.HistoryResponse{}, nil
		},
		RuntimeReady: func() bool { return true },
		RuntimeMetrics: func() runtimegrpc.RuntimeClientMetrics {
			return runtimegrpc.RuntimeClientMetrics{}
		},
		RequestTimeout:  time.Second,
		MaxRequestBytes: 64 * 1024,
	})
	if err != nil {
		t.Fatal(err)
	}
	request := httptest.NewRequest(http.MethodPost, "/services/data/v63.0/composite/batch", strings.NewReader(`{
		"batchRequests":[
			{"method":"POST","url":"ui-api/record-access","body":{"recordIds":["ISS-1"]}},
			{"method":"POST","url":"ui-api/record-access","body":{"recordIds":["ISS-2"]}}
		]
	}`))
	request.Header.Set("Authorization", "Bearer user-secret")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)

	first := <-observed
	second := <-observed
	if !first.Equal(second) {
		t.Fatalf("composite subrequests received different deadlines: %s and %s", first, second)
	}
}

func TestCompositeAllOrNoneMutationsUseOneAtomicRuntimeCommand(t *testing.T) {
	var requests []mutationsemantic.Request
	service, _ := newMutationService(t, func(
		_ context.Context,
		_ string,
		request mutationsemantic.Request,
	) (mutationsemantic.Response, error) {
		requests = append(requests, request)
		digest, err := mutationsemantic.CommandDigest(request)
		if err != nil {
			t.Fatal(err)
		}
		return mutationsemantic.Response{
			CommandID: request.CommandID, CommandDigest: digest,
			Results: []mutationsemantic.OperationResult{
				{Index: 0, Kind: mutationsemantic.OperationUpdate, RecordID: "0PS-a", Success: false, Errors: []mutationsemantic.OperationError{{Kind: mutationsemantic.ErrorAtomicRolledBack, Fields: []string{}}}},
				{Index: 1, Kind: mutationsemantic.OperationUpdate, RecordID: "0PS-b", Success: false, Errors: []mutationsemantic.OperationError{{Kind: mutationsemantic.ErrorInvalidFieldForWrite, Fields: []string{"Permissions"}}}},
			},
			Receipts: mutationReceipts(), OutcomeExpiresAtUTC: "2026-07-20T12:30:00Z",
		}, nil
	})
	request := httptest.NewRequest(http.MethodPost, "/services/data/v63.0/composite", strings.NewReader(`{
		"allOrNone":true,
		"compositeRequest":[
			{"method":"PATCH","url":"/services/data/v63.0/sobjects/PermissionSet/0PS-a","referenceId":"first","body":{"Permissions":true}},
			{"method":"PATCH","url":"/services/data/v63.0/sobjects/PermissionSet/0PS-b","referenceId":"second","body":{"Permissions":false}}
		]
	}`))
	request.Header.Set("Authorization", "Bearer user-secret")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || len(requests) != 1 ||
		requests[0].BatchMode != mutationsemantic.BatchAtomic ||
		requests[0].UseCase != mutationsemantic.MutationUseCaseCollection ||
		len(requests[0].Operations) != 2 {
		t.Fatalf("status=%d requests=%#v body=%s", recorder.Code, requests, recorder.Body.String())
	}
	var response genericCompositeResponse
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatal(err)
	}
	if len(response.CompositeResponse) != 2 ||
		response.CompositeResponse[0].ReferenceID != "first" ||
		response.CompositeResponse[1].ReferenceID != "second" ||
		!strings.Contains(recorder.Body.String(), crmCodeAllOrNoneRolledBack) ||
		!strings.Contains(recorder.Body.String(), crmCodeInvalidFieldForWrite) {
		t.Fatalf("response=%+v body=%s", response, recorder.Body.String())
	}
}

func TestCompositeAllOrNoneMixedShapeFailsBeforeRuntimeEffect(t *testing.T) {
	var calls int
	service, _ := newMutationService(t, func(
		_ context.Context,
		_ string,
		request mutationsemantic.Request,
	) (mutationsemantic.Response, error) {
		calls++
		return successfulMutationResponse(t, request), nil
	})
	request := httptest.NewRequest(http.MethodPost, "/services/data/v63.0/composite", strings.NewReader(`{
		"allOrNone":true,
		"compositeRequest":[
			{"method":"PATCH","url":"sobjects/PermissionSet/0PS-a","referenceId":"write","body":{"Permissions":true}},
			{"method":"GET","url":"query?q=SELECT+Id+FROM+PermissionSet","referenceId":"read"}
		]
	}`))
	request.Header.Set("Authorization", "Bearer user-secret")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || calls != 0 ||
		!strings.Contains(recorder.Body.String(), "cannot mix record mutations") {
		t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
	}
}

func TestCompositeAllOrNoneCollectionMutationFailsBeforeRuntimeEffect(t *testing.T) {
	var calls int
	service, _ := newMutationService(t, func(
		_ context.Context,
		_ string,
		request mutationsemantic.Request,
	) (mutationsemantic.Response, error) {
		calls++
		return successfulMutationResponse(t, request), nil
	})
	request := httptest.NewRequest(http.MethodPost, "/services/data/v63.0/composite", strings.NewReader(`{
		"allOrNone":true,
		"compositeRequest":[{
			"method":"POST",
			"url":"composite/sobjects",
			"referenceId":"collection",
			"body":{"allOrNone":true,"records":[{"attributes":{"type":"Account"},"Name":"Ada"}]}
		}]
	}`))
	request.Header.Set("Authorization", "Bearer user-secret")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || calls != 0 ||
		!strings.Contains(recorder.Body.String(), "nested collection mutation is not supported") {
		t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
	}
}

func TestCompositeAllOrNoneFilesDeleteFailsBeforeRuntimeEffect(t *testing.T) {
	var calls int
	service, _ := newMutationService(t, func(
		_ context.Context,
		_ string,
		request mutationsemantic.Request,
	) (mutationsemantic.Response, error) {
		calls++
		return successfulMutationResponse(t, request), nil
	})
	request := httptest.NewRequest(http.MethodPost, "/services/data/v63.0/composite", strings.NewReader(`{
		"allOrNone":true,
		"compositeRequest":[{
			"method":"DELETE",
			"url":"/services/data/v63.0/sobjects/_System_File/file-1",
			"referenceId":"file-delete"
		}]
	}`))
	request.Header.Set("Authorization", "Bearer user-secret")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || calls != 0 ||
		!strings.Contains(recorder.Body.String(), "allOrNone Files delete is not supported") {
		t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
	}
}

func TestCompositeDispatchBodyPreservesExactJSONNumbers(t *testing.T) {
	parsed := parseCompositeDispatchBody(
		[]byte(`{"amount":12.00,"large":9007199254740993}`),
	)
	encoded, err := json.Marshal(parsed)
	if err != nil {
		t.Fatal(err)
	}
	if string(encoded) != `{"amount":12.00,"large":9007199254740993}` {
		t.Fatalf("composite numeric response=%s", encoded)
	}
}

func accessSemanticResponseForComposite() accesssemantic.Response {
	return accesssemantic.Response{
		Results:  []accesssemantic.Result{{RecordID: "ISS-1", Access: accesssemantic.Access{Read: true}}},
		Receipts: validReceipts(),
	}
}
