package analytics

import (
	"context"
	"errors"
	"strings"
	"testing"

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

// ---------------------------------------------------------------------------
// Test 1 — Empty input
// ---------------------------------------------------------------------------

// TestOrderForBuild_EmptyInput verifies that OrderForBuild(ctx, nil) returns
// (nil, nil) — no error, no panic, no lexical fallback.
func TestOrderForBuild_EmptyInput(t *testing.T) {
	ctx := context.Background()
	svc := &FormulaViewService{
		metadata: newFakeMetadata(map[string]*models.ObjectMetadata{}),
	}

	got, err := svc.OrderForBuild(ctx, nil)
	if err != nil {
		t.Fatalf("OrderForBuild(nil): unexpected error: %v", err)
	}
	if len(got) != 0 {
		t.Errorf("OrderForBuild(nil) = %v, want nil/empty", got)
	}
}

// ---------------------------------------------------------------------------
// Test 2 — Linear chain
// ---------------------------------------------------------------------------

// TestOrderForBuild_LinearChain verifies that A→B→C produces [C, B, A]:
// each object appears after every object it depends on.
//
// Field-level dep semantics: each cross-object read must target an
// active-computed field on the target to establish a dep edge.
// ObjA reads ObjB.ComputedLabel__c (active-computed) → dep ObjA→ObjB.
// ObjB reads ObjC.ComputedLabel__c (active-computed) → dep ObjB→ObjC.
// Expected build order: ObjC, ObjB, ObjA.
func TestOrderForBuild_LinearChain(t *testing.T) {
	ctx := context.Background()
	mode := "active"

	svc := &FormulaViewService{
		metadata: newFakeMetadata(map[string]*models.ObjectMetadata{
			"ObjA": {
				APIName: "ObjA",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					makeFKField("BId", "B", "ObjB"),
					{
						// Reads ComputedLabel__c — active-computed on ObjB → dep A→B.
						APIName:     "BMirror__c",
						Type:        constants.FieldTypeFormula,
						Formula:     depsTestStrPtr("B.ComputedLabel__c"),
						FormulaMode: &mode,
						ReturnType:  depsTestFieldType(constants.FieldTypeString),
					},
				},
			},
			"ObjB": {
				APIName: "ObjB",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					makeFKField("CId", "C", "ObjC"),
					{
						// ObjB's own active-computed field (makes v_objb exist).
						APIName:     "ComputedLabel__c",
						Type:        constants.FieldTypeFormula,
						Formula:     depsTestStrPtr("Name"),
						FormulaMode: &mode,
						ReturnType:  depsTestFieldType(constants.FieldTypeString),
					},
					{
						// Reads ComputedLabel__c — active-computed on ObjC → dep B→C.
						APIName:     "CMirror__c",
						Type:        constants.FieldTypeFormula,
						Formula:     depsTestStrPtr("C.ComputedLabel__c"),
						FormulaMode: &mode,
						ReturnType:  depsTestFieldType(constants.FieldTypeString),
					},
				},
			},
			"ObjC": {
				APIName: "ObjC",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					{
						APIName:     "ComputedLabel__c",
						Type:        constants.FieldTypeFormula,
						Formula:     depsTestStrPtr("Name"),
						FormulaMode: &mode,
						ReturnType:  depsTestFieldType(constants.FieldTypeString),
					},
				},
			},
		}),
	}

	got, err := svc.OrderForBuild(ctx, []string{"ObjA", "ObjB", "ObjC"})
	if err != nil {
		t.Fatalf("OrderForBuild: unexpected error: %v", err)
	}
	if len(got) != 3 {
		t.Fatalf("OrderForBuild: got %d items, want 3: %v", len(got), got)
	}

	posA, posB, posC := -1, -1, -1
	for i, name := range got {
		switch name {
		case "ObjA":
			posA = i
		case "ObjB":
			posB = i
		case "ObjC":
			posC = i
		}
	}
	if posC >= posB || posB >= posA {
		t.Errorf("OrderForBuild order = %v, want ObjC before ObjB before ObjA", got)
	}
}

// ---------------------------------------------------------------------------
// Test 3 — Lexical determinism
// ---------------------------------------------------------------------------

