// White-box (package packs) DEEP-COMPARE + MUTATION test for the SECOND engine
// slice of the SF-skin migration.
//
// Slice 1 proved the WIRING on search_scope_order, but that kind is near
// pass-through (its component stores the raw req.Payload, which the oracle's
// PayloadHash already hashes), so it could NOT prove RECONSTRUCTION FIDELITY: a
// Label / parsed-field drift in the declaration would slip past all four oracle
// surfaces.
//
// ui_action is CONTENT-DERIVED: its component now exposes ExpectedProjection() =
// the parsed []standardActionRow (Name/Label/Type/Icon/TargetObject/Config). This
// test builds the ui_action component BOTH ways — internal dispatch (control) and
// through the external cp handler (routed) — for the SAME SF manifest, and asserts
// FULL equality across the exported surface that INCLUDES parsed content:
// Kind() + BusinessKey() + Manageability() + DependsOn() + ExpectedPayload() +
// ExpectedProjection(). A fired-counter guards against a silent no-route
// false-green.
//
// The MUTATION test then corrupts ONE parsed Label in the external declaration
// and asserts the deep-compare FAILS — proving the test catches the exact
// content drift the prior slice's surface-only check missed.
package packs

import (
	"bytes"
	"encoding/json"
	"reflect"
	"testing"

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

// fakeUIActionHandlerFires counts external-handler invocations so the deep-compare
// cannot false-green on a route that silently failed to engage.
var fakeUIActionHandlerFires int

// fakeUIActionHandler is the inline contract-only stand-in for the real
// nexuscrm-app-packs provider.go:buildUIAction. It depends ONLY on the cp
// contract package, exactly as an external provider would: parse req.Payload into
// []cp.UIActionDeclaration and hand it to the engine sink.
func fakeUIActionHandler(ctx cp.ComponentBuildContext, req cp.ComponentBuildRequest) error {
	pctx, ok := ctx.(cp.PresentationComponentBuildContext)
	if !ok {
		return errNotPresentationContext
	}
	decls, err := uiActionDeclarationsFromPayloadForTest(req.Payload)
	if err != nil {
		return err
	}
	fakeUIActionHandlerFires++
	return pctx.AddUIActionComponent(req, decls)
}

// uiActionDeclarationsFromPayloadForTest reproduces the provider's
// uiActionDeclarationsFromPayload (fan-out appliesTo -> one decl per object,
// trim, dedupe, sort) WITHOUT importing the app-packs module, mirroring how
// fakeSearchScopeOrderHandler stands in for the real provider in slice 1.
func uiActionDeclarationsFromPayloadForTest(payload []byte) ([]cp.UIActionDeclaration, error) {
	actions, err := parseStandardActionPayload(payload, "ui_action test payload")
	if err != nil {
		return nil, err
	}
	out := make([]cp.UIActionDeclaration, 0)
	for _, action := range actions {
		var config []byte
		if len(action.Config) > 0 {
			raw, err := json.Marshal(action.Config)
			if err != nil {
				return nil, err
			}
			config = raw
		}
		for _, objectAPIName := range action.scopedObjectAPINames() {
			out = append(out, cp.UIActionDeclaration{
				ObjectAPIName: objectAPIName,
				Name:          action.Name,
				Label:         action.Label,
				Type:          action.Type,
				Icon:          action.Icon,
				TargetObject:  action.TargetObject,
				Config:        append([]byte(nil), config...),
			})
		}
	}
	return out, nil
}

// uiActionRoutes builds the external-route map that diverts ui_action through the
// supplied handler.
func uiActionRoutes(t *testing.T, handler cp.ComponentBuildHandler) map[ComponentKind]externalBuildRoute {
	t.Helper()
	routes := newExternalBuildRoutes(
		[]cp.ComponentKindRegistration{{
			Contract:     cp.ComponentKindContract{Kind: cp.ComponentKindUIAction},
			Capability:   cp.SourceBackedComponentCapability{Kind: cp.ComponentKindUIAction, Installable: true},
			BuildHandler: handler,
		}},
		map[ComponentKind]struct{}{ComponentKindUIAction: {}},
	)
	if _, ok := routes[ComponentKindUIAction]; !ok {
		t.Fatalf("expected ui_action to be routed externally; install gate dropped it")
	}
	return routes
}

// setupSFRouteTestHost installs the same host wiring the golden oracle and the
// slice-1 route test rely on (the SF pack requires it).
func setupSFRouteTestHost(t *testing.T) {
	t.Helper()
	// 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"},
	)
}

