// White-box (package packs) DEEP-COMPARE + MUTATION route test for the
// page_layout presentation kind in the SF-skin migration.
//
// page_layout is the first BESPOKE presentation pair (curated layout id assignment
// + dual-table PageLayout/LayoutAssignment write surface + layoutownership). The
// migration routes ONLY the BUILD path (parse payload -> component); APPLY/verify
// stays in L0. The internal buildPageLayout sets NO expectedProjection
// (ExpectedProjection()==nil); the component's compared surface is exactly
// {raw payload, manifest fields (BusinessKey/Manageability/DependsOn)}. The
// relatedListRules the constructor closes over are engine-resident and touched only
// at apply/verify, never on the build surface. So cp.PageLayoutDeclaration's
// {ObjectAPIName, LayoutName, IsDefault, Config(raw payload)} is sufficient: the
// sink reuses the SAME engine constructor with the RAW req.Payload, giving a
// byte-identical ExpectedPayload and nil==nil projection.
//
// This test builds the page_layout components BOTH ways — internal dispatch
// (control) and through the external cp handler (routed) — for the SAME SF manifest
// (14 real page_layout components, all <Object>/default) and asserts FULL equality
// across the exported surface, with a fired-counter guard against a silent no-route
// false-green.
//
// The MUTATION test then corrupts ONE byte of the raw payload in the external path
// and asserts the deep-compare FAILS. Because ExpectedProjection is nil for this
// kind, the honest mutation lever is the RAW ExpectedPayload (the frozen fidelity
// floor for nil-projection kinds), NOT a parsed projection.
package packs

