package packs

import (
	"fmt"
	"strings"
	"testing"

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

// ---------------------------------------------------------------------------
// Pack-apply-time cycle validator tests
// ---------------------------------------------------------------------------

func TestValidateRollupCycles_NoCycles_Passes(t *testing.T) {
	// Grandparent → Parent → Child acyclic chain.
	// Grandparent.TotalGP (SUM over Parent), Parent.Total (SUM over Child).
	specs := []ObjectSpec{
		{
			APIName: "Child",
		},
		{
			APIName: "Parent",
			Fields: []FieldSpec{
				{
					APIName: "Total",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     "Child",
						SummaryField:      "Amount",
						RelationshipField: "ParentId",
					},
				},
			},
		},
		{
			APIName: "Grandparent",
			Fields: []FieldSpec{
				{
					APIName: "TotalGP",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     "Parent",
						SummaryField:      "Total",
						RelationshipField: "GrandparentId",
					},
				},
			},
		},
	}

	if err := ValidateRollupCycles(specs); err != nil {
		t.Errorf("expected no error for acyclic chain, got: %v", err)
	}
}

func TestValidateRollupCycles_TwoCycle_Rejected(t *testing.T) {
	// ObjectA.field1 rolls up over ObjectB; ObjectB.field2 rolls up over ObjectA → cycle.
	specs := []ObjectSpec{
		{
			APIName: "ObjectA",
			Fields: []FieldSpec{
				{
					APIName: "field1",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     "ObjectB",
						SummaryField:      "Amount",
						RelationshipField: "ObjectAId",
					},
				},
			},
		},
		{
			APIName: "ObjectB",
			Fields: []FieldSpec{
				{
					APIName: "field2",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     "ObjectA",
						SummaryField:      "Total",
						RelationshipField: "ObjectBId",
					},
				},
			},
		},
	}

	err := ValidateRollupCycles(specs)
	if err == nil {
		t.Fatal("expected cycle error for A↔B, got nil")
	}
	// Error must name both objects.
	msg := err.Error()
	if !strings.Contains(msg, "ObjectA") || !strings.Contains(msg, "ObjectB") {
		t.Errorf("error message must name both objects in cycle, got: %q", msg)
	}
	// Must use the term "cycle" to be admin-actionable.
	if !strings.Contains(strings.ToLower(msg), "cycle") {
		t.Errorf("error message must contain 'cycle', got: %q", msg)
	}
}

func TestValidateRollupCycles_SelfRollup_Rejected(t *testing.T) {
	// ObjectA.Total rolls up over ObjectA itself → self-loop.
	specs := []ObjectSpec{
		{
			APIName: "ObjectA",
			Fields: []FieldSpec{
				{
					APIName: "Total",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     "ObjectA",
						SummaryField:      "Amount",
						RelationshipField: "ParentId",
					},
				},
			},
		},
	}

	err := ValidateRollupCycles(specs)
	if err == nil {
		t.Fatal("expected self-rollup error, got nil")
	}
	msg := err.Error()
	if !strings.Contains(msg, "ObjectA") {
		t.Errorf("error message must name ObjectA, got: %q", msg)
	}
	if !strings.Contains(strings.ToLower(msg), "cycle") {
		t.Errorf("error message must contain 'cycle', got: %q", msg)
	}
}

// TestValidateRollupCycles_MixedCaseSummaryObject_SelfLoop_Rejected verifies that
// mixed-case / whitespace in SummaryObject that canonicalizes to the parent object
// name is caught as a self-loop cycle.
// Regression test for P1-A: raw key comparison bypassed canonicalization.
func TestValidateRollupCycles_MixedCaseSummaryObject_SelfLoop_Rejected(t *testing.T) {
	// "  objecta  " → trimmed+lowered → "objecta" == APIName "ObjectA" lowered → self-loop.
	specs := []ObjectSpec{
		{
			APIName: "ObjectA",
			Fields: []FieldSpec{
				{
					APIName: "Total",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     "  OBJECTA  ",
						SummaryField:      "Amount",
						RelationshipField: "ParentId",
					},
				},
			},
		},
	}

	err := ValidateRollupCycles(specs)
	if err == nil {
		t.Fatal("expected self-loop cycle error for mixed-case/whitespace SummaryObject, got nil")
	}
	msg := err.Error()
	if !strings.Contains(strings.ToLower(msg), "cycle") {
		t.Errorf("error must contain 'cycle', got: %q", msg)
	}
}