// buildSFPackForRouteTest builds the SF pack via NewSourceBackedPack with the same
// options the production validation path applies, optionally diverting kinds
// through the external-route map, and returns the built pack so the test can pull
// out individual components for a deep compare.
func buildSFPackForRouteTest(t *testing.T, root string, routes map[ComponentKind]externalBuildRoute) *SourceBackedPack {
	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)
	}
	return pack
}

// uiActionComponentFromPack returns the single ui_action PackComponent built into
// the pack. There is exactly one ui_action manifest component in the SF pack.
func uiActionComponentFromPack(t *testing.T, pack *SourceBackedPack) PackComponent {
	t.Helper()
	var found PackComponent
	for _, c := range pack.Components() {
		if c.Kind() != ComponentKindUIAction {
			continue
		}
		if found != nil {
			t.Fatalf("expected exactly one ui_action component, found a second (businessKey=%q)", c.BusinessKey())
		}
		found = c
	}
	if found == nil {
		t.Fatalf("no ui_action component found in SF pack")
	}
	return found
}

// assertUIActionComponentsEqual deep-compares the full exported surface of two
// ui_action components, INCLUDING the parsed-content projection. Returns a
// non-empty mismatch description on the FIRST surface that drifts; empty string
// means fully identical.
func assertUIActionComponentsEqual(t *testing.T, control, routed PackComponent) string {
	t.Helper()

	if control.Kind() != routed.Kind() {
		return "Kind: control=" + string(control.Kind()) + " routed=" + string(routed.Kind())
	}
	if control.BusinessKey() != routed.BusinessKey() {
		return "BusinessKey: control=" + control.BusinessKey() + " routed=" + routed.BusinessKey()
	}
	if control.Manageability() != routed.Manageability() {
		return "Manageability: control=" + string(control.Manageability()) + " routed=" + string(routed.Manageability())
	}

	controlDep, okC := control.(ComponentDependencyProvider)
	routedDep, okR := routed.(ComponentDependencyProvider)
	if okC != okR {
		return "DependsOn provider mismatch: control=" + boolStr(okC) + " routed=" + boolStr(okR)
	}
	if okC && okR && !reflect.DeepEqual(controlDep.DependsOn(), routedDep.DependsOn()) {
		return "DependsOn drift"
	}

	cPayload, err := control.ExpectedPayload()
	if err != nil {
		t.Fatalf("control ExpectedPayload: %v", err)
	}
	rPayload, err := routed.ExpectedPayload()
	if err != nil {
		t.Fatalf("routed ExpectedPayload: %v", err)
	}
	if !bytes.Equal(cPayload, rPayload) {
		return "ExpectedPayload drift (raw bytes)"
	}

	cProj, err := control.ExpectedProjection()
	if err != nil {
		t.Fatalf("control ExpectedProjection: %v", err)
	}
	rProj, err := routed.ExpectedProjection()
	if err != nil {
		t.Fatalf("routed ExpectedProjection: %v", err)
	}
	if cProj == nil || rProj == nil {
		return "ExpectedProjection is nil (control=" + boolStr(cProj == nil) + " routed=" + boolStr(rProj == nil) + "); the content surface must be non-nil for ui_action"
	}
	if !reflect.DeepEqual(cProj, rProj) {
		return "ExpectedProjection drift (parsed content incl. Label):\ncontrol=" + projString(cProj) + "\nrouted =" + projString(rProj)
	}
	return ""
}

func projString(p any) string {
	b, err := json.Marshal(p)
	if err != nil {
		return "<unmarshalable projection>"
	}
	return string(b)
}

func boolStr(b bool) string {
	if b {
		return "true"
	}
	return "false"
}

