// White-box (package packs) DRIFT-CAPTURE test for the layout_assignment
// presentation kind in the SF-skin migration.
//
// layout_assignment is the SECOND bespoke presentation kind, and unlike the other
// seven presentation kinds it is NOT routable through the BUILD-path contract: it
// DRIFTS. This test captures the EXACT field diff and the missing engine-only input,
// so the DRIFT is proven rather than papered over. ComponentKindLayoutAssignment is
// intentionally absent from every routed `only` set; the internal
// buildLayoutAssignment stays authoritative.
//
// WHY IT DRIFTS
// -------------
// The internal component's ExpectedProjection ([]sourceBackedLayoutAssignmentRow)
// carries a RecordTypeID that is resolved from engine-only sibling state —
// b.recordTypeIndex[key].expectedID, written into each row inside
// validateSourceBackedLayoutAssignmentComponent (source_backed_layout_assignment_component.go:224-235).
// The contract declaration cp.LayoutAssignmentDeclaration is only
// {ObjectAPIName, RecordType}; it carries NEITHER that id NOR any field from which
// it can be derived. A reconstruction that consumes ONLY the declaration (as the
// BUILD-path contract requires) therefore emits rows with RecordTypeID == "".
//
// WHY THERE IS ALSO NO REAL DEEP-COMPARE FIXTURE
// ----------------------------------------------
// The SF oracle corpus (testdata/golden_oracle/salesforce-crm-standard.declared_components.json)
// and the SF pack source contain 14 page_layout components and ZERO
// layout_assignment components. So the established "deep-compare on a real SF
// manifest" pattern cannot be satisfied for this kind at all; this synthetic
// component-level diff is the only way to exhibit the drift, and the synthesis
// itself is off-pattern — a second reason the kind is not routed.
package packs