// TestValidateRollupCycles_MixedCaseTwoCycle_Rejected verifies that a 2-cycle
// declared with mixed case/whitespace names is detected after canonicalization.
// Regression test for P1-A.
func TestValidateRollupCycles_MixedCaseTwoCycle_Rejected(t *testing.T) {
	// ObjectA.f1 → SummaryObject=" objectb " (== "ObjectB" lowered)
	// ObjectB.f2 → SummaryObject=" ObjectA " (== "ObjectA" lowered) → 2-cycle.
	specs := []ObjectSpec{
		{
			APIName: "ObjectA",
			Fields: []FieldSpec{
				{
					APIName: "f1",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     " objectb ",
						SummaryField:      "Amount",
						RelationshipField: "ObjectAId",
					},
				},
			},
		},
		{
			APIName: "ObjectB",
			Fields: []FieldSpec{
				{
					APIName: "f2",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     " ObjectA ",
						SummaryField:      "Total",
						RelationshipField: "ObjectBId",
					},
				},
			},
		},
	}

	err := ValidateRollupCycles(specs)
	if err == nil {
		t.Fatal("expected 2-cycle error for mixed-case/whitespace SummaryObject, got nil")
	}
	msg := err.Error()
	if !strings.Contains(strings.ToLower(msg), "cycle") {
		t.Errorf("error must contain 'cycle', got: %q", msg)
	}
}

func TestValidateRollupCycles_ThreeObjectCycle_Rejected(t *testing.T) {
	// A→B→C→A three-object cycle.
	specs := []ObjectSpec{
		{
			APIName: "ObjA",
			Fields: []FieldSpec{
				{
					APIName: "FA",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						SummaryObject:     "ObjC",
						SummaryField:      "FC",
						RelationshipField: "ObjAId",
					},
				},
			},
		},
		{
			APIName: "ObjB",
			Fields: []FieldSpec{
				{
					APIName: "FB",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						SummaryObject:     "ObjA",
						SummaryField:      "FA",
						RelationshipField: "ObjBId",
					},
				},
			},
		},
		{
			APIName: "ObjC",
			Fields: []FieldSpec{
				{
					APIName: "FC",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						SummaryObject:     "ObjB",
						SummaryField:      "FB",
						RelationshipField: "ObjCId",
					},
				},
			},
		},
	}

	err := ValidateRollupCycles(specs)
	if err == nil {
		t.Fatal("expected 3-cycle error, got nil")
	}
	if !strings.Contains(strings.ToLower(err.Error()), "cycle") {
		t.Errorf("error must contain 'cycle', got: %q", err.Error())
	}
}

// ---------------------------------------------------------------------------
// Max-depth authoring check (Fix 1)
// ---------------------------------------------------------------------------

// TestValidateRollupCycles_MaxDepthExceeded_Rejected verifies that an acyclic
// rollup chain deeper than maxRollupDepthAuthoring is rejected at authoring time,
// matching the runtime depth cap so that valid-looking metadata cannot install
// successfully only to fail on the first record write.
func TestValidateRollupCycles_MaxDepthExceeded_Rejected(t *testing.T) {
	// Build a linear acyclic chain: L0 → L1 → ... → L8 (9 objects, 8 edges).
	// maxRollupDepthAuthoring = 8, so a path of length 8 (9 nodes) is over the cap.
	// L0.F0 rolls up over L1; L1.F1 over L2; ... ; L7.F7 over L8.
	specs := make([]ObjectSpec, 9)
	for i := 0; i < 9; i++ {
		name := fmt.Sprintf("Level%d", i)
		spec := ObjectSpec{APIName: name}
		if i < 8 {
			childName := fmt.Sprintf("Level%d", i+1)
			spec.Fields = []FieldSpec{
				{
					APIName: fmt.Sprintf("F%d", i),
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     childName,
						SummaryField:      fmt.Sprintf("F%d", i+1),
						RelationshipField: fmt.Sprintf("Level%dId", i),
					},
				},
			}
		}
		specs[i] = spec
	}

	err := ValidateRollupCycles(specs)
	if err == nil {
		t.Fatal("expected depth-exceeded error for 9-object acyclic chain, got nil")
	}
	msg := err.Error()
	if strings.Contains(strings.ToLower(msg), "cycle detected") {
		t.Errorf("error should be depth-exceeded, not cycle-detected; got: %q", msg)
	}
	if !strings.Contains(strings.ToLower(msg), "depth") {
		t.Errorf("error must contain 'depth', got: %q", msg)
	}
}

