package analytics

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

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

// ---------------------------------------------------------------------------
// Test helpers local to this file
// ---------------------------------------------------------------------------

func depsTestStrPtr(v string) *string { return &v }

func depsTestFieldType(v constants.SchemaFieldType) *models.FieldType {
	ft := models.FieldType(v)
	return &ft
}

// makeFormulaField builds a formula-mode-active FieldMetadata for test fixtures.
// Only formula-backed active fields are used (no ProjectionConfig-only fields).
// ProjectionConfig-only active fields are intentionally excluded from the
// equivalence test because the pack-side resolver (objectSpecFieldHasActiveFormula
// at source_backed_pack.go:471) only walks fields with non-empty Formula strings,
// while the live runtime isActiveComputedField also accepts ProjectionConfig != nil.
// Mixing projection-config fixtures would produce false BLOCKs unrelated to the
// dependency algorithm. See design doc R2-4.
func makeFormulaField(apiName, formula string) models.FieldMetadata {
	mode := "active"
	return models.FieldMetadata{
		APIName:     apiName,
		Type:        constants.FieldTypeFormula,
		Formula:     depsTestStrPtr(formula),
		FormulaMode: &mode,
		ReturnType:  depsTestFieldType(constants.FieldTypeString),
	}
}

func makeFKField(apiName, relationshipName, refTo string) models.FieldMetadata {
	return models.FieldMetadata{
		APIName:          apiName,
		Type:             constants.FieldTypeReference,
		ReferenceTo:      []string{refTo},
		RelationshipName: depsTestStrPtr(relationshipName),
	}
}

func makeRollupSummaryField(apiName, summaryObject, summaryField, relationshipField, calcType string) models.FieldMetadata {
	return models.FieldMetadata{
		APIName: apiName,
		Type:    constants.FieldTypeRollupSummary,
		RollupConfig: &models.RollupConfig{
			SummaryObject:     summaryObject,
			SummaryField:      summaryField,
			RelationshipField: relationshipField,
			CalcType:          calcType,
		},
	}
}

// ---------------------------------------------------------------------------
// Test 1 — Field-level dependency: A reads active-computed field on B → dep
// ---------------------------------------------------------------------------

// TestDerivedViewDependencies_FieldLevelDependency verifies that DerivedViewDependencies
// creates a dependency on B only when the SPECIFIC field read on B is active-computed.
//
// The live-metadata resolver uses field-level filtering (CI Codex finding): the
// dependency on v_B arises only when A reads a column that lives in v_B (i.e., an
// active-computed field). This intentionally diverges from the pack-side algorithm
// (objectSpecDerivedViewDependencies in source_backed_pack.go) which uses object-level
// filtering for metadata registration order. The two algorithms serve different purposes
// and are not required to agree.
//
// Sub-test A: A reads ObjB.ComputedLabel__c (active-computed) → dep on ObjB.
// Sub-test B: A reads ObjB.Name (plain string, not active-computed) → NO dep on ObjB.
func TestDerivedViewDependencies_FieldLevelDependency(t *testing.T) {
	ctx := context.Background()
	mode := "active"

	objBWithComputedLabel := &models.ObjectMetadata{
		APIName: "ObjB",
		Fields: []models.FieldMetadata{
			{APIName: "Id", Type: constants.FieldTypeString},
			{APIName: "Name", Type: constants.FieldTypeString},
			// ComputedLabel__c is active-computed — lives in v_objb.
			{
				APIName:     "ComputedLabel__c",
				Type:        constants.FieldTypeFormula,
				Formula:     depsTestStrPtr("Name"),
				FormulaMode: &mode,
				ReturnType:  depsTestFieldType(constants.FieldTypeString),
			},
		},
	}

	t.Run("reads active-computed field → dep", func(t *testing.T) {
		svc := &FormulaViewService{
			metadata: newFakeMetadata(map[string]*models.ObjectMetadata{
				"ObjA": {
					APIName: "ObjA",
					Fields: []models.FieldMetadata{
						{APIName: "Id", Type: constants.FieldTypeString},
						makeFKField("ParentId", "Parent", "ObjB"),
						// Reads ComputedLabel__c — active-computed on ObjB → dep expected.
						makeFormulaField("MirroredLabel__c", "Parent.ComputedLabel__c"),
					},
				},
				"ObjB": objBWithComputedLabel,
			}),
		}

		got, err := svc.DerivedViewDependencies(ctx, "ObjA")
		if err != nil {
			t.Fatalf("DerivedViewDependencies(ObjA): unexpected error: %v", err)
		}
		sort.Strings(got)
		want := []string{"ObjB"}
		if strings.Join(got, ",") != strings.Join(want, ",") {
			t.Errorf("DerivedViewDependencies(ObjA) = %v, want %v (read active-computed field)", got, want)
		}
	})

	t.Run("reads physical field only → no dep", func(t *testing.T) {
		svc := &FormulaViewService{
			metadata: newFakeMetadata(map[string]*models.ObjectMetadata{
				"ObjA": {
					APIName: "ObjA",
					Fields: []models.FieldMetadata{
						{APIName: "Id", Type: constants.FieldTypeString},
						makeFKField("ParentId", "Parent", "ObjB"),
						// Reads Name — plain physical field on ObjB → no dep.
						makeFormulaField("ComputedName__c", "Parent.Name"),
					},
				},
				"ObjB": objBWithComputedLabel,
			}),
		}

		got, err := svc.DerivedViewDependencies(ctx, "ObjA")
		if err != nil {
			t.Fatalf("DerivedViewDependencies(ObjA): unexpected error: %v", err)
		}
		if len(got) != 0 {
			t.Errorf("DerivedViewDependencies(ObjA) = %v, want empty (read physical field Name, not active-computed)", got)
		}
	})
}

