// White-box (package packs) test that proves the ENGINE can consume an
// EXTERNAL-STYLE (contract-only) build handler for ONE presentation kind
// (search_scope_order) and reconstruct a BYTE-IDENTICAL component, gated by the
// same four boundary surfaces the black-box golden oracle freezes.
//
// The "fake leaf provider" (fakeSearchScopeOrderHandler) mimics the real
// nexuscrm-app-packs provider.go:buildSearchScopeOrder WITHOUT importing that
// module or touching go.work: it parses req.Payload into []cp.SearchScopeOrderItem
// and hands it to the engine sink ctx.AddSearchScopeOrderComponent. We then build
// the SF pack TWICE — once on the internal dispatch (control) and once with
// search_scope_order routed through the fake handler — and assert all four
// surfaces are identical. Identity proves the typed declaration is loss-free for
// this kind; any drift would be the make-or-break finding.
package packs

import (
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"testing"

	featureauth "github.com/nexuscrm/auth"
	"github.com/nexuscrm/automation"
	"github.com/nexuscrm/shared/pkg/models"
	cp "github.com/pingcap-inc/nexuscrm-contract/pkg/controlplane"
)

// fakeSearchScopeOrderHandlerFires counts invocations so the test can prove the
// external handler genuinely fired (guards against a false-green where the route
// silently did not engage and the internal path produced the identical output).
var fakeSearchScopeOrderHandlerFires int

// fakeSearchScopeOrderHandler is the inline contract-only stand-in for the real
// provider handler. It depends ONLY on the cp contract package, exactly as an
// external provider would.
func fakeSearchScopeOrderHandler(ctx cp.ComponentBuildContext, req cp.ComponentBuildRequest) error {
	pctx, ok := ctx.(cp.PresentationComponentBuildContext)
	if !ok {
		return errNotPresentationContext
	}
	var items []cp.SearchScopeOrderItem
	if err := json.Unmarshal(req.Payload, &items); err != nil {
		return err
	}
	fakeSearchScopeOrderHandlerFires++
	return pctx.AddSearchScopeOrderComponent(req, items)
}

var errNotPresentationContext = errStr("engine build context does not implement cp.PresentationComponentBuildContext")

type errStr string

func (e errStr) Error() string { return string(e) }

// withExternalBuildRoutesForTest installs the external route map on the pack
// before buildComponents runs (NewSourceBackedPack applies options, then builds).
func withExternalBuildRoutesForTest(routes map[ComponentKind]externalBuildRoute) SourceBackedPackOption {
	return func(p *SourceBackedPack) {
		p.externalBuildRoutes = routes
	}
}

