package standardaction

import (
	"context"
	"errors"
	"testing"

	"github.com/nexuscrm/automation"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

type catalogInvocationPermissionChecker struct {
	permissions map[string]bool
	objects     map[string]bool
}

func (checker catalogInvocationPermissionChecker) HasPermission(
	_ context.Context,
	_ *models.UserSession,
	permission string,
) bool {
	return checker.permissions[permission]
}

func (checker catalogInvocationPermissionChecker) CheckObjectPermissionWithUser(
	_ context.Context,
	objectAPIName string,
	operation string,
	_ *models.UserSession,
) bool {
	return checker.objects[objectAPIName+"."+operation]
}

func (checker catalogInvocationPermissionChecker) CheckFieldVisibilityWithUser(
	_ context.Context,
	_ string,
	_ string,
	_ *models.UserSession,
) (bool, error) {
	return true, nil
}

type catalogInvocationMetadata struct{}

func (catalogInvocationMetadata) GetSchema(context.Context, string) *models.ObjectMetadata {
	return nil
}

func TestCatalogInvocationUsesImmutableActionAuthorityAndExistingGuardEngine(t *testing.T) {
	invocation, err := ResolveCatalogInvocation(
		"sha256:component",
		[]byte(`[{"name":"doThing","flowDeveloperName":"BoundedFlow","requiredPermissions":["DoThing"],"isAtomic":true,"inputs":[{"apiName":"recordId","dataType":"id","isRequired":true}],"fieldDefaults":[{"targetObject":"$Input","fieldAPIName":"recordId","valueFormula":"\"001\"","priority":0},{"targetObject":"Task","fieldAPIName":"Subject","valueFormula":"\"Created\"","priority":10}],"inputGuards":[{"condition":"true","requiredObject":"Task","requiredOperation":"Edit","sortOrder":0}],"internalAccessScopes":[{"objectApiName":"Task","operation":"Read"}]}]`),
		"doThing",
	)
	if err != nil {
		t.Fatal(err)
	}
	if invocation.ActionRef() != "doThing" ||
		invocation.DefinitionRef() != "BoundedFlow" ||
		!invocation.Atomic() {
		t.Fatalf("Catalog invocation identity = %#v", invocation)
	}

	inputs := map[string]interface{}{}
	prepared, outcome, reject, err := invocation.Prepare(
		context.Background(),
		catalogInvocationPermissionChecker{
			permissions: map[string]bool{"DoThing": true},
			objects:     map[string]bool{"Task.Edit": true},
		},
		nil,
		inputs,
		&models.UserSession{ID: "user-1"},
	)
	if err != nil || outcome != nil || reject != nil {
		t.Fatalf("Prepare() = outcome %#v reject %#v error %v", outcome, reject, err)
	}
	if inputs["recordId"] != "001" {
		t.Fatalf("input default = %#v", inputs)
	}
	if !constants.StandardActionExecutionOwnsRootFlow(prepared, "BoundedFlow") ||
		constants.StandardActionExecutionRef(prepared) != "doThing" ||
		!constants.IsScopedInternalAccess(prepared, "Task", constants.PermRead) {
		t.Fatal("prepared context is missing exact root or internal access scope")
	}
	defaults, ok := automation.ActionFieldDefaultsFromCtx(prepared)
	if !ok || len(defaults) != 1 || defaults[0].TargetObject != "Task" ||
		defaults[0].FieldAPIName != "Subject" {
		t.Fatalf("record defaults = %#v, present=%v", defaults, ok)
	}
}

func TestPreparedCatalogInvocationRebindsDetachedAuthorityToTransactionContext(t *testing.T) {
	invocation, err := ResolveCatalogInvocation(
		"sha256:component",
		[]byte(`[{"name":"doThing","flowDeveloperName":"BoundedFlow","requiredPermissions":["DoThing"],"isAtomic":true,"inputs":[{"apiName":"recordId","dataType":"id","isRequired":true}],"fieldDefaults":[{"targetObject":"$Input","fieldAPIName":"recordId","valueFormula":"\"001\"","priority":0},{"targetObject":"Task","fieldAPIName":"Subject","valueFormula":"\"Created\"","priority":10}],"internalAccessScopes":[{"objectApiName":"Task","operation":"Read"}]}]`),
		"doThing",
	)
	if err != nil {
		t.Fatal(err)
	}
	plan, outcome, reject, err := invocation.PreparePlan(
		context.Background(),
		catalogInvocationPermissionChecker{permissions: map[string]bool{"DoThing": true}},
		nil,
		map[string]interface{}{},
		&models.UserSession{ID: "user-1"},
	)
	if err != nil || outcome != nil || reject != nil || plan == nil {
		t.Fatalf("PreparePlan() = plan %#v outcome %#v reject %#v error %v", plan, outcome, reject, err)
	}
	preparedInputs, err := plan.Inputs()
	if err != nil || preparedInputs["recordId"] != "001" {
		t.Fatalf("prepared inputs = %#v, error %v", preparedInputs, err)
	}
	preparedInputs["recordId"] = "mutated"
	detachedInputs, err := plan.Inputs()
	if err != nil || detachedInputs["recordId"] != "001" {
		t.Fatalf("detached inputs = %#v, error %v", detachedInputs, err)
	}

	// Mutating the source invocation after preparation cannot change the plan
	// that crosses the transaction boundary.
	invocation.fieldDefaults[1].FieldAPIName = "Mutated"
	invocation.internalScopes[0].ObjectAPIName = "Mutated"
	type transactionMarker struct{}
	txCtx := context.WithValue(context.Background(), transactionMarker{}, "tx")
	bound := plan.Bind(txCtx)
	if bound.Value(transactionMarker{}) != "tx" ||
		!constants.StandardActionExecutionOwnsRootFlow(bound, "BoundedFlow") ||
		constants.StandardActionExecutionRef(bound) != "doThing" ||
		!constants.IsScopedInternalAccess(bound, "Task", constants.PermRead) ||
		constants.IsScopedInternalAccess(bound, "Mutated", constants.PermRead) {
		t.Fatalf("prepared transaction authority was not detached/rebound")
	}
	defaults, ok := automation.ActionFieldDefaultsFromCtx(bound)
	if !ok || len(defaults) != 1 || defaults[0].FieldAPIName != "Subject" {
		t.Fatalf("detached defaults = %#v, present=%v", defaults, ok)
	}
}

func TestCatalogInvocationPreservesExistingHighPriorityDefaultPrecedence(t *testing.T) {
	invocation, err := ResolveCatalogInvocation(
		"sha256:component",
		[]byte(`[{"name":"doThing","flowDeveloperName":"BoundedFlow","requiredPermissions":["DoThing"],"isAtomic":true,"inputs":[{"apiName":"recordId","dataType":"id"}],"fieldDefaults":[{"targetObject":"$Input","fieldAPIName":"recordId","valueFormula":"\"low-input\"","priority":1},{"targetObject":"Task","fieldAPIName":"Subject","valueFormula":"\"low-record\"","priority":1},{"targetObject":"$Input","fieldAPIName":"recordId","valueFormula":"\"high-input\"","priority":10},{"targetObject":"Task","fieldAPIName":"Subject","valueFormula":"\"high-record\"","priority":10}]}]`),
		"doThing",
	)
	if err != nil {
		t.Fatal(err)
	}
	inputs := map[string]interface{}{}
	prepared, outcome, reject, err := invocation.Prepare(
		context.Background(),
		catalogInvocationPermissionChecker{permissions: map[string]bool{"DoThing": true}},
		nil,
		inputs,
		&models.UserSession{ID: "user-1"},
	)
	if err != nil || outcome != nil || reject != nil || inputs["recordId"] != "high-input" {
		t.Fatalf(
			"Prepare() = inputs %#v outcome %#v reject %#v error %v",
			inputs, outcome, reject, err,
		)
	}
	record := models.SObject{}
	if err := automation.NewDefaultFieldDefaultResolver(nil).Apply(
		prepared, record, "Task", inputs, false,
	); err != nil {
		t.Fatal(err)
	}
	if record["Subject"] != "high-record" {
		t.Fatalf("record defaults = %#v, want high-priority value", record)
	}
}

func TestCatalogInvocationFailsClosedBeforeExecution(t *testing.T) {
	invocation, err := ResolveCatalogInvocation(
		"component-1",
		[]byte(`[{"name":"doThing","flowDeveloperName":"BoundedFlow","requiredPermissions":["DoThing"],"isAtomic":true,"inputs":[{"apiName":"recordId","dataType":"id","isRequired":true}],"outputs":[{"apiName":"result","dataType":"string"}],"inputGuards":[{"condition":"true","requiredObject":"Task","requiredOperation":"Edit"}]}]`),
		"doThing",
	)
	if err != nil {
		t.Fatal(err)
	}
	user := &models.UserSession{ID: "user-1"}

	_, outcome, reject, err := invocation.Prepare(
		context.Background(), catalogInvocationPermissionChecker{}, nil,
		map[string]interface{}{"recordId": "001"}, user,
	)
	if err != nil || outcome == nil || reject != nil {
		t.Fatalf("missing required permission = outcome %#v reject %#v error %v", outcome, reject, err)
	}

	_, outcome, reject, err = invocation.Prepare(
		context.Background(),
		catalogInvocationPermissionChecker{permissions: map[string]bool{"DoThing": true}},
		nil,
		map[string]interface{}{"recordId": "001"},
		user,
	)
	if err != nil || outcome != nil || reject == nil ||
		reject.Code != RejectInsufficientPerms ||
		reject.DiagnosticKind() != diagnosticGuardDenied ||
		reject.DiagnosticTarget() != "object:Task:Edit" {
		t.Fatalf("missing object guard = outcome %#v reject %#v error %v", outcome, reject, err)
	}

	_, outcome, reject, err = invocation.Prepare(
		context.Background(),
		catalogInvocationPermissionChecker{
			permissions: map[string]bool{"DoThing": true},
			objects:     map[string]bool{"Task.Edit": true},
		},
		nil,
		map[string]interface{}{"recordId": "001", "undeclared": true},
		user,
	)
	if err != nil || outcome != nil || reject == nil || reject.Code != RejectInvalidField || reject.DiagnosticKind() != diagnosticUnknownInput {
		t.Fatalf("unknown input = outcome %#v reject %#v error %v", outcome, reject, err)
	}

	_, outcome, reject, err = invocation.Prepare(
		context.Background(),
		catalogInvocationPermissionChecker{
			permissions: map[string]bool{"DoThing": true},
			objects:     map[string]bool{"Task.Edit": true},
		},
		nil,
		map[string]interface{}{"recordId": "001", "result": "forged-output"},
		user,
	)
	if err != nil || outcome != nil || reject == nil || reject.Code != RejectInvalidField {
		t.Fatalf("output supplied as input = outcome %#v reject %#v error %v", outcome, reject, err)
	}
}

func TestCatalogInvocationRejectsCallerInputsWhenCatalogDeclaresNone(t *testing.T) {
	invocation, err := ResolveCatalogInvocation(
		"component-1",
		[]byte(`[{"name":"doThing","flowDeveloperName":"BoundedFlow","requiredPermissions":["DoThing"],"isAtomic":true}]`),
		"doThing",
	)
	if err != nil {
		t.Fatal(err)
	}
	_, outcome, reject, err := invocation.Prepare(
		context.Background(),
		catalogInvocationPermissionChecker{permissions: map[string]bool{"DoThing": true}},
		nil,
		map[string]interface{}{"undeclared": "caller-controlled"},
		&models.UserSession{ID: "user-1"},
	)
	if err != nil || outcome != nil || reject == nil || reject.Code != RejectInvalidField {
		t.Fatalf(
			"zero-schema caller input = outcome %#v reject %#v error %v",
			outcome, reject, err,
		)
	}
}

func TestResolveCatalogInvocationRejectsAmbiguousOrMalformedAuthority(t *testing.T) {
	tests := []struct {
		name string
		raw  string
		ref  string
	}{
		{name: "null", raw: `null`, ref: "doThing"},
		{name: "trailing", raw: `[] {}`, ref: "doThing"},
		{
			name: "case insensitive duplicate",
			raw:  `[{"name":"doThing","flowDeveloperName":"One","requiredPermissions":["P"]},{"name":"DoThing","flowDeveloperName":"Two","requiredPermissions":["P"]}]`,
			ref:  "doThing",
		},
		{
			name: "missing permission gate",
			raw:  `[{"name":"doThing","flowDeveloperName":"One"}]`,
			ref:  "doThing",
		},
		{
			name: "noncanonical action",
			raw:  `[{"name":" doThing","flowDeveloperName":"One","requiredPermissions":["P"]}]`,
			ref:  "doThing",
		},
		{
			name: "missing exact action",
			raw:  `[{"name":"other","flowDeveloperName":"One","requiredPermissions":["P"]}]`,
			ref:  "doThing",
		},
		{
			name: "noncanonical action reference",
			raw:  `[{"name":"doThing","flowDeveloperName":"One","requiredPermissions":["P"]}]`,
			ref:  " doThing",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if _, err := ResolveCatalogInvocation(
				"component-1", []byte(test.raw), test.ref,
			); err == nil {
				t.Fatal("malformed Catalog action authority was accepted")
			}
		})
	}
}

