package sfcrmappservice

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

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

func newNotificationTestService(t *testing.T, query RuntimeQueryFunc) *Service {
	t.Helper()
	service, err := NewService(ServiceConfig{
		RuntimeEvaluate: func(context.Context, string, recordsemantic.Request) (recordsemantic.Response, error) {
			return recordsemantic.Response{}, nil
		},
		RuntimeQuery: query,
		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)
	}
	return service
}

func notificationHTTPRequest(path string) *http.Request {
	request := httptest.NewRequest(http.MethodGet, path, nil)
	request.Header.Set("Authorization", "Bearer user-secret")
	return request
}

func TestNotificationReadRoutesUseTrustedPrincipalAndOneRuntimeQuery(t *testing.T) {
	if err := querysemantic.ValidateResponse(notificationTestResponse()); err != nil {
		t.Fatalf("notification Runtime fixture is invalid: %#v", err)
	}
	tests := []struct {
		name, path string
		unreadOnly bool
		limit      uint32
	}{
		{name: "product UI", path: ProductNotificationPath + "?limit=25", limit: 25},
		{name: "connect compatibility", path: "/services/data/v63.0/connect/notifications", unreadOnly: true, limit: 50},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			calls := 0
			var gotBearer string
			var got querysemantic.Request
			service := newNotificationTestService(t, func(_ context.Context, bearer string, request querysemantic.Request) (querysemantic.Response, error) {
				calls++
				gotBearer, got = bearer, request
				return notificationTestResponse(), nil
			})
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, notificationHTTPRequest(test.path))
			if recorder.Code != http.StatusOK || calls != 1 || gotBearer != "user-secret" {
				t.Fatalf("status=%d calls=%d bearer=%q body=%s", recorder.Code, calls, gotBearer, recorder.Body.String())
			}
			if got.ObjectID != notificationObjectID || got.Limit != test.limit || len(got.Selections) != 8 ||
				len(got.OrderBy) != 1 || got.OrderBy[0].Field != notificationFieldCreatedDate || got.OrderBy[0].Direction != querysemantic.SortDescending {
				t.Fatalf("Runtime notification request = %#v", got)
			}
			recipient := got.Filter
			if test.unreadOnly {
				if got.Filter.Kind != querysemantic.PredicateAnd || len(got.Filter.Children) != 2 {
					t.Fatalf("connect filter = %#v", got.Filter)
				}
				recipient = got.Filter.Children[0]
				unread := got.Filter.Children[1]
				if unread.Kind != querysemantic.PredicateFieldComparison || unread.Field != notificationFieldIsRead ||
					unread.Value == nil || unread.Value.Boolean == nil || *unread.Value.Boolean {
					t.Fatalf("unread filter = %#v", unread)
				}
			}
			if recipient.Kind != querysemantic.PredicateComparison || recipient.Left.Field != notificationFieldRecipientID ||
				recipient.Right.Kind != querysemantic.OperandTrustedContext ||
				recipient.Right.TrustedContext != querysemantic.TrustedContextPrincipalID || recipient.Right.Literal != nil {
				t.Fatalf("recipient authority filter = %#v", recipient)
			}
			if !strings.Contains(recorder.Body.String(), `"id":"notification-1"`) ||
				!strings.Contains(recorder.Body.String(), `"recipientId":"trusted-user"`) ||
				!strings.Contains(recorder.Body.String(), `"unreadCount":1`) {
				t.Fatalf("notification projection = %s", recorder.Body.String())
			}
			if test.unreadOnly && !strings.Contains(recorder.Body.String(), `"currentPageUrl":"/services/data/v63.0/connect/notifications"`) {
				t.Fatalf("connect projection = %s", recorder.Body.String())
			}
		})
	}
}