// TestValidateRollupCycles_MaxDepthExactlyAtCap_Passes verifies that a chain
// with exactly maxRollupDepthAuthoring-1 edges (8 objects) is accepted, confirming
// the boundary condition.
func TestValidateRollupCycles_MaxDepthExactlyAtCap_Passes(t *testing.T) {
	// 8 objects → 7 edges → depth 7, which is < maxRollupDepthAuthoring (8) → should pass.
	specs := make([]ObjectSpec, 8)
	for i := 0; i < 8; i++ {
		name := fmt.Sprintf("Cap%d", i)
		spec := ObjectSpec{APIName: name}
		if i < 7 {
			childName := fmt.Sprintf("Cap%d", i+1)
			spec.Fields = []FieldSpec{
				{
					APIName: fmt.Sprintf("F%d", i),
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     childName,
						SummaryField:      fmt.Sprintf("F%d", i+1),
						RelationshipField: fmt.Sprintf("Cap%dId", i),
					},
				},
			}
		}
		specs[i] = spec
	}

	if err := ValidateRollupCycles(specs); err != nil {
		t.Errorf("expected no error for 7-edge acyclic chain (at cap), got: %v", err)
	}
}

// ---------------------------------------------------------------------------
// Cross-component cycle detection (Fix 2)
// ---------------------------------------------------------------------------

// TestValidateRollupCyclesWithResolver_CrossComponentCycle_Rejected verifies
// that a cycle introduced across pack-apply boundaries is rejected.
// Scenario: object B→A edge is already installed (present in resolver.schemasByObject);
// the incoming specs add A→B edge — together they form a 2-cycle.
func TestValidateRollupCyclesWithResolver_CrossComponentCycle_Rejected(t *testing.T) {
	// Existing installed object: ObjectB rolls up over ObjectA.
	existingSchema := &models.ObjectMetadata{
		APIName: "ObjectB",
		Fields: []models.FieldMetadata{
			{
				APIName: "ExistingField",
				Type:    "rollupsummary",
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     "ObjectA",
					SummaryField:      "Amount",
					RelationshipField: "ObjectBId",
				},
			},
		},
	}
	resolver := objectSpecFieldResolver{
		schemasByObject: map[string]*models.ObjectMetadata{
			"objectb": existingSchema,
		},
	}

	// Incoming spec: ObjectA rolls up over ObjectB → closes the cycle.
	specs := []ObjectSpec{
		{
			APIName: "ObjectA",
			Fields: []FieldSpec{
				{
					APIName: "NewField",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     "ObjectB",
						SummaryField:      "ExistingField",
						RelationshipField: "ObjectAId",
					},
				},
			},
		},
	}

	err := ValidateRollupCyclesWithResolver(specs, resolver)
	if err == nil {
		t.Fatal("expected cross-component cycle error, got nil")
	}
	msg := err.Error()
	if !strings.Contains(strings.ToLower(msg), "cycle") {
		t.Errorf("error must contain 'cycle', got: %q", msg)
	}
	// Error should name both objects.
	if !strings.Contains(msg, "ObjectA") || !strings.Contains(msg, "ObjectB") {
		t.Errorf("error must name both objects, got: %q", msg)
	}
}

// ---------------------------------------------------------------------------
// Production path: frozenFormulaSchemasFromObjectSpecs must propagate RollupConfig
// ---------------------------------------------------------------------------

// TestFrozenFormulaSchemasFromObjectSpecs_PropagatesRollupConfig verifies that
// frozenFormulaSchemasFromObjectSpecs copies RollupConfig from FieldSpec into the
// resulting FieldMetadata.  This is the regression test for the R4 P1 finding:
// if RollupConfig is not copied, ValidateRollupCyclesWithResolver sees nil for all
// installed-object rollup fields and silently passes every cross-component cycle.
func TestFrozenFormulaSchemasFromObjectSpecs_PropagatesRollupConfig(t *testing.T) {
	specs := []ObjectSpec{
		{
			APIName: "ObjA",
			Fields: []FieldSpec{
				{
					APIName: "RollupField",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     "ObjB",
						SummaryField:      "Amount",
						RelationshipField: "ObjAId",
					},
				},
			},
		},
		{APIName: "ObjB"},
	}

	schemas := frozenFormulaSchemasFromObjectSpecs(specs)

	objA, ok := schemas["ObjA"]
	if !ok {
		t.Fatal("expected 'ObjA' in frozen schemas")
	}
	var found *models.FieldMetadata
	for i := range objA.Fields {
		if objA.Fields[i].APIName == "RollupField" {
			f := objA.Fields[i]
			found = &f
			break
		}
	}
	if found == nil {
		t.Fatal("expected RollupField in frozen ObjA schema")
	}
	if found.RollupConfig == nil {
		t.Fatal("RollupConfig must be propagated to frozen schema field, got nil")
	}
	if found.RollupConfig.SummaryObject != "ObjB" {
		t.Errorf("expected SummaryObject=ObjB, got %q", found.RollupConfig.SummaryObject)
	}
}