func TestResolveCatalogInvocationClassifiesAbsentAction(t *testing.T) {
	_, err := ResolveCatalogInvocation(
		"component-1",
		[]byte(`[{"name":"other","flowDeveloperName":"One","requiredPermissions":["P"]}]`),
		"doThing",
	)
	var missing *CatalogActionNotFoundError
	if !errors.As(err, &missing) || missing.ActionRef != "doThing" {
		t.Fatalf("absent action error = %#v, want typed not found", err)
	}
}

func TestCatalogInvocationPreservesNonAtomicDeclaration(t *testing.T) {
	invocation, err := ResolveCatalogInvocation(
		"component-1",
		[]byte(`[{"name":"externalEffect","flowDeveloperName":"ExternalEffectFlow","requiredPermissions":["Send"],"isAtomic":false}]`),
		"externalEffect",
	)
	if err != nil {
		t.Fatal(err)
	}
	if invocation.Atomic() {
		t.Fatal("non-atomic Catalog action was promoted to the bounded profile")
	}
}

func TestCatalogInvocationPreviewsUseImmutableSchemaAndFlowBindings(t *testing.T) {
	invocation, err := ResolveCatalogInvocation(
		"component-1",
		[]byte(`[{"name":"convert","flowDeveloperName":"ConvertFlow","previewFlowDeveloperName":"ConvertPreview","requiredPermissions":["Convert"],"isAtomic":true,"inputs":[{"apiName":"leadId","dataType":"id","isRequired":true,"uiMode":"hidden","targetObject":"Lead","targetFieldAPIName":"Id"}],"outputs":[{"apiName":"campaignFound","dataType":"boolean"}]}]`),
		"convert",
	)
	if err != nil {
		t.Fatal(err)
	}
	if invocation.PreviewDefinitionRef() != "ConvertPreview" {
		t.Fatalf("preview Flow = %q", invocation.PreviewDefinitionRef())
	}
	permissions := catalogInvocationPermissionChecker{
		permissions: map[string]bool{"Convert": true},
	}
	user := &models.UserSession{ID: "user-1"}
	forgedRun := false
	forged, outcome, err := invocation.PreviewCatalogRecordCreate(
		context.Background(),
		permissions,
		catalogInvocationMetadata{},
		permissions,
		map[string]interface{}{
			"leadId": "00Q-1", "campaignFound": true,
		},
		user,
		func(context.Context, string, *models.UserSession) (bool, error) {
			return true, nil
		},
		func(context.Context, string, map[string]interface{}, *models.UserSession) (*automation.FlowRecordCreatePreviewResult, error) {
			forgedRun = true
			return &automation.FlowRecordCreatePreviewResult{IsSuccess: true}, nil
		},
	)
	if err != nil || outcome != nil || forged == nil || forged.IsSuccess || forgedRun {
		t.Fatalf(
			"output field accepted as preview input: result %#v outcome %#v run=%v error %v",
			forged, outcome, forgedRun, err,
		)
	}
	create, outcome, err := invocation.PreviewCatalogRecordCreate(
		context.Background(),
		permissions,
		catalogInvocationMetadata{},
		permissions,
		map[string]interface{}{"leadId": "00Q-1"},
		user,
		func(_ context.Context, flowName string, _ *models.UserSession) (bool, error) {
			return flowName == "ConvertFlow", nil
		},
		func(ctx context.Context, flowName string, inputs map[string]interface{}, _ *models.UserSession) (*automation.FlowRecordCreatePreviewResult, error) {
			if flowName != "ConvertFlow" || inputs["leadId"] != "00Q-1" ||
				!constants.StandardActionExecutionOwnsRootFlow(ctx, "ConvertFlow") {
				t.Fatalf("create preview authority = flow %q inputs %#v", flowName, inputs)
			}
			return &automation.FlowRecordCreatePreviewResult{IsSuccess: true}, nil
		},
	)
	if err != nil || outcome != nil || create == nil || !create.IsSuccess {
		t.Fatalf("create preview = %#v outcome %#v error %v", create, outcome, err)
	}

	emptyRun := false
	empty, outcome, err := invocation.PreviewCatalogOutputs(
		context.Background(), permissions, "", user,
		func(context.Context, string, map[string]interface{}, *models.UserSession) (*automation.FlowPreviewOutputsResult, error) {
			emptyRun = true
			return &automation.FlowPreviewOutputsResult{}, nil
		},
	)
	var emptyError *Error
	if empty != nil || outcome != nil || !errors.As(err, &emptyError) ||
		emptyError.Kind != ErrorInvalidRequest || emptyError.Code != RejectInvalidRequest ||
		emptyRun {
		t.Fatalf(
			"empty recordId = result %#v outcome %#v run=%v error %#v",
			empty, outcome, emptyRun, err,
		)
	}

	outputs, outcome, err := invocation.PreviewCatalogOutputs(
		context.Background(),
		permissions,
		"00Q-1",
		user,
		func(ctx context.Context, flowName string, inputs map[string]interface{}, _ *models.UserSession) (*automation.FlowPreviewOutputsResult, error) {
			if flowName != "ConvertPreview" || inputs["leadId"] != "00Q-1" ||
				!constants.StandardActionExecutionOwnsRootFlow(ctx, "ConvertPreview") {
				t.Fatalf("output preview authority = flow %q inputs %#v", flowName, inputs)
			}
			return &automation.FlowPreviewOutputsResult{
				IsSuccess:    true,
				OutputValues: map[string]interface{}{"campaignFound": true},
			}, nil
		},
	)
	if err != nil || outcome != nil || outputs == nil || !outputs.IsSuccess ||
		outputs.OutputValues["campaignFound"] != true {
		t.Fatalf("output preview = %#v outcome %#v error %v", outputs, outcome, err)
	}
}