func TestNotificationReadPreservesSurfaceSpecificNullAndEmptyURL(t *testing.T) {
	response := notificationTestResponse()
	base := response.Records[0]
	empty := ""
	clone := func(id string, link *querysemantic.Value) querysemantic.Record {
		record := querysemantic.Record{ObjectID: base.ObjectID}
		for _, field := range base.Fields {
			if field.Name == notificationFieldID {
				value := id
				field.Value = querysemantic.Value{Kind: querysemantic.ValueString, String: &value}
			}
			if field.Name == notificationFieldLink {
				if link == nil {
					continue
				}
				field.Value = *link
			}
			record.Fields = append(record.Fields, field)
		}
		return record
	}
	response.TotalSize = 3
	response.Records = []querysemantic.Record{
		clone("notification-null", &querysemantic.Value{Kind: querysemantic.ValueString, Null: true}),
		clone("notification-empty", &querysemantic.Value{Kind: querysemantic.ValueString, String: &empty}),
		clone("notification-absent", nil),
	}
	service := newNotificationTestService(t, func(context.Context, string, querysemantic.Request) (querysemantic.Response, error) {
		return response, nil
	})

	connect := httptest.NewRecorder()
	service.ServeHTTP(connect, notificationHTTPRequest("/services/data/v63.0/connect/notifications"))
	var connectBody struct {
		Notifications []map[string]any `json:"notifications"`
	}
	if connect.Code != http.StatusOK || json.Unmarshal(connect.Body.Bytes(), &connectBody) != nil || len(connectBody.Notifications) != 3 {
		t.Fatalf("Connect response = status %d body %s", connect.Code, connect.Body.String())
	}
	if value, present := connectBody.Notifications[0]["url"]; !present || value != nil {
		t.Fatalf("Connect explicit-null url = (%#v,%v), want present null", value, present)
	}
	if value, present := connectBody.Notifications[1]["url"]; !present || value != "" {
		t.Fatalf("Connect empty url = (%#v,%v), want present empty string", value, present)
	}
	if value, present := connectBody.Notifications[2]["url"]; !present || value != nil {
		t.Fatalf("Connect absent source url = (%#v,%v), want legacy present null", value, present)
	}

	product := httptest.NewRecorder()
	service.ServeHTTP(product, notificationHTTPRequest(ProductNotificationPath))
	var productBody struct {
		Notifications []map[string]any `json:"notifications"`
	}
	if product.Code != http.StatusOK || json.Unmarshal(product.Body.Bytes(), &productBody) != nil || len(productBody.Notifications) != 3 {
		t.Fatalf("product response = status %d body %s", product.Code, product.Body.String())
	}
	for index, notification := range productBody.Notifications {
		if value, present := notification["url"]; present {
			t.Fatalf("product notification %d url = %#v, want legacy omission", index, value)
		}
	}
}

func TestNotificationReadOwnsEveryConnectVersionAndProductLimitParity(t *testing.T) {
	for path, version := range supportedConnectNotificationPaths {
		t.Run(version, func(t *testing.T) {
			calls := 0
			service := newNotificationTestService(t, func(context.Context, string, querysemantic.Request) (querysemantic.Response, error) {
				calls++
				return notificationTestResponse(), nil
			})
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, notificationHTTPRequest(path))
			if recorder.Code != http.StatusOK || calls != 1 || !strings.Contains(recorder.Body.String(), `/services/data/`+version+`/connect/notifications`) {
				t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
			}
		})
	}
	for _, test := range []struct {
		query string
		want  uint32
	}{
		{want: productNotificationDefaultLimit},
		{query: "?limit=999", want: productNotificationMaxLimit},
	} {
		var got uint32
		service := newNotificationTestService(t, func(_ context.Context, _ string, request querysemantic.Request) (querysemantic.Response, error) {
			got = request.Limit
			return notificationTestResponse(), nil
		})
		recorder := httptest.NewRecorder()
		service.ServeHTTP(recorder, notificationHTTPRequest(ProductNotificationPath+test.query))
		if recorder.Code != http.StatusOK || got != test.want {
			t.Fatalf("query=%q status=%d limit=%d body=%s", test.query, recorder.Code, got, recorder.Body.String())
		}
	}
}

func TestNotificationReadLocalRejectionsAndFailureParity(t *testing.T) {
	calls := 0
	service := newNotificationTestService(t, func(context.Context, string, querysemantic.Request) (querysemantic.Response, error) {
		calls++
		return querysemantic.Response{}, &querysemantic.Failure{Category: querysemantic.FailureUnavailable, RetryAfterMillis: 1000}
	})

	unauthorized := httptest.NewRecorder()
	service.ServeHTTP(unauthorized, httptest.NewRequest(http.MethodGet, ProductNotificationPath, nil))
	if unauthorized.Code != http.StatusUnauthorized || calls != 0 {
		t.Fatalf("unauthorized status=%d calls=%d body=%s", unauthorized.Code, calls, unauthorized.Body.String())
	}

	invalidLimit := httptest.NewRecorder()
	service.ServeHTTP(invalidLimit, notificationHTTPRequest(ProductNotificationPath+"?limit=0"))
	if invalidLimit.Code != http.StatusBadRequest || calls != 0 || invalidLimit.Body.String() != "{\"error\":\"invalid limit\"}\n" {
		t.Fatalf("invalid limit status=%d calls=%d body=%s", invalidLimit.Code, calls, invalidLimit.Body.String())
	}

	unavailable := httptest.NewRecorder()
	service.ServeHTTP(unavailable, notificationHTTPRequest(ProductNotificationPath))
	if unavailable.Code != http.StatusServiceUnavailable || unavailable.Header().Get("Retry-After") != "" ||
		calls != 1 || strings.Contains(unavailable.Body.String(), "notifications") {
		t.Fatalf("unavailable status=%d calls=%d body=%s", unavailable.Code, calls, unavailable.Body.String())
	}

	// Connect has a pre-existing compatibility promise: query failures look
	// like an empty unread feed. Keep that behavior isolated from the real
	// product route, which remains fail-closed above.
	connectUnavailable := httptest.NewRecorder()
	service.ServeHTTP(connectUnavailable, notificationHTTPRequest("/services/data/v63.0/connect/notifications"))
	if connectUnavailable.Code != http.StatusOK || connectUnavailable.Header().Get("Retry-After") != "" ||
		calls != 2 || connectUnavailable.Body.String() != "{\"notifications\":[],\"unreadCount\":0}\n" {
		t.Fatalf("connect unavailable status=%d calls=%d body=%s", connectUnavailable.Code, calls, connectUnavailable.Body.String())
	}

	wrongMethod := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, ProductNotificationPath, nil)
	request.Header.Set("Authorization", "Bearer user-secret")
	service.ServeHTTP(wrongMethod, request)
	if wrongMethod.Code != http.StatusMethodNotAllowed || wrongMethod.Header().Get("Allow") != http.MethodGet || calls != 2 {
		t.Fatalf("method status=%d allow=%q calls=%d", wrongMethod.Code, wrongMethod.Header().Get("Allow"), calls)
	}
}

