package sfcrmappservice

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync/atomic"
	"testing"
	"time"

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

type fakeRuntime struct {
	ready    atomic.Bool
	calls    atomic.Uint64
	response semantic.Response
	err      error
}

func (f *fakeRuntime) Evaluate(context.Context, string, semantic.Request) (semantic.Response, error) {
	f.calls.Add(1)
	return f.response, f.err
}

func (f *fakeRuntime) Ready() bool { return f != nil && f.ready.Load() }

func (f *fakeRuntime) Metrics() runtimegrpc.RuntimeClientMetrics {
	return runtimegrpc.RuntimeClientMetrics{IdentityCalls: f.calls.Load(), RecordsCalls: f.calls.Load()}
}

func newTestService(t *testing.T, runtime *fakeRuntime) *Service {
	t.Helper()
	service, err := NewService(ServiceConfig{
		RuntimeEvaluate: runtime.Evaluate, RuntimeReady: runtime.Ready, RuntimeMetrics: runtime.Metrics,
		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
		},
		RequestTimeout: time.Second, MaxRequestBytes: 64 * 1024,
	})
	if err != nil {
		t.Fatal(err)
	}
	return service
}

func TestServiceConstructionRequiresDirectlyNilCheckableRuntimePorts(t *testing.T) {
	ready := func() bool { return true }
	metrics := func() runtimegrpc.RuntimeClientMetrics { return runtimegrpc.RuntimeClientMetrics{} }
	evaluate := RuntimeEvaluateFunc(func(context.Context, string, semantic.Request) (semantic.Response, error) {
		return semantic.Response{}, nil
	})
	query := RuntimeQueryFunc(func(context.Context, string, querysemantic.Request) (querysemantic.Response, error) {
		return querysemantic.Response{}, nil
	})
	history := RuntimeHistoryQueryFunc(func(context.Context, string, querysemantic.HistoryRequest) (querysemantic.HistoryResponse, error) {
		return querysemantic.HistoryResponse{}, nil
	})
	for _, config := range []ServiceConfig{
		{RuntimeQuery: query, RuntimeHistoryQuery: history, RuntimeReady: ready, RuntimeMetrics: metrics, RequestTimeout: time.Second, MaxRequestBytes: 1},
		{RuntimeEvaluate: evaluate, RuntimeHistoryQuery: history, RuntimeReady: ready, RuntimeMetrics: metrics, RequestTimeout: time.Second, MaxRequestBytes: 1},
		{RuntimeEvaluate: evaluate, RuntimeQuery: query, RuntimeReady: ready, RuntimeMetrics: metrics, RequestTimeout: time.Second, MaxRequestBytes: 1},
		{RuntimeEvaluate: evaluate, RuntimeQuery: query, RuntimeHistoryQuery: history, RuntimeMetrics: metrics, RequestTimeout: time.Second, MaxRequestBytes: 1},
		{RuntimeEvaluate: evaluate, RuntimeQuery: query, RuntimeHistoryQuery: history, RuntimeReady: ready, RequestTimeout: time.Second, MaxRequestBytes: 1},
	} {
		if service, err := NewService(config); err == nil || service != nil {
			t.Fatalf("NewService(%+v) = %#v, %v; want rejected", config, service, err)
		}
	}
}

func recordAccessRequest(body string) *http.Request {
	request := httptest.NewRequest(http.MethodPost, RecordAccessPath, strings.NewReader(body))
	request.Header.Set("Authorization", "Bearer user-secret")
	return request
}