func TestExternalSearchScopeOrderRouteByteIdentical(t *testing.T) {
	root := officialAppPacksRootForRouteTest(t)
	if root == "" {
		t.Skip("no nexuscrm-app-packs checkout found; skipping external-route byte-identity test")
	}

	// Same host wiring the golden oracle uses (the SF pack requires it).
	// Snapshot + clear the global host-primitive registry and restore it on
	// cleanup, so the salesforce-v63 product-dialect registered below does not
	// leak into later tests (e.g. the app-blind rejection assertions in
	// pack_source_test.go) when the full package suite runs.
	resetDefaultHostPrimitivesForTest(t)
	restoreIssuer := featureauth.SetProductOAuthSessionIssuerClassOwners([]featureauth.ProductOAuthSessionIssuerClassOwner{{
		Class:          "salesforce_compat",
		OwningAppPacks: []string{"salesforce-crm-standard"},
	}})
	t.Cleanup(restoreIssuer)
	RegisterDefaultHostPrimitives(
		HostPrimitive{Kind: "renderer", Name: "app-pack:crm/action-renderer/lead-convert"},
		HostPrimitive{Kind: "renderer", Name: "app-pack:crm/action-renderer/create-record-from-source"},
		HostPrimitive{Kind: "renderer", Name: "app-pack:crm/action-renderer/email-compose"},
		HostPrimitive{Kind: "renderer", Name: "app-pack:crm/record-panel/activity-timeline"},
		HostPrimitive{Kind: "renderer", Name: "app-pack:crm/record-panel/record-activity-feed"},
		HostPrimitive{Kind: "renderer", Name: "app-pack:crm/banner/converted-lead"},
		HostPrimitive{Kind: HostPrimitiveKindProductDialect, Name: "salesforce-v63"},
	)

	routes := newExternalBuildRoutes(
		[]cp.ComponentKindRegistration{{
			Contract:     cp.ComponentKindContract{Kind: cp.ComponentKindSearchScopeOrder},
			Capability:   cp.SourceBackedComponentCapability{Kind: cp.ComponentKindSearchScopeOrder, Installable: true},
			BuildHandler: fakeSearchScopeOrderHandler,
		}},
		map[ComponentKind]struct{}{ComponentKindSearchScopeOrder: {}},
	)
	if _, ok := routes[ComponentKindSearchScopeOrder]; !ok {
		t.Fatalf("expected search_scope_order to be routed externally; install gate dropped it")
	}

	// Control: internal dispatch (no routes). Routed: search_scope_order external.
	control := buildSFPackSurfacesForRouteTest(t, root, nil)
	fakeSearchScopeOrderHandlerFires = 0
	routed := buildSFPackSurfacesForRouteTest(t, root, routes)
	if fakeSearchScopeOrderHandlerFires == 0 {
		t.Fatalf("external search_scope_order handler never fired; route did not engage (the byte-identity below would be a false-green)")
	}

	if string(control.declaredComponents) != string(routed.declaredComponents) {
		t.Errorf("DRIFT surface 1 (DeclaredComponents): external route is lossy.\n%s",
			firstDiff(string(control.declaredComponents), string(routed.declaredComponents)))
	}
	if control.packPayloadHash != routed.packPayloadHash {
		t.Errorf("DRIFT surface 2 (PayloadHash): control=%s routed=%s", control.packPayloadHash, routed.packPayloadHash)
	}
	if control.runtimeShallow != routed.runtimeShallow {
		t.Errorf("DRIFT surface 3 (RuntimeStartupShallow): external route changed shallow rows.\n%s",
			firstDiff(control.runtimeShallow, routed.runtimeShallow))
	}
	if string(control.installPlan) != string(routed.installPlan) {
		t.Errorf("DRIFT surface 4 (InstallPlan): external route changed plan grouping.\n%s",
			firstDiff(string(control.installPlan), string(routed.installPlan)))
	}
}

// routeTestSurfaces mirrors the four golden-oracle surfaces.
type routeTestSurfaces struct {
	declaredComponents []byte
	packPayloadHash    string
	runtimeShallow     string
	installPlan        []byte
}