// ---------------------------------------------------------------------------
// Test 2 — Stored rollupsummary references do NOT create child view dependencies
// ---------------------------------------------------------------------------

// TestDerivedViewDependencies_RollupSummaryReferenceNoDepEdge verifies that an
// active formula which references a stored rollupsummary field does NOT include
// the child object in DerivedViewDependencies.
//
// Rationale: the supported contract is "rollupsummary stores the parent value,
// active formula reads that stored parent column". The formula view therefore
// has no reason to depend on `v_<child>` even when the child object itself has
// active-computed fields.
func TestDerivedViewDependencies_RollupSummaryReferenceNoDepEdge(t *testing.T) {
	ctx := context.Background()
	mode := "active"

	svc := &FormulaViewService{
		metadata: newFakeMetadata(map[string]*models.ObjectMetadata{
			"Parent": {
				APIName: "Parent",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					makeRollupSummaryField("StoredTotal__c", "Child", "Amount", "ParentId", "SUM"),
					makeFormulaField("ProjectedTotal__c", "StoredTotal__c"),
				},
			},
			"Child": {
				APIName: "Child",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					{APIName: "Amount", Type: constants.FieldTypeCurrency},
					makeFKField("ParentId", "Parent", "Parent"),
					// Child still has an active formula and would have a v_child view.
					{
						APIName:     "ComputedAmt__c",
						Type:        constants.FieldTypeFormula,
						Formula:     depsTestStrPtr("Amount * 2"),
						FormulaMode: &mode,
						ReturnType:  depsTestFieldType(constants.FieldTypeCurrency),
					},
				},
			},
		}),
	}

	got, err := svc.DerivedViewDependencies(ctx, "Parent")
	if err != nil {
		t.Fatalf("DerivedViewDependencies(Parent): unexpected error: %v", err)
	}

	for _, dep := range got {
		if strings.EqualFold(dep, "Child") {
			t.Errorf("DerivedViewDependencies(Parent) = %v, want Child NOT in result (stored rollupsummary reference reads parent column, not v_child)", got)
		}
	}
}

// ---------------------------------------------------------------------------
// Test 3 — Self-edge exclusion
// ---------------------------------------------------------------------------

