package sfcrmappservice

import (
	"context"
	"encoding/json"
	"os"
	"reflect"
	"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 officialLayoutActionFixture(t *testing.T) (appPageLayout, effectiveObjectActionSpec) {
	t.Helper()
	raw, err := os.ReadFile("../../salesforce-crm-standard/layouts/opportunity_page_layout.json")
	if err != nil {
		t.Fatal(err)
	}
	layout, err := decodeAppPageLayout(raw)
	if err != nil {
		t.Fatal(err)
	}
	raw, err = os.ReadFile("../../salesforce-crm-standard/actions/ui_actions.json")
	if err != nil {
		t.Fatal(err)
	}
	var actions []effectiveObjectActionSpec
	if err := json.Unmarshal(raw, &actions); err != nil {
		t.Fatal(err)
	}
	for _, action := range actions {
		if action.Name == "AddProducts" {
			return layout, action
		}
	}
	t.Fatal("official AddProducts definition missing")
	return appPageLayout{}, effectiveObjectActionSpec{}
}

func withActionDefinition(t *testing.T, view catalogsemantic.AppMetadataViewV3, object string, action effectiveObjectActionSpec) catalogsemantic.AppMetadataViewV3 {
	t.Helper()
	behavior, err := json.Marshal(effectiveObjectActionDocument{ObjectAPIName: object, Actions: []effectiveObjectActionSpec{action}})
	if err != nil {
		t.Fatal(err)
	}
	view.EffectiveComponents = append(view.EffectiveComponents, catalogsemantic.EffectiveMetadataComponent{
		ComponentKind: effectiveObjectActionComponentKind, ComponentFormatVersion: 1, BusinessKey: object,
		ContentDigest: catalogsemantic.ComputeEffectiveMetadataComponentDigest(behavior), CanonicalBehavior: behavior,
	})
	slices.SortFunc(view.EffectiveComponents, func(a, b catalogsemantic.EffectiveMetadataComponent) int {
		return strings.Compare(a.ComponentKind+"\x00"+a.BusinessKey, b.ComponentKind+"\x00"+b.BusinessKey)
	})
	view.Lineage.ViewDigest = catalogsemantic.ComputeAppMetadataViewV3Digest(view)
	return view
}

// Exercise the real shared Runtime layout assembly before permission selection,
// using the authored layout and action documents rather than a hand-built picker.
func TestResolvedLayoutAndQuickActionsShareCreationAuthority(t *testing.T) {
	for _, tc := range []struct {
		name                                       string
		objectCreate, fieldCreate, intrinsicCreate bool
	}{
		{"allowed", true, true, true}, {"object_denied", false, true, true},
		{"field_denied", true, false, true}, {"intrinsic_denied", true, true, false},
	} {
		for _, surface := range []string{"header", "quick", "legacy_placement"} {
			t.Run(tc.name+"/"+surface, func(t *testing.T) {
				layout, action := officialLayoutActionFixture(t)
				if surface == "legacy_placement" {
					layout.HeaderActions[0].Type = action.Type
					layout.HeaderActions[0].TargetObject = action.TargetObject
					layout.HeaderActions[0].Config = map[string]any{"targetObject": "OpportunityLineItem", "lookupField": "OpportunityId", "visibilityCondition": "record.Pricebook2Id != null"}
				}
				if surface == "quick" {
					layout.QuickActions = layout.HeaderActions[:1]
					layout.HeaderActions = nil
				}
				table := "standard_object"
				specList := []searchObjectSpec{
					{APIName: "Opportunity", TableType: &table, Fields: []searchFieldSpec{{APIName: "Pricebook2Id"}}},
					{APIName: "OpportunityLineItem", TableType: &table, Fields: []searchFieldSpec{{APIName: "OpportunityId", Createable: tc.intrinsicCreate}}},
					{APIName: "Product2", TableType: &table}, {APIName: "PricebookEntry", TableType: &table},
				}
				specs := map[string]searchObjectSpec{}
				for _, spec := range specList {
					specs[strings.ToLower(spec.APIName)] = spec
				}
				view := withActionDefinition(t, searchTestView(t, specList), "Opportunity", action)
				document, err := json.Marshal(layout)
				if err != nil {
					t.Fatal(err)
				}
				service := &Service{resolvePageLayoutRuntime: func(_ context.Context, _ string, request pagelayoutsemantic.Request) (pagelayoutsemantic.Response, error) {
					if request.MinCatalogRevision == nil || *request.MinCatalogRevision != view.Receipts.CatalogRevision {
						t.Fatal("layout read did not pin Catalog revision")
					}
					receipts := view.Receipts
					receipts.CapabilityVersion = pagelayoutsemantic.CapabilityName
					return pagelayoutsemantic.Response{ObjectID: "Opportunity", Receipts: receipts,
						Selections: []pagelayoutsemantic.Selection{{LayoutID: layout.ID}},
						Layouts:    []pagelayoutsemantic.ResolvedLayout{{LayoutID: layout.ID, DocumentKind: pagelayoutsemantic.PageLayoutDocumentKind, DocumentFormatVersion: pagelayoutsemantic.PageLayoutDocumentFormatVersion, LayoutDocument: document}},
					}, nil
				}}
				resolved, _, err := service.resolvePageLayout(context.Background(), "end-user", view, "Opportunity", "")
				if err != nil {
					t.Fatal(err)
				}
				selection, err := pageLayoutPermissionSelection(resolved, specs)
				if err != nil {
					t.Fatal(err)
				}
				for _, object := range []string{"Opportunity", "OpportunityLineItem", "Product2", "PricebookEntry"} {
					if !slices.Contains(selection.ResourceTypes, object) {
						t.Fatalf("action dependency %s not selected: %v", object, selection.ResourceTypes)
					}
				}
				perms := permissionsemantic.Response{FieldPermissions: []permissionsemantic.FieldPermission{
					{ResourceType: "Opportunity", FieldName: "Pricebook2Id", CanRead: true},
					{ResourceType: "OpportunityLineItem", FieldName: "OpportunityId", CanRead: true, CanCreate: tc.fieldCreate},
				}}
				for _, spec := range specList {
					perms.ObjectPermissions = append(perms.ObjectPermissions, permissionsemantic.ObjectPermission{ResourceType: spec.APIName, CanRead: true, CanCreate: spec.APIName == "OpportunityLineItem" && tc.objectCreate})
				}
				authorized, err := authorizePageLayout(resolved, specs, perms, testCurrentListFields(specs))
				if err != nil {
					t.Fatal(err)
				}
				want := tc.objectCreate && tc.fieldCreate && tc.intrinsicCreate
				var found *appLayoutAction
				for _, placement := range append(authorized.HeaderActions, authorized.QuickActions...) {
					if placement.Name == action.Name {
						copy := placement
						found = &copy
					}
				}
				quick := projectQuickActions("Opportunity", []effectiveObjectActionSpec{action}, specs, perms)
				if (found != nil) != want || (len(quick) == 1) != want {
					t.Fatalf("layout=%v quick=%d want=%v", found, len(quick), want)
				}
				if found != nil && !reflect.DeepEqual(found.Config, quick[0].Config) {
					t.Fatal("layout and quick-action execution payloads differ")
				}
			})
		}
	}
}

