package analytics

import (
	"context"
	"strings"
	"testing"

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

// ---------------------------------------------------------------------------
// Runtime depth-guard tests
// ---------------------------------------------------------------------------

// cyclicDerivedWriter simulates a derived writer that triggers another
// ProcessRollups call, creating a runtime cycle by calling back into the
// rollup engine via a shared state counter.  The writer increments a
// counter and calls back into svc when depth < limit, replicating how
// UpdateDerived in production triggers another ProcessRollups for the
// newly-written parent's own parent.
type cyclicDerivedWriter struct {
	svc      *RollupService
	callBack bool // when true, each write re-invokes ProcessRollups
	calls    int
}

func (c *cyclicDerivedWriter) UpdateDerived(ctx context.Context, objectName, _ string, _ models.SObject, _ *models.UserSession) error {
	c.calls++
	if c.callBack && c.svc != nil {
		// Re-enter ProcessRollups to simulate a parent-of-parent rollup chain.
		// The childRecord carries a non-empty relationship field so the engine
		// finds an AffectedRollup and recurses.
		child := models.SObject{"ParentId": "parent-001"}
		return c.svc.ProcessRollups(ctx, objectName, child)
	}
	return nil
}

func (c *cyclicDerivedWriter) LockDerivedRecord(_ context.Context, _ string, _ string) error {
	return nil
}

func (c *cyclicDerivedWriter) GetDerivedRecordForUpdate(_ context.Context, _ string, _ string) (models.SObject, error) {
	return models.SObject{}, nil
}

func (c *cyclicDerivedWriter) LoadSystemUserSession(_ context.Context) (*models.UserSession, error) {
	return testRollupSystemSession(), nil
}

func (c *cyclicDerivedWriter) UpdateDerivedCountRollupValues(ctx context.Context, objectName, recordID string, _ []string, user *models.UserSession, recalculate func(context.Context) (models.SObject, error)) error {
	updates, err := recalculate(ctx)
	if err != nil {
		return err
	}
	return c.UpdateDerived(ctx, objectName, recordID, updates, user)
}

func (c *cyclicDerivedWriter) CanApplyDerivedCountRollupDeltas(context.Context, string, ...string) (bool, error) {
	return false, nil
}

func (c *cyclicDerivedWriter) CountRollupFieldsDeclareEventual(context.Context, string, ...string) (bool, error) {
	return false, nil
}

func (c *cyclicDerivedWriter) EnqueueDerivedCountRollupDeltas(context.Context, string, string, string, string, map[string]int64) error {
	return nil
}

func (c *cyclicDerivedWriter) ApplyDerivedCountRollupDeltas(ctx context.Context, objectName, recordID string, deltas map[string]int64, user *models.UserSession) error {
	updates := make(models.SObject, len(deltas))
	for fieldName, delta := range deltas {
		updates[fieldName] = delta
	}
	return c.UpdateDerived(ctx, objectName, recordID, updates, user)
}

// buildChainSchemas returns a schema map where each object in the chain
// has a COUNT rollup summary pointing at the previous object.
// chain = [Obj0, Obj1, Obj2, ...]: ObjN.rollup → ObjN-1
func buildChainSchemas(chain []string) map[string]*models.ObjectMetadata {
	schemas := make(map[string]*models.ObjectMetadata, len(chain))
	for i, name := range chain {
		schema := &models.ObjectMetadata{APIName: name}
		if i > 0 {
			parent := chain[i-1]
			schema.Fields = []models.FieldMetadata{
				{
					APIName: "RollupCount",
					Type:    constants.FieldTypeRollupSummary,
					RollupConfig: &models.RollupConfig{
						CalcType:          "COUNT",
						SummaryObject:     parent,
						SummaryField:      "Id",
						RelationshipField: "ParentId",
					},
				},
			}
		}
		schemas[name] = schema
	}
	return schemas
}

// TestProcessRollups_DepthGuard_Fails verifies that a contrived depth-9
// chain is rejected with an explicit error citing the depth cap.
func TestProcessRollups_DepthGuard_Fails(t *testing.T) {
	// Build a chain of 10 objects (Obj0..Obj9).
	// Obj9 has a rollup over Obj8, which has one over Obj7, etc.
	// cyclicDerivedWriter calls back into ProcessRollups for the parent
	// object on every UpdateDerived, simulating depth-9 re-entrancy.
	chain := make([]string, 10)
	for i := range chain {
		chain[i] = "ChainObj" + string(rune('0'+i))
	}

	schemas := buildChainSchemas(chain)
	meta := newFakeMetadata(schemas)
	repo := &stubRollupCalcRepo{calcValue: float64(1)}

	svc := NewRollupService(repo, meta, alwaysPessimisticRollupTxManager{}, noopLogger())
	dw := &cyclicDerivedWriter{svc: svc, callBack: true}
	svc.SetDerivedWriter(dw)

	// Trigger from the deepest child → should hit the depth cap before completion.
	childRecord := models.SObject{"ParentId": "parent-001"}
	err := svc.ProcessRollups(context.Background(), chain[0], childRecord)

	if err == nil {
		t.Fatal("expected depth-cap error, got nil")
	}
	msg := err.Error()
	// Must mention "depth" or "cap" so it's actionable.
	if !strings.Contains(strings.ToLower(msg), "depth") && !strings.Contains(strings.ToLower(msg), "cap") {
		t.Errorf("error must mention 'depth' or 'cap', got: %q", msg)
	}
	// Must mention the configured cap value (8).
	if !strings.Contains(msg, "8") {
		t.Errorf("error must cite the depth cap (8), got: %q", msg)
	}
}

// ---------------------------------------------------------------------------
// Visited-key guard tests (P1-B regression)
// ---------------------------------------------------------------------------

// visitedKeyDerivedWriter is a derived writer that always calls back into
// ProcessRollups with the SAME (objectName, parentID), simulating a short cycle.
// Unlike cyclicDerivedWriter it does not stop at any depth limit — the visited-key
// guard (not the depth cap) is expected to stop it.
type visitedKeyDerivedWriter struct {
	svc        *RollupService
	objectName string
	parentID   string
}

func (v *visitedKeyDerivedWriter) UpdateDerived(ctx context.Context, objectName, _ string, _ models.SObject, _ *models.UserSession) error {
	if v.svc == nil {
		return nil
	}
	// Call back with the same child record that triggers the same rollup.
	child := models.SObject{"ParentId": v.parentID}
	return v.svc.ProcessRollups(ctx, objectName, child)
}

func (v *visitedKeyDerivedWriter) LockDerivedRecord(_ context.Context, _ string, _ string) error {
	return nil
}

func (v *visitedKeyDerivedWriter) GetDerivedRecordForUpdate(_ context.Context, _ string, _ string) (models.SObject, error) {
	return models.SObject{}, nil
}

func (v *visitedKeyDerivedWriter) LoadSystemUserSession(_ context.Context) (*models.UserSession, error) {
	return testRollupSystemSession(), nil
}

func (v *visitedKeyDerivedWriter) UpdateDerivedCountRollupValues(ctx context.Context, objectName, recordID string, _ []string, user *models.UserSession, recalculate func(context.Context) (models.SObject, error)) error {
	updates, err := recalculate(ctx)
	if err != nil {
		return err
	}
	return v.UpdateDerived(ctx, objectName, recordID, updates, user)
}

func (v *visitedKeyDerivedWriter) CanApplyDerivedCountRollupDeltas(context.Context, string, ...string) (bool, error) {
	return false, nil
}

func (v *visitedKeyDerivedWriter) CountRollupFieldsDeclareEventual(context.Context, string, ...string) (bool, error) {
	return false, nil
}

func (v *visitedKeyDerivedWriter) EnqueueDerivedCountRollupDeltas(context.Context, string, string, string, string, map[string]int64) error {
	return nil
}

func (v *visitedKeyDerivedWriter) ApplyDerivedCountRollupDeltas(ctx context.Context, objectName, recordID string, deltas map[string]int64, user *models.UserSession) error {
	updates := make(models.SObject, len(deltas))
	for fieldName, delta := range deltas {
		updates[fieldName] = delta
	}
	return v.UpdateDerived(ctx, objectName, recordID, updates, user)
}

// TestProcessRollups_VisitedKey_SelfCycle_Fails verifies that a self-cycle
// (A → A on the same parentID+rollupField) is caught by the visited-key guard
// before the depth cap of 8.
func TestProcessRollups_VisitedKey_SelfCycle_Fails(t *testing.T) {
	const (
		objA     = "ObjA"
		parentID = "parent-001"
	)

	// ObjA has a rollup over itself: SummaryObject=ObjA.
	schemas := map[string]*models.ObjectMetadata{
		objA: {
			APIName: objA,
			Fields: []models.FieldMetadata{
				{
					APIName: "SelfRollup",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "COUNT",
						SummaryObject:     objA,
						SummaryField:      "Id",
						RelationshipField: "ParentId",
					},
				},
			},
		},
	}

	meta := newFakeMetadata(schemas)
	repo := &stubRollupCalcRepo{calcValue: float64(1)}
	svc := NewRollupService(repo, meta, alwaysPessimisticRollupTxManager{}, noopLogger())
	dw := &visitedKeyDerivedWriter{svc: svc, objectName: objA, parentID: parentID}
	svc.SetDerivedWriter(dw)

	child := models.SObject{"ParentId": parentID}
	err := svc.ProcessRollups(context.Background(), objA, child)
	if err == nil {
		t.Fatal("expected visited-key cycle error for self-cycle, got nil")
	}
	// Must fail before depth=8 (the visited key should trip at depth 1 or 2).
	msg := err.Error()
	if !strings.Contains(strings.ToLower(msg), "cycle") && !strings.Contains(strings.ToLower(msg), "repeat") && !strings.Contains(strings.ToLower(msg), "visit") {
		t.Errorf("error must indicate a cycle/repeat/visit, got: %q", msg)
	}
}