// TestOrderForBuild_LexicalDeterminism verifies that two independent objects
// (neither depends on the other) are always returned in lexical order regardless
// of input order.
func TestOrderForBuild_LexicalDeterminism(t *testing.T) {
	ctx := context.Background()
	mode := "active"

	mkIndependent := func(name string) *models.ObjectMetadata {
		return &models.ObjectMetadata{
			APIName: name,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: constants.FieldTypeString},
				{
					APIName:     "Computed__c",
					Type:        constants.FieldTypeFormula,
					Formula:     depsTestStrPtr("Name"),
					FormulaMode: &mode,
					ReturnType:  depsTestFieldType(constants.FieldTypeString),
				},
			},
		}
	}

	svc := &FormulaViewService{
		metadata: newFakeMetadata(map[string]*models.ObjectMetadata{
			"Alpha": mkIndependent("Alpha"),
			"Beta":  mkIndependent("Beta"),
		}),
	}

	// Input in one order.
	got1, err := svc.OrderForBuild(ctx, []string{"Beta", "Alpha"})
	if err != nil {
		t.Fatalf("OrderForBuild: unexpected error: %v", err)
	}
	// Input in reverse order.
	got2, err := svc.OrderForBuild(ctx, []string{"Alpha", "Beta"})
	if err != nil {
		t.Fatalf("OrderForBuild: unexpected error: %v", err)
	}

	if strings.Join(got1, ",") != strings.Join(got2, ",") {
		t.Errorf("OrderForBuild not deterministic: %v vs %v", got1, got2)
	}
	// Both should be lexical: Alpha before Beta.
	if len(got1) != 2 || got1[0] != "Alpha" || got1[1] != "Beta" {
		t.Errorf("OrderForBuild = %v, want [Alpha Beta]", got1)
	}
}

// ---------------------------------------------------------------------------
// Test 4 — Cycle returns named error, no lexical fallback
// ---------------------------------------------------------------------------

// TestOrderForBuild_CycleReturnsNamedError verifies that a circular dependency
// (A depends on B, B depends on A) returns a named "circular derived-view
// dependency" error and does NOT fall back to a lexical ordering.
// Per CLAUDE.md Forbidden Move #5: NO lexical/pack-payload/raw-schema fallback.
func TestOrderForBuild_CycleReturnsNamedError(t *testing.T) {
	ctx := context.Background()
	mode := "active"

	svc := &FormulaViewService{
		metadata: newFakeMetadata(map[string]*models.ObjectMetadata{
			"CycleA": {
				APIName: "CycleA",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					makeFKField("BId", "B", "CycleB"),
					{
						APIName:     "BField__c",
						Type:        constants.FieldTypeFormula,
						Formula:     depsTestStrPtr("B.ComputedX__c"),
						FormulaMode: &mode,
						ReturnType:  depsTestFieldType(constants.FieldTypeString),
					},
				},
			},
			"CycleB": {
				APIName: "CycleB",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					makeFKField("AId", "A", "CycleA"),
					// CycleB's active formula references CycleA's active computed field.
					{
						APIName:     "ComputedX__c",
						Type:        constants.FieldTypeFormula,
						Formula:     depsTestStrPtr("A.BField__c"),
						FormulaMode: &mode,
						ReturnType:  depsTestFieldType(constants.FieldTypeString),
					},
				},
			},
		}),
	}

	result, err := svc.OrderForBuild(ctx, []string{"CycleA", "CycleB"})
	if err == nil {
		t.Fatalf("OrderForBuild(cycle): expected error, got result = %v (no lexical fallback permitted)", result)
	}
	// Must name both parties in the cycle.
	errMsg := err.Error()
	if !strings.Contains(errMsg, "CycleA") || !strings.Contains(errMsg, "CycleB") {
		t.Errorf("OrderForBuild cycle error = %q, want both CycleA and CycleB in message", errMsg)
	}
	if !strings.Contains(strings.ToLower(errMsg), "circular") {
		t.Errorf("OrderForBuild cycle error = %q, want 'circular' in message", errMsg)
	}
	// Assert no slice was returned — error path must not produce a partial ordering.
	if result != nil {
		t.Errorf("OrderForBuild(cycle): got non-nil result %v alongside error; want nil result", result)
	}
}

