package sfcrmappservice

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

	"github.com/pingcap-inc/nexuscrm-api-host/runtimegrpc"
	querycodec "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/querycodec"
	querysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	sessionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/sessionv1"
)

func discoveryTestService(t *testing.T) *Service {
	s := newTestService(t, &fakeRuntime{})
	s.sessions = &sessionHTTP{publicOrigin: "https://crm.example.test", organizationID: "org-test", read: func(_ context.Context, credentials runtimegrpc.SessionPresentedCredentials, _ sessionsemantic.ReadRequest) (sessionsemantic.ReadResponse, runtimegrpc.SessionIssuedCredentials, error) {
		if credentials.AccessToken != "valid-token" {
			return sessionsemantic.ReadResponse{}, runtimegrpc.SessionIssuedCredentials{}, &sessionsemantic.Failure{Category: sessionsemantic.FailureUnauthenticated}
		}
		return sessionsemantic.ReadResponse{Session: sessionTestResponse(), DocumentPolicy: &sessionsemantic.DocumentPolicy{}}, runtimegrpc.SessionIssuedCredentials{}, nil
	}}
	return s
}

func discoveryRequest(s *Service, method, path, token string) *httptest.ResponseRecorder {
	r := httptest.NewRequest(method, path, nil)
	if token != "" {
		r.Header.Set("Authorization", "Bearer "+token)
	}
	w := httptest.NewRecorder()
	s.ServeHTTP(w, r)
	return w
}

func TestDiscoveryVersionsAndResources(t *testing.T) {
	s := discoveryTestService(t)
	for _, path := range []string{"/services/data", "/services/data/"} {
		w := discoveryRequest(s, "GET", path, "")
		if w.Code != 200 {
			t.Fatalf("%s: %d %s", path, w.Code, w.Body.String())
		}
		var versions []struct{ Version, Label, URL string }
		if err := json.Unmarshal(w.Body.Bytes(), &versions); err != nil {
			t.Fatal(err)
		}
		if len(versions) != len(querycodec.SupportedVersions()) {
			t.Fatalf("versions: %s", w.Body.String())
		}
		for i, v := range versions {
			if v.Version != strings.TrimPrefix(querycodec.SupportedVersions()[i], "v") || v.Label != []string{"Winter '24", "Spring '24", "Summer '24", "Winter '25", "Spring '25"}[i] || v.URL != "/services/data/v"+v.Version {
				t.Fatalf("version row: %+v", v)
			}
			for _, suffix := range []string{"", "/"} {
				resources := discoveryRequest(s, "GET", v.URL+suffix, "valid-token")
				if resources.Code != 200 {
					t.Fatalf("%s: %d %s", v.URL+suffix, resources.Code, resources.Body.String())
				}
				var links map[string]string
				if err := json.Unmarshal(resources.Body.Bytes(), &links); err != nil {
					t.Fatal(err)
				}
				for _, key := range []string{"query", "queryAll", "limits", "composite"} {
					if links[key] != v.URL+"/"+key {
						t.Fatalf("links: %v", links)
					}
				}
				for _, key := range []string{"chatter", "licensing", "theme", "sobjects"} {
					if links[key] != v.URL+"/"+key {
						t.Fatalf("missing legacy resource: %s", key)
					}
				}
			}
		}
	}
}

func TestDiscoveryAuthenticationAndNarrowRoutes(t *testing.T) {
	s := discoveryTestService(t)
	for _, path := range []string{"/services/data/v63.0", "/services/data/v63.0/", "/services/data/v63.0/limits", "/services/data/v63.0/limits/"} {
		for _, token := range []string{"", "invalid-token"} {
			w := discoveryRequest(s, "GET", path, token)
			if w.Code != 401 {
				t.Errorf("%s token=%q: %d", path, token, w.Code)
			}
		}
		w := discoveryRequest(s, "POST", path, "valid-token")
		if w.Code != 405 || w.Header().Get("Allow") != "GET" {
			t.Errorf("POST %s: %d", path, w.Code)
		}
	}
	for _, path := range []string{"/services/data//", "/services/data/v99.0", "/services/data/v63.0//", "/services/data/v63.0/limits/extra", "/services/data/v63.0/limits//"} {
		w := discoveryRequest(s, "GET", path, "valid-token")
		if w.Code != 404 {
			t.Errorf("%s: %d", path, w.Code)
		}
	}
}

func TestQuerySlashAliasesPreserveAuthentication(t *testing.T) {
	s := newTestService(t, &fakeRuntime{})
	for _, version := range querycodec.SupportedVersions() {
		for _, family := range []string{"query", "queryAll"} {
			path := "/services/data/" + version + "/" + family + "/?q=SELECT+Id+FROM+Account"
			w := discoveryRequest(s, "GET", path, "")
			if w.Code != http.StatusMovedPermanently || w.Header().Get("Location") != strings.Replace(path, "/?", "?", 1) {
				t.Errorf("%s: %d %s", path, w.Code, w.Body.String())
			}
			w = discoveryRequest(s, "POST", path, "")
			if w.Code != http.StatusNotFound {
				t.Errorf("POST %s: %d", path, w.Code)
			}
		}
	}
	for _, path := range []string{"/services/data/v63.0/query//", "/services/data/v99.0/query/", "/services/data/v63.0/query/locator/extra"} {
		w := discoveryRequest(s, "GET", path, "")
		if w.Code != 404 {
			t.Errorf("%s: %d", path, w.Code)
		}
	}
}

