package sfcrmappservice

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

	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
	pagelayoutsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/pagelayoutresolutionv1"
)

func TestRelatedListCandidatesUseEffectiveRulesAndLiveColumns(t *testing.T) {
	specs := map[string]searchObjectSpec{
		"parent": {APIName: "Parent", Label: "Parent", Fields: []searchFieldSpec{{APIName: "Id", Type: "id"}}},
		"child": {APIName: "Child", Label: "Child", PluralLabel: "Children", DisplayNameField: "Name", Fields: []searchFieldSpec{
			{APIName: "Id", Type: "id"}, {APIName: "Name", Type: "string"}, {APIName: "Subject", Type: "string"}, {APIName: "CreatedDate", Type: "datetime"},
			{APIName: "ParentId", Label: "Parent", Type: "reference", ReferenceTo: []string{"pArEnT"}, RelationshipName: "Parent"},
			{APIName: "OtherParentId", Label: "Other Parent", Type: "reference", ReferenceTo: []string{"Parent"}},
		}},
		"target": {APIName: "Target", Label: "Target", Fields: []searchFieldSpec{{APIName: "Subject", Type: "string"}, {APIName: "ParentId", Type: "reference", ReferenceTo: []string{"Parent"}}}},
	}
	for name, spec := range specs {
		tableType := "standard_object"
		spec.TableType = &tableType
		specs[name] = spec
	}
	explicit := `["Id"]`
	label := "Configured children"
	presentation := relatedListPresentation{
		Relationships: []relatedListPresentationColumns{{ParentObjectAPIName: "Parent", ChildObjectAPIName: "Child", FieldAPIName: "ParentId", RelatedListFields: &explicit, RelatedListLabel: &label}},
		Rules: []relatedListPresentationRule{
			{ParentObjectAPIName: "Parent", ChildObjectAPIName: "Child", LookupField: "ParentId", Bucket: "recommended", SortOrder: -2, Fields: []string{"name", "NAME", "Parent.Id", "Missing"}},
			{ParentObjectAPIName: "Parent", ChildObjectAPIName: "Child", LookupField: "OtherParentId", AppendIfMissing: true, Fields: []string{"Subject", "Name"}, Resolution: &appRelatedListResolution{Query: appRelatedListQueryTarget{ObjectAPIName: "Target", LookupField: "ParentId"}, Detail: appRelatedListDetailTarget{ObjectAPIName: "Target"}, List: appRelatedListListTarget{ObjectAPIName: "Target", FilterField: "ParentId"}}},
		},
	}
	inputs, objects, err := discoverRelatedListCandidates("Parent", specs, presentation)
	if err != nil {
		t.Fatal(err)
	}
	if len(inputs) != 3 || len(objects) != 3 {
		t.Fatalf("discovery = %d candidates, %v", len(inputs), objects)
	}
	candidates, err := assembleRelatedListCandidates(inputs, specs, map[string][]string{"child": {"CreatedDate"}, "target": {"Subject"}})
	if err != nil {
		t.Fatal(err)
	}
	byID := make(map[string]relatedListCandidate)
	for _, candidate := range candidates {
		byID[candidate.ID] = candidate
	}
	if got := byID["Child-ParentId"]; got.Label != label || !got.IsRecommended || !reflect.DeepEqual(got.Fields, []string{"Name"}) {
		t.Fatalf("effective rule and relationship projection = %#v", got)
	}
	if got := byID["Target-ParentId"].Fields; !reflect.DeepEqual(got, []string{"Subject"}) {
		t.Fatalf("live columns = %v", got)
	}
	if got := byID["Child-OtherParentId"].Fields; !reflect.DeepEqual(got, []string{"Subject"}) {
		t.Fatalf("virtual query target columns = %v", got)
	}
	if candidates[0].ID != "Child-ParentId" {
		t.Fatalf("authored ordering lost: %#v", candidates)
	}
	presentation.Rules = nil
	inputs, _, err = discoverRelatedListCandidates("Parent", specs, presentation)
	if err != nil {
		t.Fatal(err)
	}
	candidates, err = assembleRelatedListCandidates(inputs, specs, map[string][]string{"child": {"Name"}, "target": {"Subject"}})
	if err != nil {
		t.Fatal(err)
	}
	for _, candidate := range candidates {
		if candidate.ID == "Child-ParentId" && !reflect.DeepEqual(candidate.Fields, []string{"Id"}) {
			t.Fatalf("explicit relation columns lost: %#v", candidate)
		}
	}
	if _, err := assembleRelatedListCandidates(inputs, specs, map[string][]string{}); err == nil {
		t.Fatal("missing live columns silently accepted")
	}
}

