package packs

import (
	"context"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/nexuscrm/automation"
	"github.com/nexuscrm/kernel/pkg/flowactions"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestFlowActionCallHandlerKind_AppPackAcceptedWhenRuntimeRegistrationMatches(t *testing.T) {
	root := writePackFixture(t, "jira-like-sample", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "sendemail",
			"manageability": "locked",
			"payload": {
				"actionType":      "sendEmail",
				"handlerRef":      "platform:automation/send-email",
				"atomicPolicy":    "reject",
				"previewPolicy":   "reject_email_io",
				"requiresContext": "none",
				"sendsEmail":      true
			}
		}
	]`, ``))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	if report.HasErrors() {
		t.Fatalf("ValidateInstallablePackSourceWithOptions diagnostics:\n%s", report.Error())
	}
	if report.Pack == nil {
		t.Fatal("report.Pack = nil, want constructed source-backed pack")
	}
}

func TestFlowActionCallHandlerKind_AppPackAcceptedWhenExternalHandlerRegistered(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)

	flowactions.MustRegister(flowactions.Registration{
		ActionType:    "resolveissueexternal",
		HandlerRef:    "host:workflow/resolve-issue-external",
		AtomicPolicy:  flowactions.AtomicPolicySafe,
		PreviewPolicy: flowactions.PreviewPolicyReject,
		Handler: func(context.Context, flowactions.Invocation) (string, error) {
			return "", nil
		},
	})

	root := writePackFixture(t, "jira-like-probe", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "resolveissueexternal",
			"manageability": "locked",
			"payload": {
				"actionType":      "resolveIssueExternal",
				"handlerRef":      "host:workflow/resolve-issue-external",
				"atomicPolicy":    "safe",
				"previewPolicy":   "reject",
				"requiresContext": "none"
			}
		}
	]`, ``))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	if report.HasErrors() {
		t.Fatalf("ValidateInstallablePackSourceWithOptions diagnostics:\n%s", report.Error())
	}
	if report.Pack == nil {
		t.Fatal("report.Pack = nil, want constructed source-backed pack")
	}
}

func TestFlowActionCallHandlerKind_ConnectorPushContractAcceptedFromDefaultPackOptions(t *testing.T) {
	root := writePackFixture(t, "connector-host-probe", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "pushtoconnector",
			"manageability": "locked",
			"payload": {
				"actionType":      "PushToConnector",
				"handlerRef":      "platform:connector/push-to-connector",
				"atomicPolicy":    "reject",
				"previewPolicy":   "reject",
				"requiresContext": "none"
			}
		}
	]`, ``))

	report := ValidateInstallablePackSource("local_path:" + root)
	if report.HasErrors() {
		t.Fatalf("ValidateInstallablePackSource diagnostics:\n%s", report.Error())
	}
	if report.Pack == nil {
		t.Fatal("report.Pack = nil, want constructed source-backed pack")
	}
}

func TestFlowActionCallHandlerKind_CRMHandlerAcceptedFromDefaultHandlerImplementation(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)
	registerTestFlowActionCallHandlerImplementationsForTest(t)

	root := writePackFixture(t, "crm-transitional-probe", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "stampconvertedanchors",
			"manageability": "locked",
			"payload": {
				"actionType":      "stampConvertedAnchors",
				"handlerRef":      "crm:standard-action/stamp-converted-anchors",
				"atomicPolicy":    "safe",
				"previewPolicy":   "reject",
				"requiresContext": "standard_action_protected"
			}
		}
	]`, ``))

	report := ValidateInstallablePackSource("local_path:" + root)
	if report.HasErrors() {
		t.Fatalf("ValidateInstallablePackSource diagnostics:\n%s", report.Error())
	}
	if report.Pack == nil {
		t.Fatal("report.Pack = nil, want constructed source-backed pack")
	}
}