// TestProcessRollups_VisitedKey_TwoCycle_Fails verifies that a 2-cycle
// (A→B→A on the same parent IDs) is caught by the visited-key guard
// before depth=8.
func TestProcessRollups_VisitedKey_TwoCycle_Fails(t *testing.T) {
	const (
		objA     = "TwoCycleA"
		objB     = "TwoCycleB"
		parentID = "parent-001"
	)

	// ObjA has a rollup over ObjB; ObjB has a rollup over ObjA.
	schemas := map[string]*models.ObjectMetadata{
		objA: {
			APIName: objA,
			Fields: []models.FieldMetadata{
				{
					APIName: "RollupOnB",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "COUNT",
						SummaryObject:     objB,
						SummaryField:      "Id",
						RelationshipField: "ParentId",
					},
				},
			},
		},
		objB: {
			APIName: objB,
			Fields: []models.FieldMetadata{
				{
					APIName: "RollupOnA",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "COUNT",
						SummaryObject:     objA,
						SummaryField:      "Id",
						RelationshipField: "ParentId",
					},
				},
			},
		},
	}

	meta := newFakeMetadata(schemas)
	repo := &stubRollupCalcRepo{calcValue: float64(1)}
	svc := NewRollupService(repo, meta, alwaysPessimisticRollupTxManager{}, noopLogger())
	// The writer calls back into ProcessRollups for the parent object on every write.
	dw := &cyclicDerivedWriter{svc: svc, callBack: true}
	svc.SetDerivedWriter(dw)

	child := models.SObject{"ParentId": parentID}
	err := svc.ProcessRollups(context.Background(), objA, child)
	if err == nil {
		t.Fatal("expected cycle error for A↔B 2-cycle, got nil")
	}
	msg := err.Error()
	// Must fire before depth=8.  We check by ensuring the depth cap message is NOT the only
	// signal — the visited-key guard should fire first at depth ≤ 3.
	if strings.Contains(msg, "depth cap exceeded") {
		// If the depth cap fires that means visited-key guard did NOT intercept early enough.
		// This is still a test failure for P1-B.
		t.Errorf("expected visited-key guard to fire before depth cap, but got depth-cap error: %q", msg)
	}
}

