package sfcrmappservice

import (
	"context"
	"encoding/json"
	"slices"
	"strings"
	"testing"

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

func testObjectListFieldsReader(
	t *testing.T,
	view catalogsemantic.AppMetadataViewV3,
) RuntimeReadObjectListFieldsFunc {
	t.Helper()
	specs, err := searchObjectSpecsFromView(view)
	if err != nil {
		t.Fatal(err)
	}
	return func(
		_ context.Context,
		_ string,
		request pagelayoutsemantic.ObjectListFieldsRequest,
	) (pagelayoutsemantic.ObjectListFieldsResponse, error) {
		objects := make([]pagelayoutsemantic.ObjectListFields, 0, len(request.ObjectIDs))
		for _, objectID := range request.ObjectIDs {
			spec, found := specs[strings.ToLower(objectID)]
			if !found {
				return pagelayoutsemantic.ObjectListFieldsResponse{}, unavailablePageLayout()
			}
			fields := make([]string, len(spec.ListFields))
			copy(fields, spec.ListFields)
			objects = append(objects, pagelayoutsemantic.ObjectListFields{
				ObjectID: objectID, OrderedFieldIDs: fields,
			})
		}
		receipts := view.Receipts
		receipts.CapabilityVersion = pagelayoutsemantic.CapabilityName
		return pagelayoutsemantic.ObjectListFieldsResponse{
			Objects: objects, Receipts: receipts,
		}, nil
	}
}

func TestPageLayoutRecordTypeIngressIsCanonicalAndBounded(t *testing.T) {
	if !validPageLayoutRecordTypeID("rt/enterprise", false) ||
		!validPageLayoutRecordTypeID("", true) {
		t.Fatal("valid opaque RecordType selectors were rejected")
	}
	for _, value := range []string{
		"", " rt-enterprise", "rt-enterprise ", "\xff",
		strings.Repeat("x", pagelayoutsemantic.MaxOpaqueIdentifierBytes+1),
	} {
		if validPageLayoutRecordTypeID(value, false) {
			t.Fatalf("invalid RecordType selector %q was accepted", value)
		}
	}

	info := recordTypeInfo("v63.0", "Account", projectedRecordType{RecordTypeID: "rt/enterprise"})
	layoutURL := info["urls"].(map[string]string)["layout"]
	route, matched := matchObjectLayoutsRoute(layoutURL)
	if !matched || route.recordTypeID != "rt/enterprise" {
		t.Fatalf("generated opaque RecordType URL %q = %#v matched=%v", layoutURL, route, matched)
	}
	for _, segment := range []string{
		"%20rt-enterprise",
		strings.Repeat("x", pagelayoutsemantic.MaxOpaqueIdentifierBytes+1),
		"%zz",
	} {
		if _, matched := matchObjectLayoutsRoute(
			"/services/data/v63.0/sobjects/Account/describe/layouts/" + segment,
		); matched {
			t.Fatalf("invalid RecordType path segment %q matched", segment)
		}
	}
}

func TestResolvePageLayoutConsumesCallerEffectiveNonDefaultDocument(t *testing.T) {
	view := searchTestView(t, []searchObjectSpec{{APIName: "Opportunity"}})
	recordTypeID := "rt-enterprise"
	var got pagelayoutsemantic.Request
	service := &Service{resolvePageLayoutRuntime: func(
		_ context.Context,
		bearer string,
		request pagelayoutsemantic.Request,
	) (pagelayoutsemantic.Response, error) {
		if bearer != "end-user" {
			t.Fatalf("bearer = %q", bearer)
		}
		got = request
		document, err := json.Marshal(appPageLayout{
			ID: "layout-enterprise", ObjectAPIName: "Opportunity",
			LayoutName: "Enterprise", IsDefault: false,
			Sections: []appPageLayoutSection{}, RelatedLists: []appRelatedList{{
				ID: "Files", Label: "Files", ObjectAPIName: "ContentDocumentLink",
				LookupField: "LinkedEntityId", Fields: []string{"Name"},
			}}, HeaderActions: []appLayoutAction{}, QuickActions: []appLayoutAction{},
		})
		if err != nil {
			t.Fatal(err)
		}
		receipts := view.Receipts
		receipts.CapabilityVersion = pagelayoutsemantic.CapabilityName
		selectedRecordTypeID := request.RecordTypeIDs[0]
		return pagelayoutsemantic.Response{
			ObjectID: "Opportunity",
			Selections: []pagelayoutsemantic.Selection{{
				RecordTypeID: &selectedRecordTypeID, LayoutID: "layout-enterprise",
			}},
			Layouts: []pagelayoutsemantic.ResolvedLayout{{
				LayoutID: "layout-enterprise", DocumentKind: pagelayoutsemantic.PageLayoutDocumentKind,
				DocumentFormatVersion: pagelayoutsemantic.PageLayoutDocumentFormatVersion,
				LayoutDocument:        document,
				RelatedLists: []pagelayoutsemantic.RelatedListResolution{{
					DisplayObjectID: "ContentDocumentLink", DisplayLookupField: "LinkedEntityId",
					Query:  pagelayoutsemantic.ResolvedQueryTarget{ObjectID: "ContentVersion", LookupField: "FirstPublishLocationId"},
					Detail: pagelayoutsemantic.ResolvedDetailTarget{ObjectID: "ContentDocument"},
					List:   pagelayoutsemantic.ResolvedListTarget{ObjectID: "ContentVersion", FilterField: "FirstPublishLocationId"},
					Create: pagelayoutsemantic.ResolvedCreateTarget{Kind: "upload"},
				}},
			}},
			Receipts: receipts,
		}, nil
	}}
	layout, receipts, err := service.resolvePageLayout(
		context.Background(), "end-user", view, "Opportunity", recordTypeID,
	)
	if err != nil {
		t.Fatal(err)
	}
	if got.ObjectID != "Opportunity" || len(got.RecordTypeIDs) != 1 || got.RecordTypeIDs[0] != recordTypeID ||
		got.MinCatalogRevision == nil || *got.MinCatalogRevision != view.Receipts.CatalogRevision {
		t.Fatalf("Runtime request = %#v", got)
	}
	if layout.ID != "layout-enterprise" || layout.IsDefault ||
		layout.RelatedLists[0].Resolution == nil ||
		layout.RelatedLists[0].Resolution.Query.ObjectAPIName != "ContentVersion" ||
		receipts.CapabilityVersion != pagelayoutsemantic.CapabilityName {
		t.Fatalf("resolved L1 layout = %#v receipts=%#v", layout, receipts)
	}
}

func TestVirtualRelatedListAuthorityUsesResolvedRuntimeTargets(t *testing.T) {
	standard := "standard_object"
	specs := map[string]searchObjectSpec{}
	for _, spec := range []searchObjectSpec{
		{APIName: "Parent", TableType: &standard, Fields: []searchFieldSpec{
			{APIName: "Id"}, {APIName: "QueryParentSource"}, {APIName: "ListParentSource"},
		}},
		{APIName: "DisplayChild", TableType: &standard, Fields: []searchFieldSpec{{APIName: "DisplayParentId"}}},
		{APIName: "QueryChild", TableType: &standard, ListFields: []string{"Name"}, Fields: []searchFieldSpec{{APIName: "Name"}, {APIName: "QueryParentId"}}},
		{APIName: "DetailChild", TableType: &standard},
		{APIName: "CreateChild", TableType: &standard, Fields: []searchFieldSpec{{APIName: "CreateParentId", Createable: true}}},
	} {
		specs[strings.ToLower(spec.APIName)] = spec
	}
	layout := appPageLayout{
		ID: "layout-parent", ObjectAPIName: "Parent", LayoutName: "Parent",
		Sections: []appPageLayoutSection{}, RelatedLists: []appRelatedList{{
			ID: "virtual", Label: "Virtual", ObjectAPIName: "DisplayChild",
			LookupField: "DisplayParentId", Fields: []string{"Name"},
			Resolution: &appRelatedListResolution{
				Query: appRelatedListQueryTarget{
					ObjectAPIName: "QueryChild", LookupField: "QueryParentId", ParentFieldAPIName: "QueryParentSource",
				},
				Detail: appRelatedListDetailTarget{ObjectAPIName: "DetailChild"},
				List: appRelatedListListTarget{
					ObjectAPIName: "QueryChild", FilterField: "QueryParentId", ParentFieldAPIName: "ListParentSource",
				},
				Create: appRelatedListCreateTarget{Kind: "standard", ObjectAPIName: "CreateChild", PrefillMappings: map[string]string{"CreateParentId": "Id"}},
			},
		}},
	}
	selection, err := pageLayoutPermissionSelection(layout, specs)
	if err != nil {
		t.Fatal(err)
	}
	if !slices.Equal(selection.ResourceTypes, []string{"CreateChild", "DetailChild", "DisplayChild", "Parent", "QueryChild"}) {
		t.Fatalf("resolved target permission selection = %#v", selection.ResourceTypes)
	}
	permissions := permissionsemantic.Response{
		ObjectPermissions: []permissionsemantic.ObjectPermission{
			{ResourceType: "CreateChild", CanRead: true, CanCreate: true},
			{ResourceType: "DetailChild", CanRead: true},
			{ResourceType: "DisplayChild", CanRead: true},
			{ResourceType: "Parent", CanRead: true},
			{ResourceType: "QueryChild", CanRead: true},
		},
		FieldPermissions: []permissionsemantic.FieldPermission{
			{ResourceType: "CreateChild", FieldName: "CreateParentId", CanRead: true, CanEdit: true, CanCreate: true},
			{ResourceType: "Parent", FieldName: "Id", CanRead: true},
			{ResourceType: "Parent", FieldName: "QueryParentSource", CanRead: true},
			{ResourceType: "Parent", FieldName: "ListParentSource", CanRead: true},
			{ResourceType: "QueryChild", FieldName: "Name", CanRead: true},
			{ResourceType: "QueryChild", FieldName: "QueryParentId", CanRead: true},
		},
	}
	authorized, err := authorizePageLayout(layout, specs, permissions, testCurrentListFields(specs))
	if err != nil {
		t.Fatal(err)
	}
	if len(authorized.RelatedLists) != 1 || authorized.RelatedLists[0].Resolution == nil ||
		authorized.RelatedLists[0].Resolution.Create.PrefillMappings["CreateParentId"] != "Id" {
		t.Fatalf("authorized virtual related list = %#v", authorized.RelatedLists)
	}
	for index := range permissions.ObjectPermissions {
		if permissions.ObjectPermissions[index].ResourceType == "DisplayChild" {
			permissions.ObjectPermissions[index].CanRead = false
		}
	}
	denied, err := authorizePageLayout(layout, specs, permissions, testCurrentListFields(specs))
	if err != nil {
		t.Fatal(err)
	}
	if len(denied.RelatedLists) != 0 {
		t.Fatalf("display-object-denied virtual related list = %#v", denied.RelatedLists)
	}
	for index := range permissions.ObjectPermissions {
		if permissions.ObjectPermissions[index].ResourceType == "DisplayChild" {
			permissions.ObjectPermissions[index].CanRead = true
		}
	}
	index := newPageLayoutAuthorityIndex(specs, permissions)
	projected, err := projectRecordUILayout(
		authorized, specs["parent"], index, permissions.ObjectPermissions[3], permissions, "View",
	)
	if err != nil {
		t.Fatal(err)
	}
	related := projected["relatedLists"].([]map[string]any)
	if len(related) != 1 || related[0]["resolution"] == nil {
		t.Fatalf("projected related lists = %#v", related)
	}
	describeProjected := projectPageLayout(
		authorized, specs["parent"], permissions.ObjectPermissions[3], permissions,
	)
	describeRelated := describeProjected["relatedLists"].([]map[string]any)
	if len(describeRelated) != 1 || describeRelated[0]["resolution"] == nil {
		t.Fatalf("describe projected related lists = %#v", describeRelated)
	}
	for _, fieldName := range []string{"QueryParentSource", "ListParentSource"} {
		restricted := permissions
		restricted.FieldPermissions = append(
			[]permissionsemantic.FieldPermission(nil), permissions.FieldPermissions...,
		)
		for index := range restricted.FieldPermissions {
			if restricted.FieldPermissions[index].ResourceType == "Parent" &&
				restricted.FieldPermissions[index].FieldName == fieldName {
				restricted.FieldPermissions[index].CanRead = false
			}
		}
		denied, err := authorizePageLayout(layout, specs, restricted, testCurrentListFields(specs))
		if err != nil {
			t.Fatal(err)
		}
		if len(denied.RelatedLists) != 0 {
			t.Fatalf("%s-denied virtual related lists = %#v", fieldName, denied.RelatedLists)
		}
	}
}

func testCurrentListFields(specs map[string]searchObjectSpec) map[string][]string {
	result := make(map[string][]string, len(specs))
	for key, spec := range specs {
		fields := make([]string, len(spec.ListFields))
		copy(fields, spec.ListFields)
		result[key] = fields
	}
	return result
}

func TestLayoutHistoryAuthorityUsesCanonicalParentAndPreservesPhysicalObjects(t *testing.T) {
	tableType := "standard_object"
	for _, tc := range []struct {
		parent, history string
		virtual         bool
	}{
		{"Opportunity", "OpportunityFieldHistory", true},
		{"Account", "AccountHistory", true},
		{"Asset__c", "Asset__History", true},
		{"WorkItem", "WorkItemHistory", true},
		{"Opportunity", "OpportunityHistory", false},
		{"Opportunity", "OpportunityFieldHistoryTypo", false},
		{"Event", "EventHistory", false},
	} {
		t.Run(tc.history, func(t *testing.T) {
			parent := searchObjectSpec{APIName: tc.parent, TableType: &tableType, Fields: []searchFieldSpec{{APIName: "Id"}}}
			specs := map[string]searchObjectSpec{strings.ToLower(tc.parent): parent}
			layout := appPageLayout{ObjectAPIName: tc.parent, RelatedLists: []appRelatedList{{ObjectAPIName: tc.history}}}
			want := tc.history
			if tc.virtual {
				want = tc.parent
			}
			if got := pageLayoutListFieldObjectIDs(tc.parent, specs, layout); !slices.Equal(got, []string{tc.parent, want}) {
				t.Fatalf("list-field resource identities=%v", got)
			}
			// A real Catalog object with the same spelling always owns its own
			// current list fields and permissions, even for a recognized suffix.
			specs[strings.ToLower(tc.history)] = searchObjectSpec{APIName: tc.history, TableType: &tableType}
			if got := pageLayoutListFieldObjectIDs(tc.parent, specs, layout); !slices.Equal(got, []string{tc.parent, tc.history}) {
				t.Fatalf("physical history-shaped object lost Runtime authority: %v", got)
			}
		})
	}
}

func TestRelatedHistoryFieldsShareDescribePermissionAuthority(t *testing.T) {
	tableType := "standard_object"
	parent := searchObjectSpec{APIName: "Account", Label: "Account", TableType: &tableType, Fields: []searchFieldSpec{{APIName: "Name"}}}
	specs := map[string]searchObjectSpec{"account": parent}
	for _, canReadField := range []bool{false, true} {
		permissions := permissionsemantic.Response{
			ObjectPermissions: []permissionsemantic.ObjectPermission{{ResourceType: "Account", CanRead: true}},
			FieldPermissions:  []permissionsemantic.FieldPermission{{ResourceType: "Account", FieldName: "Name", CanRead: canReadField}},
		}
		child, fields, readable := relatedListObjectAuthority("AccountHistory", specs, permissions)
		if !readable || child.APIName != "AccountHistory" {
			t.Fatal("readable parent's history missing")
		}
		for _, field := range describeHistoryObjectProjection("v63.0", "AccountHistory", parent, permissions)["fields"].([]map[string]any) {
			name := field["name"].(string)
			got := fields[strings.ToLower(name)]
			if got.CanRead != field["readable"] || got.CanEdit {
				t.Fatalf("layout/describe disagree for %s: %#v", name, got)
			}
		}
		if fields["oldvalue"].CanRead != canReadField || fields["newvalue"].CanRead != canReadField {
			t.Fatal("history values bypass parent FLS")
		}
		permissions.ObjectPermissions[0].CanRead = false
		if _, _, readable := relatedListObjectAuthority("AccountHistory", specs, permissions); readable {
			t.Fatal("history exposed without parent read")
		}
	}
}