// TestDerivedViewDependencies_SelfEdgeExclusion verifies that when an object's
// FK points back to itself (self-referential), the object does NOT appear in
// its own DerivedViewDependencies.
func TestDerivedViewDependencies_SelfEdgeExclusion(t *testing.T) {
	ctx := context.Background()
	mode := "active"

	svc := &FormulaViewService{
		metadata: newFakeMetadata(map[string]*models.ObjectMetadata{
			"TreeNode": {
				APIName: "TreeNode",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					// Self-referential FK: ParentId → TreeNode
					makeFKField("ParentId", "Parent", "TreeNode"),
					// Formula reads through self-ref join
					{
						APIName:     "ParentName__c",
						Type:        constants.FieldTypeFormula,
						Formula:     depsTestStrPtr("Parent.Name"),
						FormulaMode: &mode,
						ReturnType:  depsTestFieldType(constants.FieldTypeString),
					},
				},
			},
		}),
	}

	got, err := svc.DerivedViewDependencies(ctx, "TreeNode")
	if err != nil {
		t.Fatalf("DerivedViewDependencies(TreeNode): unexpected error: %v", err)
	}

	for _, dep := range got {
		if strings.EqualFold(dep, "TreeNode") {
			t.Errorf("DerivedViewDependencies(TreeNode) = %v, want TreeNode NOT in result (self-edge excluded)", got)
		}
	}
}

// ---------------------------------------------------------------------------
// Test 4 — Active-view-membership filter (R3-F2)
// ---------------------------------------------------------------------------

// TestDerivedViewDependencies_BaseOnlyReferenceSkipped verifies that when object
// A's formula references object B, but the SPECIFIC field read on B is a plain
// physical field (not active-computed), B is NOT included in A's
// DerivedViewDependencies and no error is returned.
//
// Per R3-F2 field-level semantics: A dependency on v_B arises only when the
// field being read exists in v_B (i.e., is active-computed on B). Reading a
// physical column — even on an object that has OTHER active-computed fields —
// does not require v_B to be built first.
func TestDerivedViewDependencies_BaseOnlyReferenceSkipped(t *testing.T) {
	ctx := context.Background()

	svc := &FormulaViewService{
		metadata: newFakeMetadata(map[string]*models.ObjectMetadata{
			"ObjA": {
				APIName: "ObjA",
				Fields: []models.FieldMetadata{
					{APIName: "Id", Type: constants.FieldTypeString},
					makeFKField("OwnerId", "Owner", "ObjB"),
					makeFormulaField("OwnerName__c", "Owner.Name"),
				},
			},
			"ObjB": {
				APIName: "ObjB",
				Fields: []models.FieldMetadata{
					// ObjB has NO active computed fields — base-only object.
					{APIName: "Id", Type: constants.FieldTypeString},
					{APIName: "Name", Type: constants.FieldTypeString},
				},
			},
		}),
	}

	got, err := svc.DerivedViewDependencies(ctx, "ObjA")
	if err != nil {
		t.Fatalf("DerivedViewDependencies(ObjA): unexpected error: %v", err)
	}
	if len(got) != 0 {
		t.Errorf("DerivedViewDependencies(ObjA) = %v, want empty (ObjB is base-only, no v_objb view)", got)
	}
}

// ---------------------------------------------------------------------------
// Test 5 — Missing metadata returns ErrFormulaViewMetadataNotReady
// ---------------------------------------------------------------------------

// TestDerivedViewDependencies_MissingSchemaReturnsTypedError verifies that when
// the target object's schema cannot be loaded, DerivedViewDependencies returns
// ErrFormulaViewMetadataNotReady (not a nil result or a generic error).
func TestDerivedViewDependencies_MissingSchemaReturnsTypedError(t *testing.T) {
	ctx := context.Background()

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

	_, err := svc.DerivedViewDependencies(ctx, "NonExistent")
	if err == nil {
		t.Fatal("DerivedViewDependencies(NonExistent): expected ErrFormulaViewMetadataNotReady, got nil")
	}
	if !errors.Is(err, ErrFormulaViewMetadataNotReady) {
		t.Errorf("DerivedViewDependencies(NonExistent): got error %v, want errors.Is(err, ErrFormulaViewMetadataNotReady) = true", err)
	}
}