func TestQuerySlashAliasesPreserveResultsAndPagination(t *testing.T) {
	for _, family := range []string{"query", "queryAll"} {
		var canonical []querysemantic.Request
		var canonicalBody string
		for _, suffix := range []string{"", "/"} {
			var calls []querysemantic.Request
			s := newNotificationTestService(t, func(_ context.Context, bearer string, request querysemantic.Request) (querysemantic.Response, error) {
				if bearer != "valid-token" {
					t.Fatalf("bearer was not preserved")
				}
				calls = append(calls, request)
				id := "001account"
				return querysemantic.Response{TotalSize: 2, Done: len(calls) == 2, Records: []querysemantic.Record{{ObjectID: "Account", Fields: []querysemantic.Field{{Name: "Id", Value: querysemantic.Value{Kind: querysemantic.ValueReference, Reference: &id}}}}}, Receipts: querysemantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: querysemantic.CapabilityName, CatalogRevision: 7}}, nil
			})
			path := "/services/data/v63.0/" + family + suffix + "?q=SELECT+Id+FROM+Account+WHERE+Name%3D%27A%2BB%27+LIMIT+2"
			first := discoveryRequest(s, "GET", path, "valid-token")
			if suffix != "" {
				if first.Code != 301 || len(calls) != 0 {
					t.Fatalf("redirect must precede query: %d calls=%d", first.Code, len(calls))
				}
				first = discoveryRequest(s, "GET", first.Header().Get("Location"), "valid-token")
			}
			if first.Code != 200 || len(calls) != 1 {
				t.Fatalf("%s: %d %s calls=%d", path, first.Code, first.Body.String(), len(calls))
			}
			var page struct {
				NextRecordsURL string `json:"nextRecordsUrl"`
			}
			if err := json.Unmarshal(first.Body.Bytes(), &page); err != nil || !strings.HasPrefix(page.NextRecordsURL, "/services/data/v63.0/query/") {
				t.Fatalf("page: %s", first.Body.String())
			}
			next := discoveryRequest(s, "GET", page.NextRecordsURL, "valid-token")
			if next.Code != 200 || len(calls) != 2 {
				t.Fatalf("pagination: %d %s", next.Code, next.Body.String())
			}
			for _, call := range calls {
				if call.IncludeDeleted != (family == "queryAll") {
					t.Fatalf("deleted visibility changed: %+v", call)
				}
			}
			if suffix == "" {
				canonical = calls
				canonicalBody = first.Body.String()
			} else if !reflect.DeepEqual(calls, canonical) || first.Body.String() != canonicalBody {
				t.Fatalf("alias changed Runtime requests or response")
			}
		}
	}
}

func TestDiscoveryLimitsAndSessionFailure(t *testing.T) {
	s := discoveryTestService(t)
	for _, version := range querycodec.SupportedVersions() {
		for _, suffix := range []string{"", "/"} {
			w := discoveryRequest(s, "GET", "/services/data/"+version+"/limits"+suffix, "valid-token")
			if w.Code != 200 || !legacyLimitsGoldenEqual(t, w.Body.Bytes()) || w.Header().Get("Cache-Control") != "no-store" {
				t.Fatalf("limits: %d %s", w.Code, w.Body.String())
			}
		}
	}
	s.sessions.read = func(ctx context.Context, _ runtimegrpc.SessionPresentedCredentials, _ sessionsemantic.ReadRequest) (sessionsemantic.ReadResponse, runtimegrpc.SessionIssuedCredentials, error) {
		if _, ok := ctx.Deadline(); !ok {
			t.Fatal("session read is not bounded")
		}
		return sessionsemantic.ReadResponse{}, runtimegrpc.SessionIssuedCredentials{}, &sessionsemantic.Failure{Category: sessionsemantic.FailureUnavailable}
	}
	for _, path := range []string{"/services/data/v63.0/", "/services/data/v63.0/limits"} {
		w := discoveryRequest(s, "GET", path, "valid-token")
		if w.Code != 503 {
			t.Fatalf("runtime unavailable: %d %s", w.Code, w.Body.String())
		}
	}
	s.sessions = nil
	if w := discoveryRequest(s, "GET", "/services/data/v63.0/limits", "valid-token"); w.Code != 503 {
		t.Fatalf("missing session client: %d", w.Code)
	}
	s.draining.Store(true)
	if w := discoveryRequest(s, "GET", "/services/data/", ""); w.Code != 503 {
		t.Fatalf("draining: %d", w.Code)
	}
}