func TestFlowActionCallHandlerKind_DefaultRegistrationFromSameDeclarationIsIdempotent(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)
	registerTestFlowActionCallHandlerImplementationsForTest(t)
	descriptor := automation.FlowActionCallRegistrationDescriptor{
		ActionType:      "stampConvertedAnchors",
		HandlerRef:      "crm:standard-action/stamp-converted-anchors",
		AtomicPolicy:    "safe",
		PreviewPolicy:   "reject",
		RequiresContext: "standard_action_protected",
	}
	registrations, err := automation.FlowActionCallRegistrationsFromDescriptors(
		[]automation.FlowActionCallRegistrationDescriptor{descriptor},
		automation.DefaultAppFlowActionCallHandlerImplementations()...,
	)
	if err != nil {
		t.Fatalf("FlowActionCallRegistrationsFromDescriptors: %v", err)
	}
	automation.RegisterDefaultAppFlowActionCallRegistrations(registrations...)

	root := writePackFixture(t, "crm-transitional-probe", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "stampconvertedanchors",
			"manageability": "locked",
			"payload": {
				"actionType":      "stampConvertedAnchors",
				"handlerRef":      "crm:standard-action/stamp-converted-anchors",
				"atomicPolicy":    "safe",
				"previewPolicy":   "reject",
				"requiresContext": "standard_action_protected"
			}
		}
	]`, ``))

	report := ValidateInstallablePackSource("local_path:" + root)
	if report.HasErrors() {
		t.Fatalf("ValidateInstallablePackSource diagnostics:\n%s", report.Error())
	}
	if report.Pack == nil {
		t.Fatal("report.Pack = nil, want constructed source-backed pack")
	}
	count := 0
	for _, registration := range report.Pack.flowActionCallRegistrations {
		if registration.Descriptor().ActionType == "stampconvertedanchors" {
			count++
		}
	}
	if count != 1 {
		t.Fatalf("stampConvertedAnchors registrations = %d, want 1", count)
	}
}

func TestFlowActionCallHandlerKind_ConflictingDefaultRegistrationStillFails(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)
	registerTestFlowActionCallHandlerImplementationsForTest(t)
	automation.RegisterDefaultAppFlowActionCallRegistrations(automation.FlowActionCallRegistration{
		ActionType:   "stampConvertedAnchors",
		HandlerRef:   "crm:standard-action/different",
		AtomicPolicy: automation.FlowActionCallAtomicPolicySafe,
		Handler: func(context.Context, *automation.FlowExecutor, *models.FlowInterview, *models.FlowMetadata, *models.FlowActionCall, *models.UserSession) (string, error) {
			return "", nil
		},
	})

	root := writePackFixture(t, "crm-transitional-probe", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "stampconvertedanchors",
			"manageability": "locked",
			"payload": {
				"actionType":      "stampConvertedAnchors",
				"handlerRef":      "crm:standard-action/stamp-converted-anchors",
				"atomicPolicy":    "safe",
				"previewPolicy":   "reject",
				"requiresContext": "standard_action_protected"
			}
		}
	]`, ``))

	report := ValidateInstallablePackSource("local_path:" + root)
	if !report.HasErrors() {
		t.Fatal("ValidateInstallablePackSource accepted conflicting default flow action registration")
	}
	if !strings.Contains(report.Error(), "conflicting flow action-call registration") {
		t.Fatalf("diagnostics:\n%s\nwant conflicting registration", report.Error())
	}
}

func TestFlowActionCallHandlerKind_EmailCompatibilityAliasesAcceptedFromDefaultHandlerImplementation(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)
	registerTestFlowActionCallHandlerImplementationsForTest(t)

	root := writePackFixture(t, "crm-transitional-email-probe", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "emailsimple",
			"manageability": "locked",
			"payload": {
				"actionType":      "emailSimple",
				"handlerRef":      "crm:automation/email-simple",
				"atomicPolicy":    "reject",
				"previewPolicy":   "reject_email_io",
				"requiresContext": "none",
				"sendsEmail":      true
			}
		},
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "emailalert",
			"manageability": "locked",
			"payload": {
				"actionType":      "emailAlert",
				"handlerRef":      "crm:automation/email-alert-disabled",
				"atomicPolicy":    "reject",
				"previewPolicy":   "invoke_handler",
				"requiresContext": "none"
			}
		}
	]`, ``))

	report := ValidateInstallablePackSource("local_path:" + root)
	if report.HasErrors() {
		t.Fatalf("ValidateInstallablePackSource diagnostics:\n%s", report.Error())
	}
	if report.Pack == nil {
		t.Fatal("report.Pack = nil, want constructed source-backed pack")
	}
}

func TestFlowActionCallHandlerKind_EmailCompatibilityAliasesAcceptedFromPackBackedHandlerImplementation(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)

	root := writePackFixture(t, "crm-transitional-email-probe", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "emailsimple",
			"manageability": "locked",
			"payload": {
				"actionType":      "emailSimple",
				"handlerRef":      "crm:automation/email-simple",
				"atomicPolicy":    "reject",
				"previewPolicy":   "reject_email_io",
				"requiresContext": "none",
				"sendsEmail":      true
			}
		}
	]`, ``))

	opts := testPackSourceOptions(t)
	opts.FlowActionCallHandlerImplementations = []automation.FlowActionCallHandlerImplementation{
		automation.LegacySendEmailAliasFlowActionCallHandlerImplementation("crm:automation/email-simple"),
	}

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, opts)
	if report.HasErrors() {
		t.Fatalf("ValidateInstallablePackSourceWithOptions diagnostics:\n%s", report.Error())
	}
	if report.Pack == nil {
		t.Fatal("report.Pack = nil, want constructed source-backed pack")
	}
	sourceBacked := report.Pack
	if len(sourceBacked.flowActionCallRegistrations) == 0 {
		t.Fatal("source-backed pack missing flow actionCall registrations resolved from handler declarations")
	}
}

