package sfcrmappservice

import (
	"context"
	"net/http"
	"net/http/httptest"
	"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"
	mutationsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recyclebinmutationv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recyclebinv1"
)

func newRecycleBinTestService(t *testing.T, list RuntimeRecycleBinFunc) *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
		},
		RuntimeRecycleBin: list,
		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 newRecycleBinMutationTestService(t *testing.T, restore, purge RuntimeRecycleBinMutationFunc) *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
		},
		RuntimeRestoreRecycleBin: restore,
		RuntimePurgeRecycleBin:   purge,
		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 recycleBinRequest(path string) *http.Request {
	request := httptest.NewRequest(http.MethodGet, path, nil)
	request.Header.Set("Authorization", "Bearer user-secret")
	return request
}

func TestRecycleBinRouteUsesOneRuntimeCallAndPreservesCRMProjection(t *testing.T) {
	var calls int
	var gotBearer string
	var got semantic.Request
	service := newRecycleBinTestService(t, func(_ context.Context, bearer string, request semantic.Request) (semantic.Response, error) {
		calls++
		gotBearer, got = bearer, request
		return semantic.Response{
			Items: []semantic.Item{{
				RecordID: "rec-1", RecordName: "Acme", ObjectID: "Account", DeletedBy: "Alice",
				DeletedDate: "2026-07-22T06:00:00Z", CreatedDate: "2026-01-01T00:00:00Z",
				LastModifiedDate: "2026-07-21T00:00:00Z",
			}},
			FailedObjects: []semantic.ObjectFailure{{ObjectID: "Opportunity", Code: semantic.ObjectFailureForbidden, Message: "no access"}},
			Receipts:      semantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName},
		}, nil
	})
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, recycleBinRequest(RecycleBinPath+"?scope=all"))
	want := "{\"items\":[{\"Id\":\"rec-1\",\"RecordId\":\"rec-1\",\"RecordName\":\"Acme\",\"ObjectApiName\":\"Account\",\"DeletedBy\":\"Alice\",\"DeletedDate\":\"2026-07-22T06:00:00Z\",\"CreatedDate\":\"2026-01-01T00:00:00Z\",\"LastModifiedDate\":\"2026-07-21T00:00:00Z\"}],\"totalSize\":1,\"partialFailure\":true,\"failedObjects\":[{\"objectApiName\":\"Opportunity\",\"errorCode\":\"forbidden\",\"message\":\"no access\"}]}"
	if recorder.Code != http.StatusOK || recorder.Body.String() != want || calls != 1 || gotBearer != "user-secret" || got.Scope != semantic.ScopeAll {
		t.Fatalf("status=%d calls=%d bearer=%q request=%#v body=%s", recorder.Code, calls, gotBearer, got, recorder.Body.String())
	}
}