func legacyLimitsGoldenEqual(t *testing.T, got []byte) bool {
	t.Helper()
	want, err := os.ReadFile("testdata/legacy-limits.json")
	if err != nil {
		t.Fatal(err)
	}
	var a, b any
	if err = json.Unmarshal(want, &a); err != nil {
		t.Fatal(err)
	}
	if err = json.Unmarshal(got, &b); err != nil {
		t.Fatal(err)
	}
	return reflect.DeepEqual(a, b)
}

func TestDiscoveryLegacyResourceDocument(t *testing.T) {
	s := discoveryTestService(t)
	w := discoveryRequest(s, "GET", "/services/data/v63.0/", "valid-token")
	var got map[string]string
	if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
		t.Fatal(err)
	}
	wantBytes, err := os.ReadFile("testdata/legacy-resources.json")
	if err != nil {
		t.Fatal(err)
	}
	var want map[string]string
	if err = json.Unmarshal(wantBytes, &want); err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("resources = %v, want %v", got, want)
	}
}

func TestDiscoveryUsesRuntimeDocumentDecision(t *testing.T) {
	for _, test := range []struct {
		name     string
		policy   *sessionsemantic.DocumentPolicy
		wantKeys []string
	}{
		{name: "restricted", policy: &sessionsemantic.DocumentPolicy{Restricted: true, AllowedLinks: []string{"query", "identity"}}, wantKeys: []string{"identity", "query"}},
		{name: "restricted empty", policy: &sessionsemantic.DocumentPolicy{Restricted: true}, wantKeys: []string{}},
	} {
		t.Run(test.name, func(t *testing.T) {
			s := discoveryTestService(t)
			s.sessions.read = func(_ context.Context, _ runtimegrpc.SessionPresentedCredentials, request sessionsemantic.ReadRequest) (sessionsemantic.ReadResponse, runtimegrpc.SessionIssuedCredentials, error) {
				want := &sessionsemantic.DocumentPolicyRequest{RouteFamilyPrefix: "/services/data/", LinkRoutes: []sessionsemantic.DocumentLinkRoute{{Link: "identity", Method: "GET", Path: "/id/:orgId/:userId"}, {Link: "identity", Method: "GET", Path: "/services/data/:version/id/:orgId/:userId"}}}
				if !reflect.DeepEqual(request.DocumentPolicy, want) {
					t.Fatalf("missing legacy identity route constraints: %+v", request.DocumentPolicy)
				}
				session := sessionTestResponse()
				session.Principal.PrincipalID = "current-user"
				return sessionsemantic.ReadResponse{Session: session, DocumentPolicy: test.policy}, runtimegrpc.SessionIssuedCredentials{}, nil
			}
			w := discoveryRequest(s, "GET", "/services/data/v63.0/", "valid-token")
			if w.Code != 200 {
				t.Fatalf("%d %s", w.Code, w.Body.String())
			}
			var links map[string]string
			if err := json.Unmarshal(w.Body.Bytes(), &links); err != nil {
				t.Fatal(err)
			}
			if len(links) != len(test.wantKeys) {
				t.Fatalf("unexpected resource visibility: %v", links)
			}
			for _, key := range test.wantKeys {
				if links[key] == "" {
					t.Fatalf("missing %s: %v", key, links)
				}
			}
			if identity, ok := links["identity"]; ok && identity != "https://crm.example.test/id/org-test/current-user" {
				t.Fatalf("identity did not use admitted principal: %s", identity)
			}
		})
	}
}

func TestDiscoveryLimitsUseRuntimeOverridesAndRequireDecision(t *testing.T) {
	s := discoveryTestService(t)
	var policy *sessionsemantic.DocumentPolicy
	s.sessions.read = func(context.Context, runtimegrpc.SessionPresentedCredentials, sessionsemantic.ReadRequest) (sessionsemantic.ReadResponse, runtimegrpc.SessionIssuedCredentials, error) {
		return sessionsemantic.ReadResponse{Session: sessionTestResponse(), DocumentPolicy: policy}, runtimegrpc.SessionIssuedCredentials{}, nil
	}
	for _, path := range []string{"/services/data/v63.0/", "/services/data/v63.0/limits"} {
		w := discoveryRequest(s, "GET", path, "valid-token")
		if w.Code != 503 {
			t.Fatalf("missing Runtime decision returned %d: %s", w.Code, w.Body.String())
		}
	}
	policy = &sessionsemantic.DocumentPolicy{HourlyLimit: 123, DailyLimit: 456}
	w := discoveryRequest(s, "GET", "/services/data/v63.0/limits", "valid-token")
	if w.Code != 200 {
		t.Fatalf("%d %s", w.Code, w.Body.String())
	}
	var rows map[string]struct{ Max, Remaining int64 }
	if err := json.Unmarshal(w.Body.Bytes(), &rows); err != nil {
		t.Fatal(err)
	}
	if len(rows) != 17 || rows["HourlyApiRequests"].Max != 123 || rows["HourlyApiRequests"].Remaining != 123 || rows["DailyApiRequests"].Max != 456 || rows["DailyApiRequests"].Remaining != 456 || rows["DataStorageMB"].Remaining != 4900 {
		t.Fatalf("overridden limits: %s", w.Body.String())
	}
}