func TestFlowActionCallHandlerLiveProjectionUsesDefaultAppRegistration(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)
	registerTestFlowActionCallHandlerImplementationsForTest(t)

	root := writePackFixture(t, "crm-transitional-probe", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "stampconvertedanchors",
			"manageability": "locked",
			"payload": {
				"actionType":      "stampConvertedAnchors",
				"handlerRef":      "crm:standard-action/stamp-converted-anchors",
				"atomicPolicy":    "safe",
				"previewPolicy":   "reject",
				"requiresContext": "standard_action_protected"
			}
		}
	]`, ``))

	report := ValidateInstallablePackSource("local_path:" + root)
	if report.HasErrors() {
		t.Fatalf("ValidateInstallablePackSource diagnostics:\n%s", report.Error())
	}
	sourceBacked := report.Pack
	if sourceBacked == nil {
		t.Fatal("report.Pack = nil, want constructed source-backed pack")
	}

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("SELECT OwnerPack, ActionType, HandlerRef, AtomicPolicy, PreviewPolicy, RequiresContext, COALESCE\\(SendsEmail, 0\\)\\s+FROM _System_FlowActionCallHandler").
		WithArgs("stampconvertedanchors").
		WillReturnRows(sqlmock.NewRows([]string{
			"OwnerPack", "ActionType", "HandlerRef", "AtomicPolicy", "PreviewPolicy", "RequiresContext", "SendsEmail",
		}).AddRow("jira-like-sample", "stampConvertedAnchors", "crm:standard-action/stamp-converted-anchors", "safe", "reject", "standard_action_protected", false))

	var component PackComponent
	for _, candidate := range sourceBacked.Components() {
		if candidate.Kind() == ComponentKindFlowActionCallHandler {
			component = candidate
			break
		}
	}
	if component == nil {
		t.Fatal("source-backed pack missing flow_action_call_handler component")
	}

	live, err := component.LiveProjection(context.Background(), VerifyDeps{DB: db})
	if err != nil {
		t.Fatalf("LiveProjection: %v", err)
	}
	got, ok := live.(sourceBackedFlowActionCallHandlerProjection)
	if !ok {
		t.Fatalf("LiveProjection type = %T, want sourceBackedFlowActionCallHandlerProjection", live)
	}
	if got.ActionType != "stampconvertedanchors" {
		t.Fatalf("LiveProjection actionType = %q, want stampconvertedanchors", got.ActionType)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestResolveInstalledPackSeesDefaultAppFlowActionRegistrations(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)
	registerTestFlowActionCallHandlerImplementationsForTest(t)

	root := writePackFixture(t, "crm-transitional-probe", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "stampconvertedanchors",
			"manageability": "locked",
			"payload": {
				"actionType":      "stampConvertedAnchors",
				"handlerRef":      "crm:standard-action/stamp-converted-anchors",
				"atomicPolicy":    "safe",
				"previewPolicy":   "reject",
				"requiresContext": "standard_action_protected"
			}
		}
	]`, ``))

	pack, err := ResolveInstalledPack(InstalledPackResolverInput{
		Name:                  "jira-like-sample",
		Category:              AppPack,
		ManifestFormatVersion: SupportedManifestFormatVersion,
		SourceKind:            string(PackSourceKindLocalPath),
		SourceURI:             root,
	}, InstalledPackResolverOptions{})
	if err != nil {
		t.Fatalf("ResolveInstalledPack: %v", err)
	}
	sourceBacked, ok := pack.(*SourceBackedPack)
	if !ok {
		t.Fatalf("ResolveInstalledPack type = %T, want *SourceBackedPack", pack)
	}
	if len(sourceBacked.flowActionCallRegistrations) == 0 {
		t.Fatal("resolved source-backed pack missing app-owned flow actionCall registrations")
	}
}