import (
	"encoding/json"
	"fmt"
	"strings"
	"testing"

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

var fakePageLayoutHandlerFires int

// fakePageLayoutHandler is the inline contract-only stand-in for the real
// nexuscrm-app-packs provider.go:buildPageLayout. It depends ONLY on the cp
// contract: parse req.Payload into a cp.PageLayoutDeclaration and hand it to the
// engine sink. The sink ignores the slim declaration fields and reconstructs from
// the raw req.Payload, exactly as buildPageLayout does for fidelity.
func fakePageLayoutHandler(ctx cp.ComponentBuildContext, req cp.ComponentBuildRequest) error {
	pctx, ok := ctx.(cp.PresentationComponentBuildContext)
	if !ok {
		return errNotPresentationContext
	}
	decl, err := pageLayoutDeclarationFromPayloadForTest(req.Payload, req.Component.BusinessKey)
	if err != nil {
		return err
	}
	fakePageLayoutHandlerFires++
	return pctx.AddPageLayoutComponent(req, decl)
}

// pageLayoutDeclarationFromPayloadForTest reproduces the provider's
// pageLayoutDeclarationFromPayload (envelope parse + business-key-derived
// objectApiName/layoutName/isDefault resolution + Config=raw payload) WITHOUT
// importing the app-packs module, mirroring how the other route tests stand in for
// the real provider.
func pageLayoutDeclarationFromPayloadForTest(payload []byte, businessKey string) (cp.PageLayoutDeclaration, error) {
	var envelope struct {
		ObjectAPIName string `json:"objectApiName"`
		LayoutName    string `json:"layoutName"`
		IsDefault     *bool  `json:"isDefault"`
	}
	if err := json.Unmarshal(payload, &envelope); err != nil {
		return cp.PageLayoutDeclaration{}, err
	}
	objectAPIName := strings.TrimSpace(envelope.ObjectAPIName)
	layoutName := strings.TrimSpace(envelope.LayoutName)
	if objectAPIName == "" {
		return cp.PageLayoutDeclaration{}, fmt.Errorf("page_layout test payload: missing objectApiName")
	}
	componentObjectAPIName, layoutKey, hasLayoutKey := splitSourceBackedPageLayoutBusinessKey(businessKey)
	if hasLayoutKey {
		if !strings.EqualFold(componentObjectAPIName, objectAPIName) {
			return cp.PageLayoutDeclaration{}, fmt.Errorf("page_layout test payload: objectApiName %q must match business key head %q", objectAPIName, componentObjectAPIName)
		}
		if !strings.EqualFold(layoutKey, "default") && layoutName == "" {
			layoutName = layoutKey
		}
	}
	isDefault := false
	switch {
	case envelope.IsDefault != nil:
		isDefault = *envelope.IsDefault
	case hasLayoutKey && strings.EqualFold(layoutKey, "default"):
		isDefault = true
	case layoutName != "" && !strings.EqualFold(layoutName, "default"):
		isDefault = false
	default:
		return cp.PageLayoutDeclaration{}, fmt.Errorf("page_layout test payload: non-default page_layout requires explicit isDefault")
	}
	return cp.PageLayoutDeclaration{
		ObjectAPIName: objectAPIName,
		LayoutName:    layoutName,
		IsDefault:     isDefault,
		Config:        append([]byte(nil), payload...),
	}, nil
}

func pageLayoutRoutes(t *testing.T, handler cp.ComponentBuildHandler) map[ComponentKind]externalBuildRoute {
	t.Helper()
	routes := newExternalBuildRoutes(
		[]cp.ComponentKindRegistration{{
			Contract:     cp.ComponentKindContract{Kind: cp.ComponentKindPageLayout},
			Capability:   cp.SourceBackedComponentCapability{Kind: cp.ComponentKindPageLayout, Installable: true},
			BuildHandler: handler,
		}},
		map[ComponentKind]struct{}{ComponentKindPageLayout: {}},
	)
	if _, ok := routes[ComponentKindPageLayout]; !ok {
		t.Fatalf("expected page_layout to be routed externally; install gate dropped it")
	}
	return routes
}

// TestExternalPageLayoutRouteDeepCompare proves the external (contract-only) handler
// reconstructs page_layout components whose FULL exported surface — Kind +
// BusinessKey + Manageability + DependsOn + ExpectedPayload (raw) + ExpectedProjection
// (nil for this kind) — is identical to the internal build path on all 14 real SF
// page_layout components.
func TestExternalPageLayoutRouteDeepCompare(t *testing.T) {
	root := officialAppPacksRootForRouteTest(t)
	if root == "" {
		t.Skip("no nexuscrm-app-packs checkout found; skipping external page_layout deep-compare test")
	}
	setupSFRouteTestHost(t)

	routes := pageLayoutRoutes(t, fakePageLayoutHandler)

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

	controlComps := componentsOfKindForRouteTest(t, control, ComponentKindPageLayout)
	routedComps := componentsOfKindForRouteTest(t, routed, ComponentKindPageLayout)
	if len(controlComps) == 0 {
		t.Fatalf("no page_layout components found in SF pack; nothing to deep-compare")
	}
	if len(controlComps) != len(routedComps) {
		t.Fatalf("page_layout component count drift: control=%d routed=%d", len(controlComps), len(routedComps))
	}

	// Guard: the build surface MUST carry a non-empty raw ExpectedPayload, otherwise
	// the deep-compare would be vacuous for this nil-projection kind.
	sawPayload := false
	for _, c := range controlComps {
		payload, err := c.ExpectedPayload()
		if err != nil {
			t.Fatalf("control ExpectedPayload: %v", err)
		}
		if len(payload) > 0 {
			sawPayload = true
		}
		proj, err := c.ExpectedProjection()
		if err != nil {
			t.Fatalf("control ExpectedProjection: %v", err)
		}
		if proj != nil {
			t.Fatalf("page_layout ExpectedProjection expected nil (build path sets none), got %T", proj)
		}
	}
	if !sawPayload {
		t.Fatalf("page_layout components carry no raw payload; the content surface is not exercised")
	}

	for key, controlComp := range controlComps {
		routedComp, ok := routedComps[key]
		if !ok {
			t.Fatalf("routed pack is missing page_layout component %q present in control", key)
		}
		if mismatch := assertRouteComponentsEqual(t, controlComp, routedComp); mismatch != "" {
			t.Errorf("DRIFT: external page_layout route is lossy for %q.\n%s", key, mismatch)
		}
	}
}

// TestExternalPageLayoutRouteMutationCaught corrupts ONE byte of the raw payload in
// the external path and asserts the deep-compare FAILS. ExpectedProjection is nil
// for page_layout, so the raw ExpectedPayload is the fidelity floor: a payload-byte
// drift MUST surface as "ExpectedPayload drift (raw bytes)". This proves the
// deep-compare is not vacuously green for the nil-projection bespoke kind.
func TestExternalPageLayoutRouteMutationCaught(t *testing.T) {
	root := officialAppPacksRootForRouteTest(t)
	if root == "" {
		t.Skip("no nexuscrm-app-packs checkout found; skipping external page_layout mutation test")
	}
	setupSFRouteTestHost(t)

	// Mutating handler: identical to the honest one EXCEPT it appends a trailing
	// whitespace byte to the raw payload (semantically inert JSON, parses the same)
	// before handing it to the engine sink. The reconstructed component stores this
	// corrupted byte slice as its ExpectedPayload, so the raw-byte compare must bite
	// even though the parsed page layout is unchanged.
	mutated := false
	mutatingHandler := func(ctx cp.ComponentBuildContext, req cp.ComponentBuildRequest) error {
		pctx, ok := ctx.(cp.PresentationComponentBuildContext)
		if !ok {
			return errNotPresentationContext
		}
		decl, err := pageLayoutDeclarationFromPayloadForTest(req.Payload, req.Component.BusinessKey)
		if err != nil {
			return err
		}
		corrupted := append(append([]byte(nil), req.Payload...), ' ')
		req.Payload = corrupted
		mutated = true
		return pctx.AddPageLayoutComponent(req, decl)
	}

	routes := pageLayoutRoutes(t, mutatingHandler)

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

	controlComps := componentsOfKindForRouteTest(t, control, ComponentKindPageLayout)
	routedComps := componentsOfKindForRouteTest(t, routed, ComponentKindPageLayout)

	caught := false
	for key, controlComp := range controlComps {
		routedComp, ok := routedComps[key]
		if !ok {
			t.Fatalf("routed pack is missing page_layout component %q present in control", key)
		}
		if mismatch := assertRouteComponentsEqual(t, controlComp, routedComp); mismatch != "" {
			caught = true
			if !strings.Contains(mismatch, "ExpectedPayload") {
				t.Errorf("page_layout mutation caught, but on an unexpected surface: %s", mismatch)
			}
		}
	}
	if !caught {
		t.Fatalf("MUTATION NOT CAUGHT: a corrupted raw payload produced no deep-compare drift; the payload surface is blind (the exact failure mode this slice must seal)")
	}
}