// TestOrderForBuild_BackReferenceActiveFormulaCycleReturnsNamedError locks the
// exact #1368 shape on current main: a child active formula reads
// Parent__r.B__c while the parent active formula reads Child__r.A__c back
// through the inverse relationship. Flow-visible active fields are projected
// through the formula-view substrate, so this ordering/cycle detector is the
// authoritative guard that must fail closed before any runtime flow path could
// recurse.
func TestOrderForBuild_BackReferenceActiveFormulaCycleReturnsNamedError(t *testing.T) {
	ctx := context.Background()
	mode := "active"

	svc := &FormulaViewService{
		metadata: newFakeMetadata(map[string]*models.ObjectMetadata{
			"Child__c": {
				APIName: "Child__c",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					makeFKField("Parent__c", "Parent__r", "Parent__c"),
					{
						APIName:     "A__c",
						Type:        constants.FieldTypeFormula,
						Formula:     depsTestStrPtr("Parent__r.B__c"),
						FormulaMode: &mode,
						ReturnType:  depsTestFieldType(constants.FieldTypeString),
					},
				},
			},
			"Parent__c": {
				APIName: "Parent__c",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					makeFKField("Child__c", "Child__r", "Child__c"),
					{
						APIName:     "B__c",
						Type:        constants.FieldTypeFormula,
						Formula:     depsTestStrPtr("Child__r.A__c"),
						FormulaMode: &mode,
						ReturnType:  depsTestFieldType(constants.FieldTypeString),
					},
				},
			},
		}),
	}

	result, err := svc.OrderForBuild(ctx, []string{"Child__c", "Parent__c"})
	if err == nil {
		t.Fatalf("OrderForBuild(back-reference cycle): expected error, got result = %v", result)
	}
	if result != nil {
		t.Errorf("OrderForBuild(back-reference cycle): got non-nil result %v alongside error; want nil result", result)
	}
	errMsg := err.Error()
	if !strings.Contains(errMsg, "Child__c") || !strings.Contains(errMsg, "Parent__c") {
		t.Errorf("OrderForBuild back-reference cycle error = %q, want both Child__c and Parent__c named", errMsg)
	}
	if !strings.Contains(strings.ToLower(errMsg), "circular") {
		t.Errorf("OrderForBuild back-reference cycle error = %q, want 'circular' in message", errMsg)
	}
}

// ---------------------------------------------------------------------------
// Test 5 — Cold-start: missing metadata returns ErrFormulaViewMetadataNotReady
// ---------------------------------------------------------------------------

// TestOrderForBuild_MissingMetadataReturnsTypedError verifies that when a
// candidate references a missing object, OrderForBuild returns
// ErrFormulaViewMetadataNotReady (propagated from DerivedViewDependencies).
func TestOrderForBuild_MissingMetadataReturnsTypedError(t *testing.T) {
	ctx := context.Background()

	// Schema map is empty — no object schemas available.
	svc := &FormulaViewService{
		metadata: newFakeMetadata(map[string]*models.ObjectMetadata{}),
	}

	_, err := svc.OrderForBuild(ctx, []string{"Ghost"})
	if err == nil {
		t.Fatal("OrderForBuild([Ghost]): expected ErrFormulaViewMetadataNotReady, got nil")
	}
	if !errors.Is(err, ErrFormulaViewMetadataNotReady) {
		t.Errorf("OrderForBuild([Ghost]): got %v, want errors.Is(err, ErrFormulaViewMetadataNotReady) = true", err)
	}
}

// ---------------------------------------------------------------------------
// Test 6 — Out-of-set dependency: object is ordered correctly
// ---------------------------------------------------------------------------

// TestOrderForBuild_OutOfSetDependencyIgnored verifies that when object A
// depends on object B (via active formula), but B is not in the candidate set,
// A is still ordered correctly (at position 0 since it has no in-set dependencies).
func TestOrderForBuild_OutOfSetDependencyIgnored(t *testing.T) {
	ctx := context.Background()
	mode := "active"

	svc := &FormulaViewService{
		metadata: newFakeMetadata(map[string]*models.ObjectMetadata{
			"ObjA": {
				APIName: "ObjA",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					makeFKField("ExtId", "Ext", "ExtObj"),
					// A's formula references ExtObj which is NOT in the candidate set.
					{
						APIName:     "ExtName__c",
						Type:        constants.FieldTypeFormula,
						Formula:     depsTestStrPtr("Ext.Name"),
						FormulaMode: &mode,
						ReturnType:  depsTestFieldType(constants.FieldTypeString),
					},
				},
			},
			// ExtObj exists in metadata (so GetSchema won't fail) and has active fields,
			// but it is not in the OrderForBuild candidate list.
			"ExtObj": {
				APIName: "ExtObj",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					{
						APIName:     "Computed__c",
						Type:        constants.FieldTypeFormula,
						Formula:     depsTestStrPtr("Name"),
						FormulaMode: &mode,
						ReturnType:  depsTestFieldType(constants.FieldTypeString),
					},
				},
			},
		}),
	}

	// Only ObjA is in the candidate set; ExtObj is not.
	got, err := svc.OrderForBuild(ctx, []string{"ObjA"})
	if err != nil {
		t.Fatalf("OrderForBuild([ObjA]): unexpected error: %v", err)
	}
	if len(got) != 1 || got[0] != "ObjA" {
		t.Errorf("OrderForBuild([ObjA]) = %v, want [ObjA]", got)
	}
}