// buildSFPackSurfacesForRouteTest resolves the SF pack source and builds it via
// NewSourceBackedPack with the SAME options the production validation path
// applies, optionally adding the external-route option. Capturing the four
// surfaces the same way the golden oracle does keeps this an apples-to-apples
// byte-identity check.
func buildSFPackSurfacesForRouteTest(t *testing.T, root string, routes map[ComponentKind]externalBuildRoute) routeTestSurfaces {
	t.Helper()

	opts := DefaultPackSourceOptions()
	opts.OfficialRepoRoot = root
	opts.FlowActionCallHandlerImplementations = append(opts.FlowActionCallHandlerImplementations, crmFlowActionCallHandlersForRouteTest()...)

	sourceReport := ValidatePackSourceWithOptions("official_repo:salesforce-crm-standard", opts)
	if sourceReport.HasErrors() {
		t.Fatalf("resolve SF source: %s", sourceReport.Error())
	}
	source := sourceReport.Source
	if source == nil || source.Manifest == nil {
		t.Fatalf("nil SF source/manifest")
	}

	flowActionCallRegistrations, err := flowActionCallRegistrationsFromSource(source, opts.FlowActionCallHandlerImplementations)
	if err != nil {
		t.Fatalf("flow action registrations: %v", err)
	}

	packOpts := []SourceBackedPackOption{}
	if len(opts.FlowActionCallRegistrations) > 0 {
		packOpts = append(packOpts, withFlowActionCallRegistrations(opts.FlowActionCallRegistrations))
	}
	if len(flowActionCallRegistrations) > 0 {
		packOpts = append(packOpts, withFlowActionCallRegistrations(flowActionCallRegistrations))
	}
	if len(opts.FlowActionCallHandlerImplementations) > 0 {
		packOpts = append(packOpts, withFlowActionCallHandlerImplementations(opts.FlowActionCallHandlerImplementations))
	}
	packOpts = append(packOpts, withConnectorPushValidationSchemaResolver(connectorPushValidationSchemaResolverForSource(source, opts, false)))
	if routes != nil {
		packOpts = append(packOpts, withExternalBuildRoutesForTest(routes))
	}

	pack, err := NewSourceBackedPack(source, packOpts...)
	if err != nil {
		t.Fatalf("NewSourceBackedPack: %v", err)
	}

	declared, err := pack.DeclaredComponents()
	if err != nil {
		t.Fatalf("DeclaredComponents: %v", err)
	}
	declaredJSON, err := json.MarshalIndent(declared, "", "  ")
	if err != nil {
		t.Fatalf("marshal declared: %v", err)
	}
	declaredJSON = append(declaredJSON, '\n')

	packHash, err := pack.PayloadHash()
	if err != nil {
		t.Fatalf("PayloadHash: %v", err)
	}

	persisted, err := BuildPersistedRuntimeDeclaration(pack)
	if err != nil {
		t.Fatalf("BuildPersistedRuntimeDeclaration: %v", err)
	}
	runtimeShallow := persisted.StartupShallowDeclaredRowsJSON()

	plan, err := pack.InstallPlan()
	if err != nil {
		t.Fatalf("InstallPlan: %v", err)
	}
	groups := make([]struct {
		Phase string   `json:"phase"`
		Steps []string `json:"steps"`
	}, 0, len(plan.Phases()))
	for _, phase := range plan.Phases() {
		comps := plan.ComponentsForPhase(phase)
		steps := make([]string, 0, len(comps))
		for _, c := range comps {
			steps = append(steps, string(c.Kind())+":"+c.BusinessKey())
		}
		sort.Strings(steps)
		groups = append(groups, struct {
			Phase string   `json:"phase"`
			Steps []string `json:"steps"`
		}{Phase: string(phase), Steps: steps})
	}
	planJSON, err := json.MarshalIndent(groups, "", "  ")
	if err != nil {
		t.Fatalf("marshal plan: %v", err)
	}
	planJSON = append(planJSON, '\n')

	return routeTestSurfaces{
		declaredComponents: declaredJSON,
		packPayloadHash:    packHash,
		runtimeShallow:     runtimeShallow,
		installPlan:        planJSON,
	}
}

func crmFlowActionCallHandlersForRouteTest() []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 officialAppPacksRootForRouteTest(t *testing.T) string {
	t.Helper()
	if root := os.Getenv("NEXUSCRM_OFFICIAL_APP_PACKS_ROOT"); root != "" {
		if resolved, err := filepath.EvalSymlinks(root); err == nil {
			return resolved
		}
		return root
	}
	_, thisFile, _, ok := runtime.Caller(0)
	if !ok {
		return ""
	}
	pkgDir := filepath.Dir(thisFile)
	backendRoot := filepath.Clean(filepath.Join(pkgDir, "..", ".."))
	sibling := filepath.Join(filepath.Dir(backendRoot), "nexuscrm-app-packs")
	if info, err := os.Stat(sibling); err == nil && info.IsDir() {
		if resolved, err := filepath.EvalSymlinks(sibling); err == nil {
			return resolved
		}
		return sibling
	}
	return ""
}

func firstDiff(a, b string) string {
	n := len(a)
	if len(b) < n {
		n = len(b)
	}
	for i := 0; i < n; i++ {
		if a[i] != b[i] {
			lo := i - 40
			if lo < 0 {
				lo = 0
			}
			hiA := i + 40
			if hiA > len(a) {
				hiA = len(a)
			}
			hiB := i + 40
			if hiB > len(b) {
				hiB = len(b)
			}
			return "first diff at byte " + itoa(i) + "\ncontrol: ..." + a[lo:hiA] + "...\nrouted : ..." + b[lo:hiB] + "..."
		}
	}
	if len(a) != len(b) {
		return "common prefix identical; lengths differ control=" + itoa(len(a)) + " routed=" + itoa(len(b))
	}
	return "(no diff)"
}

func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	neg := n < 0
	if neg {
		n = -n
	}
	var buf [20]byte
	i := len(buf)
	for n > 0 {
		i--
		buf[i] = byte('0' + n%10)
		n /= 10
	}
	if neg {
		i--
		buf[i] = '-'
	}
	return string(buf[i:])
}