func TestRecycleBinLocalRejectionsMakeNoRuntimeCall(t *testing.T) {
	var calls int
	service := newRecycleBinTestService(t, func(context.Context, string, semantic.Request) (semantic.Response, error) {
		calls++
		return semantic.Response{}, nil
	})

	invalid := httptest.NewRecorder()
	service.ServeHTTP(invalid, recycleBinRequest(RecycleBinPath+"?scope=everyone"))
	if invalid.Code != http.StatusBadRequest || calls != 0 || invalid.Body.String() != "[{\"errorCode\":\"INVALID_REQUEST\",\"message\":\"unsupported scope \\\"everyone\\\" (expected \\\"mine\\\" or \\\"all\\\")\"}]" {
		t.Fatalf("status=%d calls=%d body=%s", invalid.Code, calls, invalid.Body.String())
	}

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

func TestRecycleBinScopeAllDenialPreservesCurrentCRMError(t *testing.T) {
	service := newRecycleBinTestService(t, func(context.Context, string, semantic.Request) (semantic.Response, error) {
		return semantic.Response{}, &semantic.Failure{Category: semantic.FailureForbidden}
	})
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, recycleBinRequest(RecycleBinPath+"?scope=all"))
	want := "[{\"errorCode\":\"FORBIDDEN\",\"message\":\"ViewAllData or ModifyAllData required to view the org recycle bin\"}]"
	if recorder.Code != http.StatusForbidden || recorder.Body.String() != want {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestRecycleBinRuntimeFailurePreservesLegacyJSONFraming(t *testing.T) {
	service := newRecycleBinTestService(t, func(context.Context, string, semantic.Request) (semantic.Response, error) {
		return semantic.Response{}, &semantic.Failure{Category: semantic.FailureUnavailable, RetryAfterMillis: 1000}
	})
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, recycleBinRequest(RecycleBinPath))
	want := "[{\"errorCode\":\"UNKNOWN_EXCEPTION\",\"message\":\"Service temporarily unavailable\"}]"
	if recorder.Code != http.StatusServiceUnavailable || recorder.Header().Get("Retry-After") != "" || recorder.Body.String() != want {
		t.Fatalf("status=%d retry-after=%q body=%q", recorder.Code, recorder.Header().Get("Retry-After"), recorder.Body.String())
	}
}

func TestRecycleBinRouteIsAbsentWithoutReleasedRuntimePort(t *testing.T) {
	service := newRecycleBinTestService(t, nil)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, recycleBinRequest(RecycleBinPath))
	if recorder.Code != http.StatusNotFound {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestRecycleBinOwnsEveryPublishedCRMVersion(t *testing.T) {
	for path := range supportedRecycleBinPaths {
		t.Run(path, func(t *testing.T) {
			calls := 0
			service := newRecycleBinTestService(t, func(context.Context, string, semantic.Request) (semantic.Response, error) {
				calls++
				return semantic.Response{Receipts: semantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName}}, nil
			})
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, recycleBinRequest(path))
			if recorder.Code != http.StatusOK || calls != 1 || recorder.Body.String() != "{\"items\":[],\"totalSize\":0,\"partialFailure\":false,\"failedObjects\":[]}" {
				t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
			}
		})
	}
}

func TestRecycleBinRestoreAndPurgeUseOneRuntimeMutationEach(t *testing.T) {
	var restoreCalls, purgeCalls int
	var gotBearer, gotRecordID string
	response := func() mutationsemantic.Response {
		return mutationsemantic.Response{Receipts: mutationsemantic.Receipts{
			ProtocolVersion: "1.0", CapabilityVersion: mutationsemantic.CapabilityName,
		}}
	}
	service := newRecycleBinMutationTestService(t,
		func(_ context.Context, bearer string, request mutationsemantic.Request) (mutationsemantic.Response, error) {
			restoreCalls++
			gotBearer, gotRecordID = bearer, request.RecordID
			return response(), nil
		},
		func(_ context.Context, bearer string, request mutationsemantic.Request) (mutationsemantic.Response, error) {
			purgeCalls++
			gotBearer, gotRecordID = bearer, request.RecordID
			return response(), nil
		},
	)

	restore := recycleBinRequest(RecycleBinPath + "/restore/rec-1")
	restore.Method = http.MethodPost
	restoreRecorder := httptest.NewRecorder()
	service.ServeHTTP(restoreRecorder, restore)
	if restoreRecorder.Code != http.StatusNoContent || restoreCalls != 1 || purgeCalls != 0 || gotBearer != "user-secret" || gotRecordID != "rec-1" {
		t.Fatalf("restore status=%d calls=%d/%d bearer=%q record=%q body=%s", restoreRecorder.Code, restoreCalls, purgeCalls, gotBearer, gotRecordID, restoreRecorder.Body.String())
	}

	purge := recycleBinRequest(RecycleBinPath + "/rec-2")
	purge.Method = http.MethodDelete
	purgeRecorder := httptest.NewRecorder()
	service.ServeHTTP(purgeRecorder, purge)
	if purgeRecorder.Code != http.StatusNoContent || restoreCalls != 1 || purgeCalls != 1 || gotBearer != "user-secret" || gotRecordID != "rec-2" {
		t.Fatalf("purge status=%d calls=%d/%d bearer=%q record=%q body=%s", purgeRecorder.Code, restoreCalls, purgeCalls, gotBearer, gotRecordID, purgeRecorder.Body.String())
	}
}

