package packs

import (
	"sort"
	"strings"
	"testing"

	"github.com/nexuscrm/shared/pkg/models"
)

func TestSourceBackedCRMPackDeclaresKnownCuratedLayoutRepros(t *testing.T) {
	registerCRMHostPrimitivesForTest(t)
	opts := officialCRMPackSourceOptionsForTest(t)

	source, diagnostics := ResolvePackSourceWithOptions("official_repo:salesforce-crm-standard", opts)
	if len(diagnostics) > 0 {
		t.Fatalf("resolve CRM source diagnostics: %v", diagnostics)
	}
	pack, err := NewSourceBackedPack(source, officialCRMSourceBackedPackOptionsForTest(t, opts)...)
	if err != nil {
		t.Fatalf("construct CRM source-backed pack: %v", err)
	}

	want := map[string]struct {
		objectAPIName string
		layoutName    string
	}{
		"Case/default": {
			objectAPIName: "Case",
			layoutName:    "Case Layout (Curated)",
		},
		"Contact/default": {
			objectAPIName: "Contact",
			layoutName:    "Contact Layout (Curated)",
		},
		"Campaign/default": {
			objectAPIName: "Campaign",
			layoutName:    "Campaign Layout (Curated)",
		},
	}
	seen := make(map[string]bool, len(want))
	for _, component := range pack.Components() {
		wantLayout, ok := want[component.BusinessKey()]
		if !ok {
			continue
		}
		seen[component.BusinessKey()] = true
		if component.Kind() != ComponentKindPageLayout {
			t.Fatalf("%s kind = %q, want page_layout", component.BusinessKey(), component.Kind())
		}
		if component.Manageability() != ManageabilityDefaultsOnly {
			t.Fatalf("%s manageability = %q, want defaults_only", component.BusinessKey(), component.Manageability())
		}

		payload, err := component.ExpectedPayload()
		if err != nil {
			t.Fatalf("%s ExpectedPayload: %v", component.BusinessKey(), err)
		}
		layout, err := sourceBackedPageLayoutFromPayload(payload, component.BusinessKey())
		if err != nil {
			t.Fatalf("%s sourceBackedPageLayoutFromPayload: %v", component.BusinessKey(), err)
		}
		assertSourceBackedCuratedLayoutIdentity(t, component.BusinessKey(), wantLayout.objectAPIName, wantLayout.layoutName, layout)
	}
	for businessKey := range want {
		if !seen[businessKey] {
			t.Fatalf("CRM source-backed pack did not declare page_layout %s", businessKey)
		}
	}
}

func TestSourceBackedCRMDefaultLayoutCoverageInventoryIsClassified(t *testing.T) {
	registerCRMHostPrimitivesForTest(t)
	opts := officialCRMPackSourceOptionsForTest(t)

	source, diagnostics := ResolvePackSourceWithOptions("official_repo:salesforce-crm-standard", opts)
	if len(diagnostics) > 0 {
		t.Fatalf("resolve CRM source diagnostics: %v", diagnostics)
	}
	pack, err := NewSourceBackedPack(source, officialCRMSourceBackedPackOptionsForTest(t, opts)...)
	if err != nil {
		t.Fatalf("construct CRM source-backed pack: %v", err)
	}

	objectNames := map[string]struct{}{}
	authoredDefaultLayouts := map[string]struct{}{}
	for _, component := range pack.Components() {
		switch component.Kind() {
		case ComponentKindObjectSpec:
			payload, err := component.ExpectedPayload()
			if err != nil {
				t.Fatalf("%s ExpectedPayload: %v", component.BusinessKey(), err)
			}
			specs, err := parseObjectSpecsJSON(payload, component.BusinessKey())
			if err != nil {
				t.Fatalf("%s parseObjectSpecsJSON: %v", component.BusinessKey(), err)
			}
			for _, spec := range specs {
				objectNames[spec.APIName] = struct{}{}
			}
		case ComponentKindPageLayout:
			if !strings.HasSuffix(component.BusinessKey(), "/default") {
				continue
			}
			payload, err := component.ExpectedPayload()
			if err != nil {
				t.Fatalf("%s ExpectedPayload: %v", component.BusinessKey(), err)
			}
			layout, err := sourceBackedPageLayoutFromPayload(payload, component.BusinessKey())
			if err != nil {
				t.Fatalf("%s sourceBackedPageLayoutFromPayload: %v", component.BusinessKey(), err)
			}
			if !layout.IsDefault {
				t.Fatalf("%s page_layout IsDefault = false, want true for default-layout inventory", component.BusinessKey())
			}
			authoredDefaultLayouts[layout.ObjectAPIName] = struct{}{}
		}
	}

	// Objects in this set are setup/junction/line-comment shapes where the
	// recordType-uniform structural projection is the intended long-term
	// describe-layout answer unless a future product workflow needs a real L1
	// layout. Keep this list exact so new CRM objects cannot silently fall
	// through the projection bridge without an ownership decision.
	permanentProjectionOK := map[string]string{
		"CampaignMemberStatus":   "campaign setup/status catalog",
		"CaseComment":            "case child comment row",
		"CurrencyType":           "currency setup catalog",
		"DatedConversionRate":    "dated currency-rate setup catalog",
		"OpportunityContactRole": "opportunity-contact junction",
		"OpportunityStage":       "opportunity setup/stage catalog",
		"OrderItem":              "order line-item child",
		"PricebookEntry":         "pricebook-product junction",
	}

	transitionalProjectionBacklog := map[string]string{
		"ContentDistribution":            "file distribution setup record",
		"IdentitySyncDeactivationTicket": "identity-sync worker ticket",
		"IdentitySyncSetting":            "identity-sync setup record",
	}

	var unclassified []string
	for objectAPIName := range objectNames {
		if _, ok := authoredDefaultLayouts[objectAPIName]; ok {
			continue
		}
		if _, ok := permanentProjectionOK[objectAPIName]; ok {
			continue
		}
		if _, ok := transitionalProjectionBacklog[objectAPIName]; ok {
			continue
		}
		unclassified = append(unclassified, objectAPIName)
	}
	if len(unclassified) > 0 {
		sort.Strings(unclassified)
		t.Fatalf("CRM object(s) lack default page_layout and projection classification: %v", unclassified)
	}

	for _, objectAPIName := range sortedStringKeys(permanentProjectionOK) {
		if _, ok := objectNames[objectAPIName]; !ok {
			t.Fatalf("permanent projection classification %s missing from CRM object specs", objectAPIName)
		}
		if _, ok := authoredDefaultLayouts[objectAPIName]; ok {
			t.Fatalf("permanent projection classification %s is stale: CRM pack now declares an authored default page_layout", objectAPIName)
		}
	}
	for _, objectAPIName := range sortedStringKeys(transitionalProjectionBacklog) {
		if _, ok := objectNames[objectAPIName]; !ok {
			t.Fatalf("transitional projection classification %s missing from CRM object specs", objectAPIName)
		}
		if _, ok := authoredDefaultLayouts[objectAPIName]; ok {
			t.Fatalf("transitional projection classification %s is stale: CRM pack now declares an authored default page_layout", objectAPIName)
		}
	}

	wantAuthored := []string{
		"Account",
		"Campaign",
		"CampaignMember",
		"Case",
		"Contact",
		"Contract",
		"EmailMessage",
		"Lead",
		"Opportunity",
		"OpportunityLineItem",
		"Order",
		"Pricebook2",
		"Product2",
		"Quote",
		"QuoteLineItem",
		"Task",
	}
	if got := sortedStructKeys(authoredDefaultLayouts); !stringSlicesEqual(got, wantAuthored) {
		t.Fatalf("CRM authored default page_layout objects = %v, want %v", got, wantAuthored)
	}
}