func registerTestFlowActionCallHandlerImplementationsForTest(t *testing.T) {
	t.Helper()
	restore := automation.ResetDefaultAppFlowActionCallRegistrationsForTest()
	t.Cleanup(restore)
	restoreHandlers := automation.ResetDefaultAppFlowActionCallHandlerImplementationsForTest()
	t.Cleanup(restoreHandlers)
	automation.RegisterDefaultAppFlowActionCallHandlerImplementations(testCRMFlowActionCallHandlerImplementations()...)
}

func testCRMFlowActionCallHandlerImplementations() []automation.FlowActionCallHandlerImplementation {
	return []automation.FlowActionCallHandlerImplementation{
		automation.LegacySendEmailAliasFlowActionCallHandlerImplementation("crm:automation/email-simple"),
		automation.DisabledWorkflowAlertFlowActionCallHandlerImplementation("crm:automation/email-alert-disabled"),
		{
			HandlerRef: "crm:standard-action/send-record-email",
			Build: func(descriptor automation.FlowActionCallRegistrationDescriptor) (automation.FlowActionCallRegistration, error) {
				return automation.FlowActionCallRegistration{
					ActionType:      descriptor.ActionType,
					HandlerRef:      descriptor.HandlerRef,
					AtomicPolicy:    automation.FlowActionCallAtomicPolicyReject,
					PreviewPolicy:   automation.FlowActionCallPreviewPolicyRejectEmailIO,
					RequiresContext: automation.FlowActionCallContextStandardActionProtected,
					SendsEmail:      true,
					Handler: func(context.Context, *automation.FlowExecutor, *models.FlowInterview, *models.FlowMetadata, *models.FlowActionCall, *models.UserSession) (string, error) {
						return "", nil
					},
				}, nil
			},
		},
		{
			HandlerRef: "crm:standard-action/stamp-converted-anchors",
			Build: func(descriptor automation.FlowActionCallRegistrationDescriptor) (automation.FlowActionCallRegistration, error) {
				return automation.FlowActionCallRegistration{
					ActionType:      descriptor.ActionType,
					HandlerRef:      descriptor.HandlerRef,
					AtomicPolicy:    automation.FlowActionCallAtomicPolicySafe,
					RequiresContext: automation.FlowActionCallContextStandardActionProtected,
					Handler: func(context.Context, *automation.FlowExecutor, *models.FlowInterview, *models.FlowMetadata, *models.FlowActionCall, *models.UserSession) (string, error) {
						return "", nil
					},
				}, nil
			},
		},
		{
			HandlerRef: "crm:standard-action/change-owner",
			Build: func(descriptor automation.FlowActionCallRegistrationDescriptor) (automation.FlowActionCallRegistration, error) {
				return automation.FlowActionCallRegistration{
					ActionType:      descriptor.ActionType,
					HandlerRef:      descriptor.HandlerRef,
					AtomicPolicy:    automation.FlowActionCallAtomicPolicyReject,
					RequiresContext: automation.FlowActionCallContextStandardActionProtected,
					Handler: func(context.Context, *automation.FlowExecutor, *models.FlowInterview, *models.FlowMetadata, *models.FlowActionCall, *models.UserSession) (string, error) {
						return "", nil
					},
				}, nil
			},
		},
		{
			HandlerRef: "platform:connector/recover-remote-identity",
			Build: func(descriptor automation.FlowActionCallRegistrationDescriptor) (automation.FlowActionCallRegistration, error) {
				return automation.FlowActionCallRegistration{
					ActionType:      descriptor.ActionType,
					HandlerRef:      descriptor.HandlerRef,
					AtomicPolicy:    automation.FlowActionCallAtomicPolicyReject,
					PreviewPolicy:   automation.FlowActionCallPreviewPolicyReject,
					RequiresContext: automation.FlowActionCallContextNone,
					Handler: func(context.Context, *automation.FlowExecutor, *models.FlowInterview, *models.FlowMetadata, *models.FlowActionCall, *models.UserSession) (string, error) {
						return "", nil
					},
				}, nil
			},
		},
	}
}

