package sfcrmappservice

import (
	"context"
	"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"
	recordsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordaccessv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/relatedcountsv1"
	"google.golang.org/grpc/metadata"
)

func newRelatedCountTestService(t *testing.T, count RuntimeRelatedCountFunc) *Service {
	t.Helper()
	service, err := NewService(ServiceConfig{
		RuntimeEvaluate: func(context.Context, string, recordsemantic.Request) (recordsemantic.Response, error) {
			return recordsemantic.Response{}, 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
		},
		RuntimeRelatedCount: count,
		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 relatedCountRequest(path, body string) *http.Request {
	request := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
	request.Header.Set("Authorization", "Bearer user-secret")
	return request
}

func TestRelatedCountRouteUsesOneRuntimeBatchAndPreservesCRMProjection(t *testing.T) {
	var calls int
	var gotBearer string
	var got semantic.Request
	service := newRelatedCountTestService(t, func(_ context.Context, bearer string, request semantic.Request) (semantic.Response, error) {
		calls++
		gotBearer, got = bearer, request
		return semantic.Response{
			Results: []semantic.Result{
				{Index: 0, Outcome: semantic.CountOutcomeCounted, Count: 3},
				{Index: 1, Outcome: semantic.CountOutcomeNotAvailable},
			},
			Receipts: semantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName, CatalogRevision: 7},
		}, nil
	})
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, relatedCountRequest(RelatedCountPath, `[
		{"objectApiName":"WorkItem","lookupField":"WorkspaceId","parentRecordId":"workspace-1","parentFieldValue":"external-key"},
		{"objectApiName":"Comment","lookupField":"WorkItemId","parentRecordId":"work-1"}
	]`))
	want := "{\"counts\":[{\"objectApiName\":\"WorkItem\",\"lookupField\":\"WorkspaceId\",\"count\":3},{\"objectApiName\":\"Comment\",\"lookupField\":\"WorkItemId\",\"count\":0,\"error\":\"related list count unavailable\"}]}\n"
	if recorder.Code != http.StatusOK || recorder.Body.String() != want || calls != 1 || gotBearer != "user-secret" {
		t.Fatalf("status=%d calls=%d bearer=%q body=%s", recorder.Code, calls, gotBearer, recorder.Body.String())
	}
	if len(got.Counts) != 2 || got.Counts[0].ParentRecordID != "workspace-1" || got.Counts[0].MatchValue != "external-key" || got.Counts[1].MatchValue != "work-1" {
		t.Fatalf("Runtime request = %#v", got)
	}
}

func TestServicePropagatesBoundedRequestCorrelationToRuntime(t *testing.T) {
	var gotRequestID []string
	service := newRelatedCountTestService(t, func(ctx context.Context, _ string, _ semantic.Request) (semantic.Response, error) {
		values, _ := metadata.FromOutgoingContext(ctx)
		gotRequestID = values.Get(runtimegrpc.RequestIDMetadataKey)
		return semantic.Response{
			Results:  []semantic.Result{{Index: 0, Outcome: semantic.CountOutcomeCounted}},
			Receipts: semantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName},
		}, nil
	})
	request := relatedCountRequest(RelatedCountPath, `[{"objectApiName":"WorkItem","lookupField":"ParentId","parentRecordId":"P-1"}]`)
	request.Header.Set("X-Request-ID", "inspection-123")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if len(gotRequestID) != 1 || gotRequestID[0] != "inspection-123" {
		t.Fatalf("request ID metadata = %#v", gotRequestID)
	}
}

func TestRelatedCountLocalRejectionsMakeNoRuntimeCall(t *testing.T) {
	var calls int
	service := newRelatedCountTestService(t, func(context.Context, string, semantic.Request) (semantic.Response, error) {
		calls++
		return semantic.Response{}, nil
	})
	for _, test := range []struct {
		name, body string
		status     int
	}{
		{name: "empty", body: `[]`, status: http.StatusOK},
		{name: "malformed", body: `{`, status: http.StatusBadRequest},
		{name: "extra JSON", body: `[] []`, status: http.StatusBadRequest},
	} {
		t.Run(test.name, func(t *testing.T) {
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, relatedCountRequest(RelatedCountPath, test.body))
			if recorder.Code != test.status {
				t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
			}
		})
	}
	items := make([]string, semantic.MaxCounts+1)
	for index := range items {
		items[index] = `{"objectApiName":"WorkItem","lookupField":"ParentId","parentRecordId":"P-1"}`
	}
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, relatedCountRequest(RelatedCountPath, `[`+strings.Join(items, ",")+`]`))
	if recorder.Code != http.StatusBadRequest || calls != 0 || recorder.Body.String() != "{\"error\":\"max 50 count requests per batch\"}\n" {
		t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
	}
}

func TestRelatedCountRouteIsAbsentWithoutReleasedRuntimePort(t *testing.T) {
	service, err := NewService(ServiceConfig{
		RuntimeEvaluate: func(context.Context, string, recordsemantic.Request) (recordsemantic.Response, error) {
			return recordsemantic.Response{}, 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)
	}
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, relatedCountRequest(RelatedCountPath, `[]`))
	if recorder.Code != http.StatusNotFound {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestRelatedCountOwnsEveryPublishedCRMVersion(t *testing.T) {
	for path := range supportedRelatedCountPaths {
		t.Run(path, func(t *testing.T) {
			calls := 0
			service := newRelatedCountTestService(t, func(context.Context, string, semantic.Request) (semantic.Response, error) {
				calls++
				return semantic.Response{Results: []semantic.Result{{Index: 0, Outcome: semantic.CountOutcomeCounted}}, Receipts: semantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName}}, nil
			})
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, relatedCountRequest(path, `[{"objectApiName":"WorkItem","lookupField":"ParentId","parentRecordId":"P-1"}]`))
			if recorder.Code != http.StatusOK || calls != 1 {
				t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
			}
		})
	}
}