func TestSourceBackedLayoutIDSupportsMultipleManagedLayoutsPerObject(t *testing.T) {
	layoutA := models.PageLayout{
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Layout (Curated)",
		IsDefault:     true,
	}
	layoutB := models.PageLayout{
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Layout Expansion & Renewal",
		IsDefault:     false,
	}

	idA := sourceBackedLayoutID(layoutA.ObjectAPIName, layoutA.LayoutName, layoutA.IsDefault)
	idB := sourceBackedLayoutID(layoutB.ObjectAPIName, layoutB.LayoutName, layoutB.IsDefault)

	if idA == idB {
		t.Fatalf("different Quote layout names collapsed to same managed ID %q", idA)
	}
	if got, want := idA, "curated_Quote_default"; got != want {
		t.Fatalf("Quote layout A ID = %q, want %q", got, want)
	}
	if got, want := idB, "curated_Quote_Quote_Layout_Expansion_Renewal_edb2cd2393de"; got != want {
		t.Fatalf("Quote layout B ID = %q, want %q", got, want)
	}
}

func TestSourceBackedLayoutIDBoundsLongNonDefaultManagedIDs(t *testing.T) {
	id := sourceBackedLayoutID("Quote", strings.Repeat("Renewal Layout ", 32), false)
	if len(id) > sourceBackedLayoutIDMaxLen {
		t.Fatalf("non-default layout ID length = %d, want <= %d", len(id), sourceBackedLayoutIDMaxLen)
	}
	if !strings.HasPrefix(id, "curated_Quote_") {
		t.Fatalf("non-default layout ID = %q, want curated object prefix", id)
	}
	parts := strings.Split(id, "_")
	if got := parts[len(parts)-1]; len(got) != sourceBackedLayoutIDHashLen {
		t.Fatalf("non-default layout ID hash suffix = %q, want %d hex chars", got, sourceBackedLayoutIDHashLen)
	}
}

func sortedStructKeys(m map[string]struct{}) []string {
	keys := make([]string, 0, len(m))
	for key := range m {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

func sortedStringKeys(m map[string]string) []string {
	keys := make([]string, 0, len(m))
	for key := range m {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

func assertSourceBackedCuratedLayoutIdentity(t *testing.T, businessKey string, objectAPIName string, layoutName string, layout models.PageLayout) {
	t.Helper()
	if layout.ObjectAPIName != objectAPIName {
		t.Fatalf("%s object API name = %q, want %q", businessKey, layout.ObjectAPIName, objectAPIName)
	}
	if layout.LayoutName != layoutName {
		t.Fatalf("%s layout name = %q, want %q", businessKey, layout.LayoutName, layoutName)
	}
	if layout.ID != sourceBackedLayoutID(objectAPIName, layoutName, layout.IsDefault) {
		t.Fatalf("%s layout ID = %q, want %q", businessKey, layout.ID, sourceBackedLayoutID(objectAPIName, layoutName, layout.IsDefault))
	}
	if layout.Source != curatedLayoutSource {
		t.Fatalf("%s layout Source = %q, want %q", businessKey, layout.Source, curatedLayoutSource)
	}
	if layout.IsCustomized {
		t.Fatalf("%s IsCustomized = true, want false for source-backed curated layout", businessKey)
	}
	wantAssignmentID := "curated_" + objectAPIName + "_assignment_wildcard"
	if got := sourceBackedLayoutAssignmentID(objectAPIName); got != wantAssignmentID {
		t.Fatalf("%s assignment ID = %q, want %q", businessKey, got, wantAssignmentID)
	}
	if wantAssignmentID == models.LayoutAssignmentRouteSourceRuntimeFallback {
		t.Fatalf("%s source-backed assignment identity must not be runtime_fallback", businessKey)
	}
}