func TestLayoutActionDefinitionConflictsAndNamespaces(t *testing.T) {
	for _, mode := range []string{"missing_definition", "missing_component", "conflicting_target", "conflicting_config", "conflicting_type", "standard_namespace", "inline_action", "renamed_reference"} {
		t.Run(mode, func(t *testing.T) {
			layout, action := officialLayoutActionFixture(t)
			view := searchTestView(t, []searchObjectSpec{{APIName: "Opportunity"}})
			placement := &layout.HeaderActions[0]
			wantError := true
			switch mode {
			case "missing_definition":
				action.Name = "Other"
			case "missing_component":
			case "conflicting_target":
				placement.TargetObject = "Other"
			case "conflicting_config":
				placement.Config = map[string]any{"productPicker": false}
			case "conflicting_type":
				placement.Type = "NavigateToNewRecord"
			case "standard_namespace":
				placement.Type = "Standard"
				wantError = false
			case "inline_action":
				placement.Name = "Inline"
				placement.Type = "NavigateToRelatedList"
				placement.Config = map[string]any{"targetObject": "OpportunityLineItem", "lookupField": "OpportunityId"}
				wantError = false
			case "renamed_reference":
				placement.Name = "Renamed"
				action.Name = "Renamed"
				wantError = false
			}
			if mode != "missing_component" {
				view = withActionDefinition(t, view, "Opportunity", action)
			}
			err := resolveLayoutActionDefinitions(&layout, view)
			if (err != nil) != wantError {
				t.Fatalf("error=%v wantError=%v", err, wantError)
			}
			if mode == "standard_namespace" && (placement.Type != "Standard" || placement.Config != nil) {
				t.Fatal("Standard placement consumed quick-action definition")
			}
			if mode == "renamed_reference" && !reflect.DeepEqual(placement.Config, action.Config) {
				t.Fatal("renamed reference was not resolved")
			}
		})
	}
}

func TestStandardQuickActionCRUDHasOneOperationOwner(t *testing.T) {
	table := "standard_object"
	specs := map[string]searchObjectSpec{"account": {APIName: "Account", TableType: &table}}
	for _, tc := range []struct {
		name       string
		permission permissionsemantic.ObjectPermission
		want       string
	}{
		{"delete_without_edit", permissionsemantic.ObjectPermission{CanDelete: true}, "Delete"},
		{"clone_without_edit", permissionsemantic.ObjectPermission{CanCreate: true}, "Clone"},
		{"edit_without_delete", permissionsemantic.ObjectPermission{CanEdit: true}, "Edit"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			tc.permission.ResourceType = "Account"
			tc.permission.CanRead = true
			perms := permissionsemantic.Response{ObjectPermissions: []permissionsemantic.ObjectPermission{tc.permission}}
			actions := []effectiveObjectActionSpec{{Name: "Delete", Type: "Standard"}, {Name: "Clone", Type: "Standard"}, {Name: "Edit", Type: "Standard"}}
			got := projectQuickActions("Account", actions, specs, perms)
			if len(got) != 1 || got[0].Name != tc.want {
				t.Fatalf("actions=%v want=%s", got, tc.want)
			}
		})
	}
}