import (
	"strings"
	"testing"

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

// TestExternalLayoutAssignmentRouteDrifts builds the layout_assignment component
// BOTH ways over identical synthetic inputs — the internal constructor (control,
// which consults the engine recordTypeIndex) and the contract-only sink
// AddLayoutAssignmentComponent (routed, declaration-only) — and asserts the
// deep-compare DRIFTS precisely on ExpectedProjection.RecordTypeID. This is the
// documented evidence that cp.LayoutAssignmentDeclaration is lossy.
func TestExternalLayoutAssignmentRouteDrifts(t *testing.T) {
	const (
		packName       = "drift-fixture-pack"
		objectAPIName  = "Account"
		recordTypeName = "Business"
		recordTypeID   = "rt_account_business_authoritative_id"
		recordTypeBK   = "Account/Business"
		layoutBK       = "Account/default"
		componentBK    = "Account/Business->Account/default"
	)

	// The synthetic manifest component depends on BOTH the page_layout target (so the
	// internal target resolution succeeds) and the record_type (so the internal
	// dependency check passes). This is the well-formed shape the internal builder
	// requires; the drift is NOT a malformed-input artifact.
	component := ManifestComponent{
		Kind:          ComponentKindLayoutAssignment,
		BusinessKey:   componentBK,
		Manageability: ManageabilityLocked,
		DependsOn: []ComponentReference{
			{Kind: ComponentKindPageLayout, BusinessKey: layoutBK, Required: true},
			{Kind: ComponentKindRecordType, BusinessKey: recordTypeBK, Required: true},
		},
	}
	payload := []byte(`{"assignments":[{"objectApiName":"Account","recordType":"Business"}]}`)

	// recordTypeIndex is the ENGINE-ONLY sibling state. The internal control path reads
	// expectedID from it; the contract declaration cannot carry it.
	recordTypeIndex := map[string]sourceBackedRecordTypeTarget{
		recordTypePicklistValueRecordTypeKey(objectAPIName, recordTypeName): {
			businessKey:   recordTypeBK,
			objectAPIName: objectAPIName,
			name:          recordTypeName,
			expectedID:    recordTypeID,
		},
	}

	// CONTROL: the internal constructor, with the engine recordTypeIndex.
	controlComp, err := newSourceBackedLayoutAssignmentComponent(
		packName, component, payload, recordTypeIndex, newSourceBackedLayoutAssignmentPackBatch(),
	)
	if err != nil {
		t.Fatalf("internal control build: %v", err)
	}

	// ROUTED: the contract-only sink, driven by the declaration the provider emits.
	// It is given a build context whose currentComponent is the same manifest
	// component, but it consumes ONLY the {ObjectAPIName, RecordType} declaration.
	routedCtx := &sourceBackedComponentBuildContext{
		p:                &SourceBackedPack{manifest: &PackManifest{Name: packName}},
		currentComponent: component,
	}
	decls := []cp.LayoutAssignmentDeclaration{{ObjectAPIName: objectAPIName, RecordType: recordTypeName}}
	if err := routedCtx.AddLayoutAssignmentComponent(cp.ComponentBuildRequest{Payload: payload}, decls); err != nil {
		t.Fatalf("routed declaration-only build: %v", err)
	}
	if len(routedCtx.p.components) != 1 {
		t.Fatalf("routed sink produced %d components, want 1", len(routedCtx.p.components))
	}
	routedComp := routedCtx.p.components[0]

	// The deep-compare MUST drift, and specifically on ExpectedProjection.
	mismatch := assertRouteComponentsEqual(t, controlComp, routedComp)
	if mismatch == "" {
		t.Fatalf("expected layout_assignment to DRIFT across the parsed projection, but the deep-compare was clean; the declaration would be sufficient and the kind should be routed")
	}
	if !strings.Contains(mismatch, "ExpectedProjection") {
		t.Fatalf("expected the drift on ExpectedProjection (RecordTypeID), got a different surface:\n%s", mismatch)
	}

	// Pin the EXACT field diff: control RecordTypeID resolved from the engine index,
	// routed RecordTypeID empty (declaration carries only ObjectAPIName + RecordType).
	controlRows := layoutAssignmentRowsForTest(t, controlComp)
	routedRows := layoutAssignmentRowsForTest(t, routedComp)
	if len(controlRows) != 1 || len(routedRows) != 1 {
		t.Fatalf("expected one row each, got control=%d routed=%d", len(controlRows), len(routedRows))
	}
	if controlRows[0].RecordTypeID != recordTypeID {
		t.Fatalf("control RecordTypeID = %q, want the engine-resolved id %q", controlRows[0].RecordTypeID, recordTypeID)
	}
	if routedRows[0].RecordTypeID != "" {
		t.Fatalf("routed RecordTypeID = %q, want \"\" (the declaration cannot carry the engine-only id)", routedRows[0].RecordTypeID)
	}
	// Everything else the declaration CAN carry must match — the drift is isolated to
	// the single engine-only field, confirming RecordTypeID is the sole lossy input.
	cr, rr := controlRows[0], routedRows[0]
	cr.RecordTypeID, rr.RecordTypeID = "", ""
	if cr != rr {
		t.Fatalf("layout_assignment drift is NOT isolated to RecordTypeID; other fields also differ:\ncontrol=%+v\nrouted =%+v", cr, rr)
	}
	t.Logf("CONFIRMED DRIFT: layout_assignment ExpectedProjection.RecordTypeID control=%q routed=%q (engine-only b.recordTypeIndex[key].expectedID is uncarried by cp.LayoutAssignmentDeclaration)", controlRows[0].RecordTypeID, routedRows[0].RecordTypeID)
}

func layoutAssignmentRowsForTest(t *testing.T, comp PackComponent) []sourceBackedLayoutAssignmentRow {
	t.Helper()
	proj, err := comp.ExpectedProjection()
	if err != nil {
		t.Fatalf("ExpectedProjection: %v", err)
	}
	rows, ok := proj.([]sourceBackedLayoutAssignmentRow)
	if !ok {
		t.Fatalf("ExpectedProjection type %T, want []sourceBackedLayoutAssignmentRow", proj)
	}
	return rows
}
