// Shared deep-compare helpers for the presentation-kind external-route tests
// (related_list_rule, report_type, search_all_default_fields, href_rewrite_fields).
// Modeled on assertUIActionComponentsEqual but generic over kind: it compares the
// FULL exported surface — Kind + BusinessKey + Manageability + DependsOn +
// ExpectedPayload + ExpectedProjection — so any reconstruction drift in the
// external cp handler is caught. Kinds whose internal builder sets no
// expectedProjection legitimately compare nil==nil on that surface; the raw-byte
// ExpectedPayload remains the frozen fidelity floor for those.
package packs

import (
	"bytes"
	"reflect"
	"testing"
)

// buildSFPackOrErrForRouteTest is buildSFPackForRouteTest's error-returning twin:
// it builds the SF pack with the optional external routes but returns the
// NewSourceBackedPack error instead of fataling, so a test can assert that a
// corrupted declaration is REJECTED at build time. Source resolution failures
// (unrelated to the routes under test) still fatal.
func buildSFPackOrErrForRouteTest(t *testing.T, root string, routes map[ComponentKind]externalBuildRoute) (*SourceBackedPack, error) {
	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))
	}

	return NewSourceBackedPack(source, packOpts...)
}

// componentsOfKindForRouteTest indexes every component of the given kind built
// into the pack by its business key, so a multi-component kind (e.g.
// related_list_rule) can be compared component-for-component across the control
// and routed builds.
func componentsOfKindForRouteTest(t *testing.T, pack *SourceBackedPack, kind ComponentKind) map[string]PackComponent {
	t.Helper()
	out := map[string]PackComponent{}
	for _, c := range pack.Components() {
		if c.Kind() != kind {
			continue
		}
		key := c.BusinessKey()
		if _, dup := out[key]; dup {
			t.Fatalf("duplicate %s component business key %q in pack", kind, key)
		}
		out[key] = c
	}
	return out
}

// assertRouteComponentsEqual deep-compares the full exported surface of two
// components built for the same manifest component via the internal (control) and
// external cp (routed) paths. Returns a non-empty mismatch description on the
// FIRST surface that drifts; empty string means fully identical.
func assertRouteComponentsEqual(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 !reflect.DeepEqual(cProj, rProj) {
		return "ExpectedProjection drift (parsed content):\ncontrol=" + projString(cProj) + "\nrouted =" + projString(rProj)
	}
	return ""
}