func TestFlowActionCallHandlerKind_RejectsUnregisteredRuntimeAction(t *testing.T) {
	root := writePackFixture(t, "jira-like-probe", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "resolveissue",
			"manageability": "locked",
			"payload": {
				"actionType":      "resolveIssue",
				"handlerRef":      "host:workflow/resolve-issue",
				"atomicPolicy":    "safe",
				"previewPolicy":   "reject",
				"requiresContext": "none"
			}
		}
	]`, ``))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	if !report.HasErrors() {
		t.Fatal("ValidateInstallablePackSourceWithOptions accepted unregistered flow actionCall handler declaration")
	}
	assertInstallableDiagnostic(t, report, "component.payload.invalid_schema")
	if !strings.Contains(report.Error(), "has no runtime registration") {
		t.Fatalf("diagnostics =\n%s\nwant missing runtime registration", report.Error())
	}
}

func TestFlowActionCallHandlerKind_RejectsRuntimePolicyDrift(t *testing.T) {
	root := writePackFixture(t, "jira-like-sample", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "sendemail",
			"manageability": "locked",
			"payload": {
				"actionType":      "sendEmail",
				"handlerRef":      "platform:automation/send-email",
				"atomicPolicy":    "safe",
				"previewPolicy":   "reject_email_io",
				"requiresContext": "none",
				"sendsEmail":      true
			}
		}
	]`, ``))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	if !report.HasErrors() {
		t.Fatal("ValidateInstallablePackSourceWithOptions accepted flow actionCall handler policy drift")
	}
	assertInstallableDiagnostic(t, report, "component.payload.invalid_schema")
	if !strings.Contains(report.Error(), "atomicPolicy") {
		t.Fatalf("diagnostics =\n%s\nwant atomicPolicy mismatch", report.Error())
	}
}

func TestFlowActionCallHandlerKind_ConnectorPackRejected(t *testing.T) {
	root := writePackFixture(t, "test-connector", validConnectorPackManifest("test-connector", `[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "parsewebhook",
			"manageability": "locked",
			"payload": {
				"actionType":   "parseWebhook",
				"handlerRef":   "host:connector/parse-webhook",
				"atomicPolicy": "reject"
			}
		}
	]`))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	assertInstallableDiagnostic(t, report, "manifest.component.unsupported_for_category")
}

func TestFlowActionCallHandlerValidatePackRejectsInvalidPolicy(t *testing.T) {
	root := writePackFixture(t, "jira-like-probe", validPackManifest(`[
		{
			"kind":          "flow_action_call_handler",
			"businessKey":   "resolveissue",
			"manageability": "locked",
			"payload": {
				"actionType":   "resolveIssue",
				"handlerRef":   "host:workflow/resolve-issue",
				"atomicPolicy": "sometimes"
			}
		}
	]`, ``))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	assertInstallableDiagnostic(t, report, "component.payload.invalid_schema")
}

func TestNewSourceBackedFlowActionCallHandlerComponentNormalizesProjection(t *testing.T) {
	component, err := newSourceBackedFlowActionCallHandlerComponent("jira-like-probe", ManifestComponent{
		Kind:          ComponentKindFlowActionCallHandler,
		BusinessKey:   "resolveissue",
		Manageability: ManageabilityLocked,
	}, []byte(`{
		"actionType":      " ResolveIssue ",
		"handlerRef":      " host:workflow/resolve-issue ",
		"atomicPolicy":    " safe ",
		"previewPolicy":   " reject ",
		"requiresContext": " none ",
		"sendsEmail":      true
	}`))
	if err != nil {
		t.Fatalf("newSourceBackedFlowActionCallHandlerComponent: %v", err)
	}
	projection, err := component.ExpectedProjection()
	if err != nil {
		t.Fatalf("ExpectedProjection: %v", err)
	}
	got, ok := projection.(sourceBackedFlowActionCallHandlerProjection)
	if !ok {
		t.Fatalf("ExpectedProjection type = %T, want sourceBackedFlowActionCallHandlerProjection", projection)
	}
	if got.OwnerPack != "jira-like-probe" {
		t.Fatalf("OwnerPack = %q, want jira-like-probe", got.OwnerPack)
	}
	if got.ActionType != "resolveissue" {
		t.Fatalf("ActionType = %q, want normalized resolveissue", got.ActionType)
	}
	if got.HandlerRef != "host:workflow/resolve-issue" {
		t.Fatalf("HandlerRef = %q, want trimmed handler ref", got.HandlerRef)
	}
	if got.AtomicPolicy != "safe" || got.PreviewPolicy != "reject" || got.RequiresContext != "none" {
		t.Fatalf("policies = %#v, want normalized policy strings", got)
	}
	if !got.SendsEmail {
		t.Fatal("SendsEmail = false, want true")
	}
}