// TestExternalUIActionRouteDeepCompare proves the external (contract-only) handler
// reconstructs a ui_action component whose FULL exported surface — including the
// parsed-content ExpectedProjection (Label-bearing) — is identical to the internal
// build path. This is the fidelity guarantee slice 1's pass-through kind could not
// give.
func TestExternalUIActionRouteDeepCompare(t *testing.T) {
	root := officialAppPacksRootForRouteTest(t)
	if root == "" {
		t.Skip("no nexuscrm-app-packs checkout found; skipping external ui_action deep-compare test")
	}
	setupSFRouteTestHost(t)

	routes := uiActionRoutes(t, fakeUIActionHandler)

	control := buildSFPackForRouteTest(t, root, nil)
	fakeUIActionHandlerFires = 0
	routed := buildSFPackForRouteTest(t, root, routes)
	if fakeUIActionHandlerFires == 0 {
		t.Fatalf("external ui_action handler never fired; route did not engage (the deep-compare below would be a false-green)")
	}

	controlComp := uiActionComponentFromPack(t, control)
	routedComp := uiActionComponentFromPack(t, routed)

	// Guard: the projection MUST carry Label content; otherwise the deep-compare
	// would be as blind as the oracle's PayloadHash and the mutation test below
	// could not bite.
	proj, err := controlComp.ExpectedProjection()
	if err != nil {
		t.Fatalf("control projection: %v", err)
	}
	rows, ok := proj.([]standardActionRow)
	if !ok {
		t.Fatalf("ui_action ExpectedProjection type %T, want []standardActionRow", proj)
	}
	if len(rows) == 0 {
		t.Fatalf("ui_action projection is empty; nothing to deep-compare")
	}
	sawLabel := false
	for _, r := range rows {
		if r.Label != "" {
			sawLabel = true
			break
		}
	}
	if !sawLabel {
		t.Fatalf("ui_action projection carries no Label; the content surface is not exercised")
	}

	if mismatch := assertUIActionComponentsEqual(t, controlComp, routedComp); mismatch != "" {
		t.Errorf("DRIFT: external ui_action route is lossy across the parsed surface.\n%s", mismatch)
	}
}

// TestExternalUIActionRouteMutationCaught corrupts ONE parsed Label in the
// external declaration path and asserts the deep-compare FAILS. This proves the
// deep-compare bites on the exact content drift (a Label change) that the oracle's
// raw-byte PayloadHash — which never re-reads the parsed projection — would miss.
func TestExternalUIActionRouteMutationCaught(t *testing.T) {
	root := officialAppPacksRootForRouteTest(t)
	if root == "" {
		t.Skip("no nexuscrm-app-packs checkout found; skipping external ui_action mutation test")
	}
	setupSFRouteTestHost(t)

	// Mutating handler: identical to the honest one EXCEPT it flips one Label in
	// the declaration slice before handing it to the engine sink. The raw
	// req.Payload is untouched, so ExpectedPayload/PayloadHash stay identical —
	// only ExpectedProjection (which is built FROM the declarations) drifts.
	mutated := false
	mutatingHandler := func(ctx cp.ComponentBuildContext, req cp.ComponentBuildRequest) error {
		pctx, ok := ctx.(cp.PresentationComponentBuildContext)
		if !ok {
			return errNotPresentationContext
		}
		decls, err := uiActionDeclarationsFromPayloadForTest(req.Payload)
		if err != nil {
			return err
		}
		if len(decls) > 0 {
			decls[0].Label = decls[0].Label + " (corrupted)"
			mutated = true
		}
		return pctx.AddUIActionComponent(req, decls)
	}

	routes := uiActionRoutes(t, mutatingHandler)

	control := buildSFPackForRouteTest(t, root, nil)
	routed := buildSFPackForRouteTest(t, root, routes)
	if !mutated {
		t.Fatalf("mutating handler never corrupted a Label; nothing was tested")
	}

	controlComp := uiActionComponentFromPack(t, control)
	routedComp := uiActionComponentFromPack(t, routed)

	// Raw payload must STILL be identical — the corruption lives only in the
	// parsed projection, exactly the blind spot the oracle's PayloadHash has.
	cPayload, err := controlComp.ExpectedPayload()
	if err != nil {
		t.Fatalf("control ExpectedPayload: %v", err)
	}
	rPayload, err := routedComp.ExpectedPayload()
	if err != nil {
		t.Fatalf("routed ExpectedPayload: %v", err)
	}
	if !bytes.Equal(cPayload, rPayload) {
		t.Fatalf("expected raw ExpectedPayload to stay identical under a Label-only mutation; a divergence here would mean the test corrupts the wrong surface")
	}

	mismatch := assertUIActionComponentsEqual(t, controlComp, routedComp)
	if mismatch == "" {
		t.Fatalf("MUTATION NOT CAUGHT: a corrupted Label produced no deep-compare drift; the content surface is blind (the exact failure mode this slice must seal)")
	}
	t.Logf("mutation correctly caught by deep-compare: %s", mismatch)
}