// TestFrozenFormulaSchemasFromObjectSpecs_CrossComponentCycleDetectedViaProductionPath
// is an end-to-end regression test for the R4 P1 production gap.
// It constructs two ObjectSpec groups:
//   - group1 (component-1): ObjectB with a rollupsummary field rolling up FROM ObjectA
//   - group2 (component-2): ObjectA with a rollupsummary field rolling up FROM ObjectB
//
// group1 is "already installed" — it goes through frozenFormulaSchemasFromObjectSpecs
// and then into newObjectSpecFieldResolverWithSchemas (the real production path).
// group2 is the "incoming apply" spec.  Together they form a cross-component cycle.
// ValidateRollupCyclesWithResolver must reject this.
func TestFrozenFormulaSchemasFromObjectSpecs_CrossComponentCycleDetectedViaProductionPath(t *testing.T) {
	// Simulate component-1 already installed: ObjectB rolls up FROM ObjectA.
	component1 := []ObjectSpec{
		{
			APIName: "ObjectB",
			Fields: []FieldSpec{
				{
					APIName: "RollupFromA",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     "ObjectA",
						SummaryField:      "Amount",
						RelationshipField: "ObjectBId",
					},
				},
			},
		},
		{APIName: "ObjectA"},
	}

	// Build frozen schemas exactly as the production path does (source_backed_pack.go).
	frozenSchemas := frozenFormulaSchemasFromObjectSpecs(component1)

	// Build the resolver exactly as sourceBackedObjectSpecComponent.fieldResolver() does.
	resolver := newObjectSpecFieldResolverWithSchemas(component1, frozenSchemas)

	// Incoming component-2 spec: ObjectA rolls up FROM ObjectB → closes the cycle.
	component2 := []ObjectSpec{
		{
			APIName: "ObjectA",
			Fields: []FieldSpec{
				{
					APIName: "RollupFromB",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     "ObjectB",
						SummaryField:      "RollupFromA",
						RelationshipField: "ObjectAId",
					},
				},
			},
		},
	}

	err := ValidateRollupCyclesWithResolver(component2, resolver)
	if err == nil {
		t.Fatal("expected cross-component cycle error via production path, got nil — RollupConfig likely not propagated by frozenFormulaSchemasFromObjectSpecs")
	}
	msg := err.Error()
	if !strings.Contains(strings.ToLower(msg), "cycle") {
		t.Errorf("error must contain 'cycle', got: %q", msg)
	}
}

// TestValidateRollupCyclesWithResolver_CrossComponentAcyclic_Passes verifies
// that a legitimate cross-component extension (no cycle introduced) is accepted.
func TestValidateRollupCyclesWithResolver_CrossComponentAcyclic_Passes(t *testing.T) {
	// Existing installed object: Grandparent rolls up over Parent.
	existingSchema := &models.ObjectMetadata{
		APIName: "Grandparent",
		Fields: []models.FieldMetadata{
			{
				APIName: "GrandTotal",
				Type:    "rollupsummary",
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     "Parent",
					SummaryField:      "Total",
					RelationshipField: "GrandparentId",
				},
			},
		},
	}
	resolver := objectSpecFieldResolver{
		schemasByObject: map[string]*models.ObjectMetadata{
			"grandparent": existingSchema,
		},
	}

	// New spec: Parent rolls up over Child — extends the chain, no cycle.
	specs := []ObjectSpec{
		{
			APIName: "Parent",
			Fields: []FieldSpec{
				{
					APIName: "Total",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "SUM",
						SummaryObject:     "Child",
						SummaryField:      "Amount",
						RelationshipField: "ParentId",
					},
				},
			},
		},
		{APIName: "Child"},
	}

	if err := ValidateRollupCyclesWithResolver(specs, resolver); err != nil {
		t.Errorf("expected no error for acyclic cross-component extension, got: %v", err)
	}
}