// TestProcessRollups_VisitedKey_AcyclicChain_Passes verifies that a legitimate
// acyclic 2-level chain (grandparent→parent→child) passes the visited-key guard
// even when each level has different parent IDs.
func TestProcessRollups_VisitedKey_AcyclicChain_Passes(t *testing.T) {
	const (
		childObj       = "VisitItem"
		parentObj      = "VisitInvoice"
		grandparentObj = "VisitAccount"
	)

	schemas := map[string]*models.ObjectMetadata{
		childObj: {APIName: childObj},
		parentObj: {
			APIName: parentObj,
			Fields: []models.FieldMetadata{
				{
					APIName: "TotalItems",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "COUNT",
						SummaryObject:     childObj,
						SummaryField:      "Id",
						RelationshipField: "InvoiceId",
					},
				},
			},
		},
		grandparentObj: {
			APIName: grandparentObj,
			Fields: []models.FieldMetadata{
				{
					APIName: "TotalInvoices",
					Type:    "rollupsummary",
					RollupConfig: &models.RollupConfig{
						CalcType:          "COUNT",
						SummaryObject:     parentObj,
						SummaryField:      "Id",
						RelationshipField: "AccountId",
					},
				},
			},
		},
	}

	meta := newFakeMetadata(schemas)
	repo := &stubRollupCalcRepo{calcValue: float64(1)}
	svc := NewRollupService(repo, meta, alwaysPessimisticRollupTxManager{}, noopLogger())
	dw := &cyclicDerivedWriter{svc: svc, callBack: true}
	svc.SetDerivedWriter(dw)

	// Child has different parent IDs at each level — genuinely acyclic.
	child := models.SObject{"InvoiceId": "inv-001", "AccountId": "acc-001"}
	if err := svc.ProcessRollups(context.Background(), childObj, child); err != nil {
		t.Errorf("expected no error for acyclic 2-level chain, got: %v", err)
	}
	if dw.calls == 0 {
		t.Error("expected at least 1 UpdateDerived call, got 0")
	}
}