func TestNewSourceBackedFlowActionCallHandlerComponentUsesActionTypeBusinessKey(t *testing.T) {
	component, err := newSourceBackedFlowActionCallHandlerComponent("jira-like-probe", ManifestComponent{
		Kind:          ComponentKindFlowActionCallHandler,
		BusinessKey:   "resolveissue",
		Manageability: ManageabilityLocked,
	}, []byte(`{
		"actionType":      " ResolveIssue ",
		"handlerRef":      " host:workflow/resolve-issue ",
		"atomicPolicy":    " safe ",
		"previewPolicy":   " reject ",
		"requiresContext": " none "
	}`))
	if err != nil {
		t.Fatalf("newSourceBackedFlowActionCallHandlerComponent: %v", err)
	}
	if got := component.BusinessKey(); got != "resolveissue" {
		t.Fatalf("BusinessKey = %q, want global actionType key resolveissue", got)
	}
}

func TestNewSourceBackedFlowActionCallHandlerComponentRejectsNonCanonicalBusinessKeyActionType(t *testing.T) {
	_, err := newSourceBackedFlowActionCallHandlerComponent("jira-like-probe", ManifestComponent{
		Kind:          ComponentKindFlowActionCallHandler,
		BusinessKey:   "ResolveIssue",
		Manageability: ManageabilityLocked,
	}, []byte(`{
		"actionType":   "ResolveIssue",
		"handlerRef":   "host:workflow/resolve-issue",
		"atomicPolicy": "safe"
	}`))
	if err == nil {
		t.Fatal("newSourceBackedFlowActionCallHandlerComponent succeeded, want canonical businessKey rejection")
	}
	if !strings.Contains(err.Error(), "must be canonical") {
		t.Fatalf("error = %v, want canonical businessKey diagnostic", err)
	}
}

func TestFlowActionCallHandlerRuntimeDeclarationIncludesProjection(t *testing.T) {
	pack := testPersistedRuntimeSourceBackedPack(t, []ManifestComponent{{
		Kind:          ComponentKindFlowActionCallHandler,
		BusinessKey:   "sendemail",
		Manageability: ManageabilityLocked,
		Payload: []byte(`{
			"actionType":      "sendEmail",
			"handlerRef":      "platform:automation/send-email",
			"atomicPolicy":    "reject",
			"previewPolicy":   "reject_email_io",
			"requiresContext": "none",
			"sendsEmail":      true
		}`),
	}})
	decl, err := BuildPersistedRuntimeDeclaration(pack)
	if err != nil {
		t.Fatalf("BuildPersistedRuntimeDeclaration: %v", err)
	}
	rows, err := unmarshalRuntimeStartupShallowDeclaredRows(decl.StartupShallowDeclaredRowsJSON())
	if err != nil {
		t.Fatalf("unmarshalRuntimeStartupShallowDeclaredRows: %v", err)
	}
	if len(rows) != 1 {
		t.Fatalf("len(rows) = %d, want 1: %#v", len(rows), rows)
	}
	row := rows[0]
	if row.Kind != ComponentKindFlowActionCallHandler {
		t.Fatalf("row.Kind = %q, want %q", row.Kind, ComponentKindFlowActionCallHandler)
	}
	if row.BusinessKey != "sendemail" {
		t.Fatalf("row.BusinessKey = %q, want normalized action type key", row.BusinessKey)
	}
	if !strings.Contains(row.Projection, `"actionType":"sendemail"`) ||
		!strings.Contains(row.Projection, `"handlerRef":"platform:automation/send-email"`) {
		t.Fatalf("row.Projection = %s, want normalized declaration", row.Projection)
	}
}