func BenchmarkRelatedListCandidateAssembly(b *testing.B) {
	for _, children := range []int{20, 200} {
		b.Run(fmt.Sprintf("%d_children_100_fields", children), func(b *testing.B) {
			objects := []searchObjectSpec{{APIName: "Parent", Label: "Parent", Fields: []searchFieldSpec{{APIName: "Id", Label: "ID", Type: "string"}}}}
			listFields := make(map[string][]string)
			for index := 0; index < children; index++ {
				name := fmt.Sprintf("Child%d", index)
				fields := []searchFieldSpec{{APIName: "ParentId", Label: "Parent", Type: "reference", ReferenceTo: []string{"Parent"}}}
				for field := 1; field < 100; field++ {
					fields = append(fields, searchFieldSpec{APIName: fmt.Sprintf("Field%d", field), Label: "Field", Type: "string"})
				}
				objects = append(objects, searchObjectSpec{APIName: name, Label: name, Fields: fields})
				listFields[strings.ToLower(name)] = []string{"Field1", "Field2", "Field3", "Field4", "Field5"}
			}
			for index := range objects {
				tableType := "standard_object"
				objects[index].TableType = &tableType
			}
			behavior, err := json.Marshal(effectiveObjectSearchDocument{Objects: objects})
			if err != nil {
				b.Fatal(err)
			}
			view := catalogsemantic.AppMetadataViewV3{EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{ComponentKind: catalogsemantic.EffectiveObjectSearchComponentKind, BusinessKey: catalogsemantic.EffectiveObjectSearchComponentBusinessKey, ComponentFormatVersion: effectiveObjectSearchComponentFormatV4, CanonicalBehavior: behavior, ContentDigest: catalogsemantic.ComputeEffectiveMetadataComponentDigest(behavior)}}}
			b.ReportAllocs()
			b.ResetTimer()
			for range b.N {
				specs, err := searchObjectSpecsFromView(view)
				if err != nil {
					b.Fatal(err)
				}
				inputs, _, err := discoverRelatedListCandidates("Parent", specs, relatedListPresentation{})
				if err != nil {
					b.Fatal(err)
				}
				candidates, err := assembleRelatedListCandidates(inputs, specs, listFields)
				if err != nil || len(candidates) != children {
					b.Fatalf("candidate assembly: %d, %v", len(candidates), err)
				}
			}
		})
	}
}