// TestProcessRollups_DepthGuard_Passes_NormalChain verifies a normal
// 2-level chain (grandparent → parent → child) terminates cleanly.
func TestProcessRollups_DepthGuard_Passes_NormalChain(t *testing.T) {
	// Grandparent.TotalGP (SUM over Parent), Parent.Total (SUM over Child).
	// When a Child record changes:
	//   ProcessRollups("Child", child) → finds Parent.Total → writes → UpdateDerived("Parent", ...)
	// With callBack=true the writer would then call ProcessRollups("Parent", parent)
	//   → finds Grandparent.TotalGP → writes → UpdateDerived("Grandparent", ...)
	// With callBack=true the writer then calls ProcessRollups("Grandparent", gp)
	//   → no rollup on Grandparent (it IS the top) → 0 updates → no further recursion.
	// Total depth = 2, well within the cap.

	const (
		childObj       = "Item"
		parentObj      = "Invoice"
		grandparentObj = "Account"
	)

	schemas := map[string]*models.ObjectMetadata{
		childObj: {APIName: childObj},
		parentObj: {
			APIName: parentObj,
			Fields: []models.FieldMetadata{
				{
					APIName: "TotalItems",
					Type:    constants.FieldTypeRollupSummary,
					RollupConfig: &models.RollupConfig{
						CalcType:          "COUNT",
						SummaryObject:     childObj,
						SummaryField:      "Id",
						RelationshipField: "InvoiceId",
					},
				},
			},
		},
		grandparentObj: {
			APIName: grandparentObj,
			Fields: []models.FieldMetadata{
				{
					APIName: "TotalInvoices",
					Type:    constants.FieldTypeRollupSummary,
					RollupConfig: &models.RollupConfig{
						CalcType:          "COUNT",
						SummaryObject:     parentObj,
						SummaryField:      "Id",
						RelationshipField: "AccountId",
					},
				},
			},
		},
	}

	meta := newFakeMetadata(schemas)
	repo := &stubRollupCalcRepo{calcValue: float64(1)}

	svc := NewRollupService(repo, meta, alwaysPessimisticRollupTxManager{}, noopLogger())
	dw := &cyclicDerivedWriter{svc: svc, callBack: true}
	svc.SetDerivedWriter(dw)

	childRecord := models.SObject{"InvoiceId": "inv-001", "AccountId": "acc-001"}
	if err := svc.ProcessRollups(context.Background(), childObj, childRecord); err != nil {
		t.Errorf("expected no error for 2-level acyclic chain, got: %v", err)
	}

	// Verify the engine actually executed (DerivedWriter was called).
	if dw.calls == 0 {
		t.Error("expected at least 1 UpdateDerived call, got 0")
	}
}