func TestFlowActionCallHandlerApplyUpsertsProjection(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	component, err := newSourceBackedFlowActionCallHandlerComponent("jira-like-probe", ManifestComponent{
		Kind:          ComponentKindFlowActionCallHandler,
		BusinessKey:   "resolveissue",
		Manageability: ManageabilityLocked,
	}, []byte(`{
		"actionType":      "resolveIssue",
		"handlerRef":      "host:workflow/resolve-issue",
		"atomicPolicy":    "safe",
		"previewPolicy":   "reject",
		"requiresContext": "none",
		"sendsEmail":      true
	}`))
	if err != nil {
		t.Fatalf("newSourceBackedFlowActionCallHandlerComponent: %v", err)
	}

	mock.ExpectQuery("SELECT OwnerPack\\s+FROM _System_FlowActionCallHandler").
		WithArgs("resolveissue").
		WillReturnRows(sqlmock.NewRows([]string{"OwnerPack"}))
	mock.ExpectExec("INSERT INTO _System_FlowActionCallHandler").
		WithArgs(
			sqlmock.AnyArg(),
			"jira-like-probe",
			"resolveissue",
			"host:workflow/resolve-issue",
			"safe",
			"reject",
			"none",
			true,
		).
		WillReturnResult(sqlmock.NewResult(1, 1))

	if err := component.Apply(context.Background(), ApplyDeps{DB: db}); err != nil {
		t.Fatalf("Apply: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestFlowActionCallHandlerApplyRejectsDifferentOwnerForGlobalActionType(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("SELECT OwnerPack\\s+FROM _System_FlowActionCallHandler").
		WithArgs("resolveissue").
		WillReturnRows(sqlmock.NewRows([]string{"OwnerPack"}).AddRow("other-pack"))

	err = upsertSourceBackedFlowActionCallHandler(context.Background(), db, sourceBackedFlowActionCallHandlerProjection{
		OwnerPack:       "jira-like-probe",
		ActionType:      "resolveIssue",
		HandlerRef:      "host:workflow/resolve-issue",
		AtomicPolicy:    "safe",
		PreviewPolicy:   "reject",
		RequiresContext: "none",
	})
	if err == nil {
		t.Fatal("upsertSourceBackedFlowActionCallHandler succeeded, want owner collision")
	}
	if !strings.Contains(err.Error(), "already owned by pack other-pack") {
		t.Fatalf("error = %v, want global owner collision diagnostic", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestFlowActionCallHandlerLiveProjectionNormalizesRow(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("SELECT OwnerPack, ActionType, HandlerRef, AtomicPolicy, PreviewPolicy, RequiresContext, COALESCE\\(SendsEmail, 0\\)\\s+FROM _System_FlowActionCallHandler").
		WithArgs("resolveissue").
		WillReturnRows(sqlmock.NewRows([]string{
			"OwnerPack", "ActionType", "HandlerRef", "AtomicPolicy", "PreviewPolicy", "RequiresContext", "SendsEmail",
		}).AddRow("jira-like-probe", "ResolveIssue", " host:workflow/resolve-issue ", " SAFE ", " REJECT ", " NONE ", true))

	projection, err := liveSourceBackedFlowActionCallHandlerProjection(context.Background(), db, "jira-like-probe", "ResolveIssue")
	if err != nil {
		t.Fatalf("liveSourceBackedFlowActionCallHandlerProjection: %v", err)
	}
	if projection.ActionType != "resolveissue" ||
		projection.HandlerRef != "host:workflow/resolve-issue" ||
		projection.AtomicPolicy != "safe" ||
		projection.PreviewPolicy != "reject" ||
		projection.RequiresContext != "none" ||
		!projection.SendsEmail {
		t.Fatalf("projection = %#v, want normalized live row", projection)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestFlowActionCallHandlerLiveProjectionRejectsDuplicateGlobalActionType(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("SELECT OwnerPack, ActionType, HandlerRef, AtomicPolicy, PreviewPolicy, RequiresContext, COALESCE\\(SendsEmail, 0\\)\\s+FROM _System_FlowActionCallHandler").
		WithArgs("resolveissue").
		WillReturnRows(sqlmock.NewRows([]string{
			"OwnerPack", "ActionType", "HandlerRef", "AtomicPolicy", "PreviewPolicy", "RequiresContext", "SendsEmail",
		}).
			AddRow("jira-like-probe", "ResolveIssue", "host:workflow/resolve-issue", "safe", "reject", "none", false).
			AddRow("other-pack", "ResolveIssue", "host:workflow/resolve-issue", "safe", "reject", "none", false))

	_, err = liveSourceBackedFlowActionCallHandlerProjection(context.Background(), db, "jira-like-probe", "ResolveIssue")
	if err == nil {
		t.Fatal("liveSourceBackedFlowActionCallHandlerProjection succeeded, want duplicate action type diagnostic")
	}
	if !strings.Contains(err.Error(), "duplicate live rows") {
		t.Fatalf("error = %v, want duplicate live rows diagnostic", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestFlowActionCallHandlerComponentLiveProjectionRejectsRuntimeRegistryDrift(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	component, err := newSourceBackedFlowActionCallHandlerComponent("crm-pack", ManifestComponent{
		Kind:          ComponentKindFlowActionCallHandler,
		BusinessKey:   "sendemail",
		Manageability: ManageabilityLocked,
	}, []byte(`{
		"actionType":      "sendEmail",
		"handlerRef":      "platform:automation/send-email",
		"atomicPolicy":    "reject",
		"previewPolicy":   "reject_email_io",
		"requiresContext": "none",
		"sendsEmail":      true
	}`))
	if err != nil {
		t.Fatalf("newSourceBackedFlowActionCallHandlerComponent: %v", err)
	}

	mock.ExpectQuery("SELECT OwnerPack, ActionType, HandlerRef, AtomicPolicy, PreviewPolicy, RequiresContext, COALESCE\\(SendsEmail, 0\\)\\s+FROM _System_FlowActionCallHandler").
		WithArgs("sendemail").
		WillReturnRows(sqlmock.NewRows([]string{
			"OwnerPack", "ActionType", "HandlerRef", "AtomicPolicy", "PreviewPolicy", "RequiresContext", "SendsEmail",
		}).AddRow("crm-pack", "sendEmail", "platform:automation/send-email", "safe", "reject_email_io", "none", true))

	_, err = component.LiveProjection(context.Background(), VerifyDeps{DB: db})
	if err == nil {
		t.Fatal("LiveProjection succeeded, want runtime registry drift diagnostic")
	}
	if !strings.Contains(err.Error(), "conflicts with runtime registration") {
		t.Fatalf("error = %v, want runtime registration drift diagnostic", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestFlowActionCallHandlerPackSweeperSoftDeletesOrphans(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	batch := &flowActionCallHandlerPackBatch{declaredActionTypes: []string{"resolveIssue"}}
	mock.ExpectQuery("SELECT Id, ActionType\\s+FROM _System_FlowActionCallHandler").
		WithArgs("jira-like-probe").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ActionType"}).
			AddRow("handler-keep", "resolveissue").
			AddRow("handler-orphan", "closeissue"))
	mock.ExpectExec("UPDATE _System_FlowActionCallHandler").
		WithArgs("handler-orphan").
		WillReturnResult(sqlmock.NewResult(0, 1))

	if err := applyFlowActionCallHandlerPackSweeper(context.Background(), db, batch, "jira-like-probe"); err != nil {
		t.Fatalf("applyFlowActionCallHandlerPackSweeper: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestFlowActionCallHandlerPackSweeperVerifyLabelsOrphanRowIDs(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	batch := &flowActionCallHandlerPackBatch{declaredActionTypes: []string{"resolveIssue"}}
	mock.ExpectQuery("SELECT Id, ActionType\\s+FROM _System_FlowActionCallHandler").
		WithArgs("jira-like-probe").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ActionType"}).
			AddRow("handler-keep", "resolveissue").
			AddRow("handler-orphan", "closeissue"))

	err = verifyFlowActionCallHandlerPackSweeper(context.Background(), db, batch, "jira-like-probe")
	if err == nil {
		t.Fatal("verifyFlowActionCallHandlerPackSweeper succeeded, want orphan diagnostic")
	}
	if !strings.Contains(err.Error(), "row ID(s)") || !strings.Contains(err.Error(), "handler-orphan") {
		t.Fatalf("error = %v, want row ID diagnostic with orphan ID", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}