func TestNotificationReadCancellationStopsBeforeRuntime(t *testing.T) {
	calls := 0
	service := newNotificationTestService(t, func(context.Context, string, querysemantic.Request) (querysemantic.Response, error) {
		calls++
		return notificationTestResponse(), nil
	})
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	request := notificationHTTPRequest(ProductNotificationPath).WithContext(ctx)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != 499 || calls != 0 {
		t.Fatalf("canceled status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
	}
}

func TestConnectNotificationReadPreservesMaskedQueryFailure(t *testing.T) {
	calls := 0
	service := newNotificationTestService(t, func(context.Context, string, querysemantic.Request) (querysemantic.Response, error) {
		calls++
		return querysemantic.Response{}, &querysemantic.Failure{Category: querysemantic.FailureUnavailable}
	})
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, notificationHTTPRequest("/services/data/v63.0/connect/notifications"))
	if recorder.Code != http.StatusOK || calls != 1 || recorder.Body.String() != "{\"notifications\":[],\"unreadCount\":0}\n" {
		t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
	}
}

func TestNotificationSelectionOrderIsDeterministic(t *testing.T) {
	want := []string{
		notificationFieldID, notificationFieldRecipientID, notificationFieldTitle, notificationFieldBody,
		notificationFieldLink, notificationFieldIsRead, notificationFieldType, notificationFieldCreatedDate,
	}
	first := notificationRequest(false, 50)
	second := notificationRequest(false, 50)
	got := make([]string, len(first.Selections))
	for index := range first.Selections {
		got[index] = first.Selections[index].Field
	}
	if !reflect.DeepEqual(got, want) || !reflect.DeepEqual(first, second) {
		t.Fatalf("notification request is not deterministic: %#v / %#v", first, second)
	}
}

func notificationTestResponse() querysemantic.Response {
	stringValue := func(value string, kind querysemantic.ValueKind) querysemantic.Value {
		if kind == querysemantic.ValueDateTime {
			return querysemantic.Value{Kind: kind, DateTime: &value}
		}
		return querysemantic.Value{Kind: kind, String: &value}
	}
	falseValue := false
	return querysemantic.Response{
		TotalSize: 1, Done: true,
		Records: []querysemantic.Record{{ObjectID: notificationObjectID, Fields: []querysemantic.Field{
			{Name: notificationFieldCreatedDate, Value: stringValue("2026-07-21T10:00:00Z", querysemantic.ValueDateTime)},
			{Name: notificationFieldID, Value: stringValue("notification-1", querysemantic.ValueString)},
			{Name: notificationFieldBody, Value: stringValue("A request changed state", querysemantic.ValueString)},
			{Name: notificationFieldIsRead, Value: querysemantic.Value{Kind: querysemantic.ValueBoolean, Boolean: &falseValue}},
			{Name: notificationFieldLink, Value: stringValue("/approvals", querysemantic.ValueString)},
			{Name: notificationFieldType, Value: stringValue("approval", querysemantic.ValueString)},
			{Name: notificationFieldRecipientID, Value: stringValue("trusted-user", querysemantic.ValueString)},
			{Name: notificationFieldTitle, Value: stringValue("Approval updated", querysemantic.ValueString)},
		}}},
		Receipts: querysemantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: querysemantic.CapabilityName, CatalogRevision: 7},
	}
}