func TestRelatedListCandidateHTTPBoundary(t *testing.T) {
	for _, test := range []struct {
		name           string
		grant          string
		object         string
		component      bool
		child          bool
		stale          bool
		wantStatus     int
		wantCandidates int
	}{
		{"configured", "CustomizeApplication", "Parent", true, true, false, 200, 1},
		{"setup read", "ViewSetup", "Parent", true, true, false, 200, 1},
		{"empty", "CustomizeApplication", "Parent", true, false, false, 200, 0},
		{"forbidden", "", "Parent", true, true, false, 403, 0},
		{"missing object", "CustomizeApplication", "Missing", true, true, false, 404, 0},
		{"missing projection", "CustomizeApplication", "Parent", false, true, false, 503, 0},
		{"authority changed", "CustomizeApplication", "Parent", true, true, true, 503, 0},
	} {
		t.Run(test.name, func(t *testing.T) {
			objects := []searchObjectSpec{{APIName: "Parent", Label: "Parent", Fields: []searchFieldSpec{{APIName: "Id", Label: "ID", Type: "string"}}}}
			if test.child {
				objects = append(objects, searchObjectSpec{APIName: "Child", Label: "Child", Fields: []searchFieldSpec{{APIName: "Name", Label: "Name", Type: "string"}, {APIName: "ParentId", Label: "Parent", Type: "reference", ReferenceTo: []string{"Parent"}}}})
			}
			for index := range objects {
				tableType := "standard_object"
				objects[index].TableType = &tableType
			}
			view := searchTestView(t, objects)
			if test.component {
				behavior := []byte(`{"relationships":[],"rules":[]}`)
				view.EffectiveComponents = append(view.EffectiveComponents, catalogsemantic.EffectiveMetadataComponent{ComponentKind: catalogsemantic.EffectiveRelatedListPresentationComponentKind, BusinessKey: catalogsemantic.EffectiveRelatedListPresentationComponentBusinessKey, ComponentFormatVersion: 1, CanonicalBehavior: behavior, ContentDigest: catalogsemantic.ComputeEffectiveMetadataComponentDigest(behavior)})
				view.Lineage.ViewDigest = catalogsemantic.ComputeAppMetadataViewV3Digest(view)
			}
			if err := catalogsemantic.ValidateAppMetadataViewV3(view); err != nil {
				t.Fatal(err)
			}
			metadataCalls, permissionCalls, columnCalls := 0, 0, 0
			service := &Service{requestTimeout: time.Second,
				appMetadataV3: func(context.Context, catalogsemantic.GetRequest) (catalogsemantic.AppMetadataViewV3, error) {
					metadataCalls++
					return view, nil
				},
				permissionMetadata: &RuntimePermissionMetadataClient{readEffective: func(_ context.Context, bearer string, request permissionsemantic.Request) (permissionsemantic.Response, error) {
					permissionCalls++
					if bearer != "user-token" || request.MinCatalogRevision == nil || *request.MinCatalogRevision != view.Receipts.CatalogRevision {
						t.Fatal("permission authority not bound")
					}
					receipts := view.Receipts
					receipts.CapabilityVersion = permissionsemantic.CapabilityName
					response := permissionsemantic.Response{Receipts: receipts}
					if test.grant != "" {
						response.SystemPermissions = []permissionsemantic.PermissionGrant{{PermissionID: test.grant, Granted: true}}
					}
					return selectedPermissionTestResponse(response, request), nil
				}},
				readObjectListFieldsRuntime: func(_ context.Context, bearer string, request pagelayoutsemantic.ObjectListFieldsRequest) (pagelayoutsemantic.ObjectListFieldsResponse, error) {
					columnCalls++
					if bearer != "user-token" || !reflect.DeepEqual(request.ObjectIDs, []string{"Child"}) {
						t.Fatalf("columns not batched: %#v", request)
					}
					receipts := view.Receipts
					receipts.CapabilityVersion = pagelayoutsemantic.CapabilityName
					if test.stale {
						receipts.SecurityEpoch++
					}
					return pagelayoutsemantic.ObjectListFieldsResponse{Receipts: receipts, Objects: []pagelayoutsemantic.ObjectListFields{{ObjectID: "Child", OrderedFieldIDs: []string{"Name"}}}}, nil
				},
			}
			request := httptest.NewRequest(http.MethodGet, "/services/data/v63.0/ui-api/related-list-candidates/"+test.object, nil)
			request.Header.Set("Authorization", "Bearer user-token")
			response := httptest.NewRecorder()
			service.ServeHTTP(response, request)
			if response.Code != test.wantStatus {
				t.Fatalf("HTTP = %d, body %s", response.Code, response.Body.String())
			}
			if metadataCalls != 1 || permissionCalls != 1 || columnCalls > 1 {
				t.Fatalf("read counts = %d/%d/%d", metadataCalls, permissionCalls, columnCalls)
			}
			if test.wantStatus == 200 {
				var result struct {
					Candidates []relatedListCandidate `json:"candidates"`
				}
				if err := json.Unmarshal(response.Body.Bytes(), &result); err != nil {
					t.Fatal(err)
				}
				if result.Candidates == nil || len(result.Candidates) != test.wantCandidates+1 {
					t.Fatalf("candidate envelope = %s", response.Body.String())
				}
				history := result.Candidates[len(result.Candidates)-1]
				if history.ID != "ParentHistory-ParentId" || !history.IsSystem || !reflect.DeepEqual(history.Fields, []string{"Field", "OldValue", "NewValue", "CreatedDate", "CreatedById"}) {
					t.Fatalf("history candidate = %#v", history)
				}
				if test.wantCandidates > 0 && !reflect.DeepEqual(result.Candidates[0].Fields, []string{"Name"}) {
					t.Fatalf("live columns lost: %#v", result.Candidates)
				}
			} else if strings.Contains(response.Body.String(), "candidates") {
				t.Fatalf("error became empty success: %s", response.Body.String())
			}
		})
	}
}