func TestServicePreservesGoldenCRMProjectionAndMasking(t *testing.T) {
	runtime := &fakeRuntime{response: semantic.Response{Results: []semantic.Result{
		{RecordID: "ISS-1", Access: semantic.Access{Read: true, Update: true}},
		{RecordID: "ISS-2", Access: semantic.Access{Read: true, Locked: true}},
	}, Receipts: validReceipts()}}
	runtime.ready.Store(true)
	recorder := httptest.NewRecorder()
	newTestService(t, runtime).ServeHTTP(recorder, recordAccessRequest(`{"recordIds":["ISS-2","ISS-MISSING","ISS-1","ISS-RLS"]}`))
	want := "{\"recordAccess\":{\"ISS-1\":{\"hasReadAccess\":true,\"hasEditAccess\":true,\"hasDeleteAccess\":false,\"isLocked\":false},\"ISS-2\":{\"hasReadAccess\":true,\"hasEditAccess\":false,\"hasDeleteAccess\":false,\"isLocked\":true}}}\n"
	if recorder.Code != http.StatusOK || recorder.Body.String() != want || runtime.calls.Load() != 1 {
		t.Fatalf("status=%d calls=%d body=%s", recorder.Code, runtime.calls.Load(), recorder.Body.String())
	}
}

func TestServiceOwnsEveryPublishedCRMRecordAccessVersion(t *testing.T) {
	for path := range supportedRecordAccessPaths {
		t.Run(path, func(t *testing.T) {
			runtime := &fakeRuntime{response: semantic.Response{Results: []semantic.Result{}, Receipts: validReceipts()}}
			runtime.ready.Store(true)
			request := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{"recordIds":["ISS-MISSING"]}`))
			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 calls=%d body=%s", recorder.Code, runtime.calls.Load(), recorder.Body.String())
			}
		})
	}
}

func TestServiceMakesZeroRuntimeCallsForEmptyAndLocalRejection(t *testing.T) {
	runtime := &fakeRuntime{response: semantic.Response{Receipts: validReceipts()}}
	runtime.ready.Store(true)
	service := newTestService(t, runtime)
	for _, test := range []struct {
		name string
		body string
		want int
	}{
		{name: "empty", body: `{"recordIds":[]}`, want: http.StatusOK},
		{name: "invalid JSON", body: `{`, want: http.StatusBadRequest},
		{name: "normalized empty", body: `{"recordIds":[" ",""]}`, want: http.StatusOK},
	} {
		t.Run(test.name, func(t *testing.T) {
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, recordAccessRequest(test.body))
			if recorder.Code != test.want {
				t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
			}
		})
	}
	ids := make([]string, semantic.MaxRecords+1)
	for index := range ids {
		ids[index] = `"ISS-1"`
	}
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, recordAccessRequest(`{"recordIds":[`+strings.Join(ids, ",")+`]}`))
	if recorder.Code != http.StatusBadRequest || runtime.calls.Load() != 0 {
		t.Fatalf("status=%d calls=%d body=%s", recorder.Code, runtime.calls.Load(), recorder.Body.String())
	}
}

func TestServiceRequiresOneBearerAndNeverReturnsIt(t *testing.T) {
	runtime := &fakeRuntime{}
	runtime.ready.Store(true)
	service := newTestService(t, runtime)
	request := httptest.NewRequest(http.MethodPost, RecordAccessPath, strings.NewReader(`{"recordIds":["ISS-1"]}`))
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusUnauthorized || runtime.calls.Load() != 0 || strings.Contains(recorder.Body.String(), "user-secret") {
		t.Fatalf("status=%d calls=%d body=%s", recorder.Code, runtime.calls.Load(), recorder.Body.String())
	}
	request = recordAccessRequest(`{"recordIds":["ISS-1"]}`)
	request.Header.Add("Authorization", "Bearer second")
	recorder = httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusUnauthorized || runtime.calls.Load() != 0 {
		t.Fatalf("duplicate bearer status=%d calls=%d", recorder.Code, runtime.calls.Load())
	}
}

func TestServiceProjectsNeutralFailuresToCRMGrammar(t *testing.T) {
	tests := []struct {
		name           string
		err            error
		wantStatus     int
		wantBody       string
		wantRetryAfter string
	}{
		{name: "identity unauthenticated", err: &identitysemantic.Failure{Category: identitysemantic.FailureUnauthenticated}, wantStatus: 401, wantBody: `[{"errorCode":"INVALID_SESSION_ID","message":"Session expired or invalid"}]` + "\n"},
		{name: "identity forbidden", err: &identitysemantic.Failure{Category: identitysemantic.FailureForbidden}, wantStatus: 403, wantBody: `[{"errorCode":"FORBIDDEN","message":"Insufficient access"}]` + "\n"},
		{name: "records invalid", err: &semantic.Failure{Category: semantic.FailureInvalidRequest}, wantStatus: 400, wantBody: `[{"errorCode":"INVALID_REQUEST","message":"Invalid request"}]` + "\n"},
		{name: "records exhausted", err: &semantic.Failure{Category: semantic.FailureResourceExhausted}, wantStatus: 400, wantBody: `[{"errorCode":"LIMIT_EXCEEDED","message":"Request exceeds a supported limit"}]` + "\n"},
		{name: "runtime admission retry instructed", err: &semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitAdmission, Maximum: 16, RetryAfterMillis: 50}, wantStatus: 503, wantBody: `[{"errorCode":"UNKNOWN_EXCEPTION","message":"Service temporarily unavailable"}]` + "\n", wantRetryAfter: "1"},
		{name: "runtime admission without retry instruction", err: &semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitAdmission, Maximum: 16}, wantStatus: 503, wantBody: `[{"errorCode":"UNKNOWN_EXCEPTION","message":"Service temporarily unavailable"}]` + "\n"},
		{name: "records missing", err: &semantic.Failure{Category: semantic.FailureMissing}, wantStatus: 404, wantBody: `[{"errorCode":"NOT_FOUND","message":"Record not found"}]` + "\n"},
		{name: "deadline", err: &semantic.Failure{Category: semantic.FailureDeadlineExceeded}, wantStatus: 504, wantBody: `[{"errorCode":"UNKNOWN_EXCEPTION","message":"Request deadline exceeded"}]` + "\n"},
		{name: "unavailable", err: &semantic.Failure{Category: semantic.FailureUnavailable}, wantStatus: 503, wantBody: `[{"errorCode":"UNKNOWN_EXCEPTION","message":"Service temporarily unavailable"}]` + "\n"},
		{name: "unavailable ignores internal delay hint", err: &semantic.Failure{Category: semantic.FailureUnavailable, RetryAfterMillis: 1000}, wantStatus: 503, wantBody: `[{"errorCode":"UNKNOWN_EXCEPTION","message":"Service temporarily unavailable"}]` + "\n"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			runtime := &fakeRuntime{err: test.err}
			runtime.ready.Store(true)
			recorder := httptest.NewRecorder()
			newTestService(t, runtime).ServeHTTP(recorder, recordAccessRequest(`{"recordIds":["ISS-1"]}`))
			if recorder.Code != test.wantStatus || recorder.Body.String() != test.wantBody ||
				recorder.Header().Get("Retry-After") != test.wantRetryAfter {
				t.Fatalf("status=%d retry-after=%q body=%s", recorder.Code, recorder.Header().Get("Retry-After"), recorder.Body.String())
			}
		})
	}
}

func TestRetryAfterHeaderRoundsMillisecondsUpToHTTPSeconds(t *testing.T) {
	for milliseconds, want := range map[uint64]string{0: "", 1: "1", 1000: "1", 1001: "2"} {
		recorder := httptest.NewRecorder()
		setRetryAfterHeader(recorder, milliseconds)
		if got := recorder.Header().Get("Retry-After"); got != want {
			t.Fatalf("Retry-After for %dms = %q, want %q", milliseconds, got, want)
		}
	}
}

func TestServiceDrainRejectsNewAdmission(t *testing.T) {
	runtime := &fakeRuntime{response: semantic.Response{Results: []semantic.Result{}, Receipts: validReceipts()}}
	runtime.ready.Store(true)
	service := newTestService(t, runtime)
	if !service.Ready() {
		t.Fatal("service should be ready")
	}
	service.BeginDrain()
	if service.Ready() {
		t.Fatal("draining service reported ready")
	}
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, recordAccessRequest(`{"recordIds":["ISS-1"]}`))
	if recorder.Code != http.StatusServiceUnavailable || runtime.calls.Load() != 0 {
		t.Fatalf("status=%d calls=%d", recorder.Code, runtime.calls.Load())
	}
}
