package sfcrmappservice

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

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

func TestObjectQuickActionsUsesRevisionBoundCatalogProjection(t *testing.T) {
	service, view := describeTestService(t, permissionsemantic.Response{
		ObjectPermissions: []permissionsemantic.ObjectPermission{{
			ResourceType: "Lead", CanRead: true, CanEdit: true,
		}},
		FieldPermissions: []permissionsemantic.FieldPermission{{
			ResourceType: "Lead", FieldName: "Status", CanRead: true,
		}},
	})
	service.requestTimeout = time.Second
	behavior := []byte(`{"objectApiName":"Lead","actions":[{"id":"action-upload","name":"UploadArchivedContract","label":"Upload Archived Contr.","type":"UploadFileUpdateRecord","config":{"visibilityCondition":"record.Status == 'Approved'"}}]}`)
	view.EffectiveComponents = append(view.EffectiveComponents, catalogsemantic.EffectiveMetadataComponent{
		ComponentKind:          effectiveObjectActionComponentKind,
		ComponentFormatVersion: 1,
		BusinessKey:            "Lead",
		ContentDigest:          catalogsemantic.ComputeEffectiveMetadataComponentDigest(behavior),
		CanonicalBehavior:      behavior,
	})
	sort.Slice(view.EffectiveComponents, func(left, right int) bool {
		leftKey := view.EffectiveComponents[left].ComponentKind + "\x00" + view.EffectiveComponents[left].BusinessKey
		rightKey := view.EffectiveComponents[right].ComponentKind + "\x00" + view.EffectiveComponents[right].BusinessKey
		return leftKey < rightKey
	})
	view.Lineage.ViewDigest = catalogsemantic.ComputeAppMetadataViewV3Digest(view)
	service.appMetadataV3 = func(context.Context, catalogsemantic.GetRequest) (catalogsemantic.AppMetadataViewV3, error) {
		return view, nil
	}
	originalRead := service.permissionMetadata.readEffective
	var permissionRequest permissionsemantic.Request
	service.permissionMetadata.readEffective = func(
		ctx context.Context,
		bearer string,
		request permissionsemantic.Request,
	) (permissionsemantic.Response, error) {
		permissionRequest = request
		response, err := originalRead(ctx, bearer, request)
		response.RecordTypePermissions = nil
		return response, err
	}
	request := httptest.NewRequest(http.MethodGet, "/services/data/v63.0/sobjects/Lead/quickActions", nil)
	request.Header.Set("Authorization", "Bearer token")
	recorder := httptest.NewRecorder()
	service.serveObjectQuickActions(recorder, request)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	var response struct {
		Data []quickActionWireResponse `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatal(err)
	}
	if len(response.Data) != 1 || response.Data[0].Name != "UploadArchivedContract" ||
		response.Data[0].Type != "UploadFileUpdateRecord" ||
		response.Data[0].Config["visibilityCondition"] != "record.Status == 'Approved'" {
		t.Fatalf("response = %#v", response)
	}
	if permissionRequest.Selection == nil || !slices.Equal(
		permissionRequest.Selection.Sections,
		[]permissionsemantic.Section{
			permissionsemantic.SectionObjectPermissions,
			permissionsemantic.SectionFieldPermissions,
		},
	) || !slices.Equal(permissionRequest.Selection.ResourceTypes, []string{"Lead"}) {
		t.Fatalf("quick-action permission selection = %#v", permissionRequest.Selection)
	}
}

func TestQuickActionPermissionSelectionUsesOnlyMetadataReferencedObjects(t *testing.T) {
	tableType := "standard_object"
	specs := map[string]searchObjectSpec{
		"account":     {APIName: "Account", TableType: &tableType},
		"contact":     {APIName: "Contact", TableType: &tableType},
		"opportunity": {APIName: "Opportunity", TableType: &tableType},
		"case":        {APIName: "Case", TableType: &tableType},
	}
	actions := []effectiveObjectActionSpec{
		{Name: "ChangeOwner", Type: "ChangeOwner", Config: map[string]any{"field": "OwnerId"}},
		{Name: "NewContact", Type: "NavigateToNewRecord", TargetObject: "Contact", Config: map[string]any{"lookupField": "AccountId"}},
		{Name: "OpenOpportunity", Type: "Standard", Config: map[string]any{"visibilityCondition": "Opportunity.Amount > 0"}},
	}

	selection, err := quickActionPermissionSelection("Account", actions, specs)
	if err != nil {
		t.Fatal(err)
	}
	if !slices.Equal(selection.Sections, []permissionsemantic.Section{
		permissionsemantic.SectionObjectPermissions,
		permissionsemantic.SectionFieldPermissions,
	}) || !slices.Equal(selection.ResourceTypes, []string{"Account", "Contact", "Opportunity"}) {
		t.Fatalf("selection = %#v", selection)
	}
}

func TestQuickActionPermissionSelectionFailsClosedOverContractLimit(t *testing.T) {
	tableType := "standard_object"
	specs := map[string]searchObjectSpec{
		"base": {APIName: "Base", TableType: &tableType},
	}
	actions := make([]effectiveObjectActionSpec, 0, permissionsemantic.MaxSelectedResourceTypes)
	for index := 0; index < permissionsemantic.MaxSelectedResourceTypes; index++ {
		apiName := fmt.Sprintf("Target%02d", index)
		specs[strings.ToLower(apiName)] = searchObjectSpec{APIName: apiName, TableType: &tableType}
		actions = append(actions, effectiveObjectActionSpec{
			Name: apiName, Type: "NavigateToNewRecord", TargetObject: apiName,
		})
	}

	if _, err := quickActionPermissionSelection("Base", actions, specs); err == nil {
		t.Fatal("quick-action permission selection unexpectedly widened past the contract limit")
	}
}

func TestProjectQuickActionsOmitsHiddenConfigAndTargetAuthority(t *testing.T) {
	tableType := "standard"
	specs := map[string]searchObjectSpec{
		"account": {
			APIName: "Account", TableType: &tableType,
			Fields: []searchFieldSpec{{APIName: "Secret"}},
		},
		"contact": {
			APIName: "Contact", TableType: &tableType,
			Fields: []searchFieldSpec{{APIName: "AccountId", Createable: true}},
		},
	}
	actions := []effectiveObjectActionSpec{
		{ID: "hidden-config", Name: "HiddenConfig", Label: "Hidden", Type: "Standard", Config: map[string]any{"visibilityCondition": "record.Secret != null"}},
		{ID: "hidden-target", Name: "CreateContact", Label: "New Contact", Type: "NavigateToNewRecord", TargetObject: "Contact", Config: map[string]any{"lookupField": "AccountId"}},
		{ID: "hidden-config-target", Name: "EditProducts", Label: "Edit Products", Type: "NavigateToRelatedList", Config: map[string]any{"targetObject": "Contact", "lookupField": "AccountId"}},
		{ID: "clone-without-create", Name: "Clone", Label: "Clone", Type: "Standard"},
	}
	permissions := permissionsemantic.Response{
		ObjectPermissions: []permissionsemantic.ObjectPermission{{ResourceType: "Account", CanRead: true, CanEdit: true}},
	}
	if projected := projectQuickActions("Account", actions, specs, permissions); len(projected) != 0 {
		t.Fatalf("quick actions = %#v, want unauthorized actions omitted", projected)
	}
}

// A related-list lookup is a query filter, while a new-record lookup is a
// value to write. Object operation authority and field usage are independent.
func TestQuickActionLookupAuthorityFollowsUsage(t *testing.T) {
	for _, tc := range []struct {
		name                                                                         string
		objectCreate, objectEdit, fieldRead, fieldCreate, fieldEdit, intrinsicCreate bool
		missingPermission, missingField                                              bool
		want                                                                         []string
	}{
		{name: "edit_only", fieldEdit: true, objectEdit: true, fieldRead: true, intrinsicCreate: true, want: []string{"EditProducts", "Browse"}},
		{name: "readonly_filter", objectCreate: true, objectEdit: true, fieldRead: true, want: []string{"EditProducts", "Browse"}},
		{name: "create_only", objectCreate: true, fieldRead: true, fieldCreate: true, intrinsicCreate: true, want: []string{"New", "AddProducts", "EditProducts", "Browse"}},
		{name: "field_create_denied", objectCreate: true, fieldRead: true, intrinsicCreate: true, want: []string{"EditProducts", "Browse"}},
		{name: "intrinsic_create_denied", objectCreate: true, fieldRead: true, fieldCreate: true, want: []string{"EditProducts", "Browse"}},
		{name: "object_create_denied", fieldRead: true, fieldCreate: true, intrinsicCreate: true, want: []string{"EditProducts", "Browse"}},
		{name: "all_allowed", objectCreate: true, objectEdit: true, fieldRead: true, fieldCreate: true, intrinsicCreate: true, want: []string{"New", "AddProducts", "EditProducts", "Browse"}},
		{name: "unreadable", objectCreate: true, objectEdit: true, fieldCreate: true, intrinsicCreate: true},
		{name: "missing_permission", objectCreate: true, objectEdit: true, intrinsicCreate: true, missingPermission: true},
		{name: "missing_field", objectCreate: true, objectEdit: true, fieldRead: true, fieldCreate: true, intrinsicCreate: true, missingField: true},
	} {
		t.Run(tc.name, func(t *testing.T) {
			tableType := "standard_object"
			specs := map[string]searchObjectSpec{
				"opportunity":         {APIName: "Opportunity", TableType: &tableType},
				"opportunitylineitem": {APIName: "OpportunityLineItem", TableType: &tableType},
			}
			if !tc.missingField {
				target := specs["opportunitylineitem"]
				target.Fields = []searchFieldSpec{{APIName: "OpportunityId", Createable: tc.intrinsicCreate}}
				specs["opportunitylineitem"] = target
			}
			permissions := permissionsemantic.Response{ObjectPermissions: []permissionsemantic.ObjectPermission{
				{ResourceType: "Opportunity", CanRead: true},
				{ResourceType: "OpportunityLineItem", CanRead: true, CanCreate: tc.objectCreate, CanEdit: tc.objectEdit},
			}}
			if !tc.missingPermission {
				permissions.FieldPermissions = []permissionsemantic.FieldPermission{{ResourceType: "OpportunityLineItem", FieldName: "OpportunityId", CanRead: tc.fieldRead, CanCreate: tc.fieldCreate, CanEdit: tc.fieldEdit}}
			}
			var actions []effectiveObjectActionSpec
			for _, name := range []string{"New", "AddProducts", "EditProducts", "Browse"} {
				kind := "NavigateToRelatedList"
				if name == "New" {
					kind = "NavigateToNewRecord"
				}
				config := map[string]any{"targetObject": "OpportunityLineItem", "lookupField": "OpportunityId"}
				if name == "AddProducts" {
					config["productPicker"] = map[string]any{"lineItemObject": "OpportunityLineItem", "parentLookupField": "OpportunityId", "batchCreate": map[string]any{"targetObject": "OpportunityLineItem"}}
				}
				actions = append(actions, effectiveObjectActionSpec{ID: name, Name: name, Label: name, Type: kind, Config: config})
			}
			got := projectQuickActions("Opportunity", actions, specs, permissions)
			var names []string
			for _, action := range got {
				names = append(names, action.Name)
			}
			if !slices.Equal(names, tc.want) {
				t.Fatalf("visible actions=%v want=%v", names, tc.want)
			}
		})
	}
}