func TestConflictReasonHTTPMessages(t *testing.T) {
	for _, test := range []struct {
		reason  commonsemantic.ConflictReason
		message string
	}{
		{commonsemantic.ConflictRevisionMismatch, "Reload the latest version"},
		{commonsemantic.ConflictReferenced, "Remove its references"},
		{commonsemantic.ConflictDefaultRequired, "Choose another default"},
		{"", "Record conflicts with current state"},
	} {
		response := httptest.NewRecorder()
		writeRuntimeFailure(response, &commonsemantic.Failure{Category: commonsemantic.FailureConflict, ConflictReason: test.reason})
		if response.Code != http.StatusConflict || !strings.Contains(response.Body.String(), test.message) {
			t.Fatalf("reason %s => %d %s", test.reason, response.Code, response.Body.String())
		}
	}
}

func TestRelatedListPresentationRequiresCompleteUnambiguousComponent(t *testing.T) {
	valid := relatedListPresentation{Rules: []relatedListPresentationRule{}, Relationships: []relatedListPresentationColumns{}}
	encoded, err := json.Marshal(valid)
	if err != nil {
		t.Fatal(err)
	}
	component := catalogsemantic.EffectiveMetadataComponent{ComponentKind: catalogsemantic.EffectiveRelatedListPresentationComponentKind, BusinessKey: catalogsemantic.EffectiveRelatedListPresentationComponentBusinessKey, ComponentFormatVersion: 1, CanonicalBehavior: encoded}
	if _, err := relatedListPresentationFromView(catalogsemantic.AppMetadataViewV3{EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{component}}); err != nil {
		t.Fatal(err)
	}
	for _, components := range [][]catalogsemantic.EffectiveMetadataComponent{nil, {component, component}} {
		if _, err := relatedListPresentationFromView(catalogsemantic.AppMetadataViewV3{EffectiveComponents: components}); err == nil {
			t.Fatal("missing/ambiguous presentation accepted")
		}
	}
	for _, raw := range []string{`{}`, `{"rules":[],"relationships":[],"unknown":true}`, `{"rules":[{"parentObjectApiName":"Parent","childObjectApiName":"Child","lookupField":"ParentId","bucket":"invalid"}],"relationships":[]}`} {
		component.CanonicalBehavior = []byte(raw)
		if _, err := relatedListPresentationFromView(catalogsemantic.AppMetadataViewV3{EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{component}}); err == nil {
			t.Fatalf("malformed presentation accepted: %s", raw)
		}
	}
}

func TestRelatedListCandidateIngressRejectsBeforeMetadataRead(t *testing.T) {
	for _, test := range []struct {
		name, method, path, bearer string
		draining                   bool
		status                     int
	}{
		{"missing bearer", "GET", "/services/data/v63.0/ui-api/related-list-candidates/Parent", "", false, http.StatusUnauthorized},
		{"write method", "POST", "/services/data/v63.0/ui-api/related-list-candidates/Parent", "Bearer user", false, http.StatusMethodNotAllowed},
		{"unsupported version", "GET", "/services/data/v999.0/ui-api/related-list-candidates/Parent", "Bearer user", false, http.StatusNotFound},
		{"invalid object", "GET", "/services/data/v63.0/ui-api/related-list-candidates/Parent%2FChild", "Bearer user", false, http.StatusBadRequest},
		{"draining", "GET", "/services/data/v63.0/ui-api/related-list-candidates/Parent", "Bearer user", true, http.StatusServiceUnavailable},
	} {
		t.Run(test.name, func(t *testing.T) {
			service := &Service{requestTimeout: time.Second, appMetadataV3: func(context.Context, catalogsemantic.GetRequest) (catalogsemantic.AppMetadataViewV3, error) {
				t.Fatal("rejected ingress reached metadata")
				return catalogsemantic.AppMetadataViewV3{}, nil
			}}
			service.draining.Store(test.draining)
			request := httptest.NewRequest(test.method, test.path, nil)
			request.Header.Set("Authorization", test.bearer)
			response := httptest.NewRecorder()
			service.ServeHTTP(response, request)
			if response.Code != test.status {
				t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
			}
			if test.status == http.StatusMethodNotAllowed && response.Header().Get("Allow") != "GET" {
				t.Fatalf("missing allowed method: %v", response.Header())
			}
		})
	}
}