func TestRecycleBinMutationRoutePreservesListedRecordIdentity(t *testing.T) {
	operation, recordID, ok := matchRecycleBinMutationRoute(RecycleBinPath + "/restore/%20row-1")
	if !ok || operation != recycleBinMutationRestore || recordID != " row-1" {
		t.Fatalf("route = %d %q %v, want restore with exact listed identity", operation, recordID, ok)
	}
	if _, _, ok := matchRecycleBinMutationRoute(RecycleBinPath + "/restore/a%2Fb"); ok {
		t.Fatal("encoded path separator was accepted as one record identity")
	}
}

func TestRecycleBinMutationsOwnEveryPublishedCRMVersion(t *testing.T) {
	for path := range supportedRecycleBinPaths {
		t.Run(path, func(t *testing.T) {
			var calls int
			mutation := func(context.Context, string, mutationsemantic.Request) (mutationsemantic.Response, error) {
				calls++
				return mutationsemantic.Response{Receipts: mutationsemantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: mutationsemantic.CapabilityName}}, nil
			}
			service := newRecycleBinMutationTestService(t, mutation, mutation)
			for _, tc := range []struct {
				method string
				path   string
			}{{http.MethodPost, path + "/restore/rec-1"}, {http.MethodDelete, path + "/rec-1"}} {
				request := recycleBinRequest(tc.path)
				request.Method = tc.method
				recorder := httptest.NewRecorder()
				service.ServeHTTP(recorder, request)
				if recorder.Code != http.StatusNoContent {
					t.Fatalf("%s %s status=%d body=%s", tc.method, tc.path, recorder.Code, recorder.Body.String())
				}
			}
			if calls != 2 {
				t.Fatalf("calls=%d, want 2", calls)
			}
		})
	}
}

func TestRecycleBinMutationFailsClosedForPartialPortsAndLocalRejections(t *testing.T) {
	mutation := func(context.Context, string, mutationsemantic.Request) (mutationsemantic.Response, error) {
		return mutationsemantic.Response{}, nil
	}
	base := 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
		},
		RuntimeRestoreRecycleBin: mutation,
		RuntimeReady:             func() bool { return true }, RuntimeMetrics: func() runtimegrpc.RuntimeClientMetrics { return runtimegrpc.RuntimeClientMetrics{} },
		RequestTimeout: time.Second, MaxRequestBytes: 64 * 1024,
	}
	if service, err := NewService(base); err == nil || service != nil {
		t.Fatalf("partial recycle-bin mutation service=%#v err=%v", service, err)
	}

	var calls int
	runtime := func(context.Context, string, mutationsemantic.Request) (mutationsemantic.Response, error) {
		calls++
		return mutationsemantic.Response{}, nil
	}
	service := newRecycleBinMutationTestService(t, runtime, runtime)
	unauthorized := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, RecycleBinPath+"/restore/rec-1", nil)
	service.ServeHTTP(unauthorized, request)
	if unauthorized.Code != http.StatusUnauthorized || calls != 0 {
		t.Fatalf("unauthorized status=%d calls=%d body=%s", unauthorized.Code, calls, unauthorized.Body.String())
	}
	wrongMethod := httptest.NewRecorder()
	request = recycleBinRequest(RecycleBinPath + "/restore/rec-1")
	service.ServeHTTP(wrongMethod, request)
	if wrongMethod.Code != http.StatusMethodNotAllowed || wrongMethod.Header().Get("Allow") != http.MethodPost || calls != 0 {
		t.Fatalf("method status=%d allow=%q calls=%d body=%s", wrongMethod.Code, wrongMethod.Header().Get("Allow"), calls, wrongMethod.Body.String())
	}
}

func TestRecycleBinMutationConflictPreservesSalesforceFraming(t *testing.T) {
	conflict := func(context.Context, string, mutationsemantic.Request) (mutationsemantic.Response, error) {
		return mutationsemantic.Response{}, &mutationsemantic.Failure{Category: mutationsemantic.FailureConflict}
	}
	service := newRecycleBinMutationTestService(t, conflict, conflict)
	request := recycleBinRequest(RecycleBinPath + "/restore/rec-1")
	request.Method = http.MethodPost
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusConflict || recorder.Body.String() != "[{\"errorCode\":\"CONFLICT\",\"message\":\"Record conflicts with current state\"}]" {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}
