package formula

// relation_resolver_test.go — table-driven unit tests for the shared
// relation-name → FK column → fetch helper that records/ and automation/
// both consume.
//
// CLAUDE.md R10: "multiple read paths is the defect even when answers
// currently agree." This file locks in that the two callers wired through
// formula.FetchRelatedRecord agree on the same 3-value contract for every
// relevant input class:
//
//   - success                    → (related, target, true)
//   - definitive null            → (nil,     "",     true)
//   - indeterminate / repo error → (nil,     target, false)

import (
	"context"
	"errors"
	"testing"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// --- minimal RelationFetchDeps mocks --------------------------------------

// stubDeps is a hand-written RelationFetchDeps that lets each test pin the
// ReferenceTo lookup result and the FetchByID result. Keeping it explicit
// (rather than reaching for a generic mock framework) makes the test cases
// trivially readable.
type stubDeps struct {
	// referenceTo maps fkColumn → ReferenceTo slice; absent key → empty slice.
	referenceTo map[string][]string
	// records maps target object → id → SObject; missing → (nil, nil); used
	// to simulate orphan FK and successful fetches.
	records map[string]map[string]models.SObject
	// fetchErr, when non-nil, is returned for every FetchByID call regardless
	// of (target, id). Lets a test simulate a transient repo error.
	fetchErr error
	// fetchCalls counts FetchByID invocations for cache-equivalence assertions.
	fetchCalls int
}

func (s *stubDeps) LookupReferenceTo(_ string, fkColumn string) []string {
	return s.referenceTo[fkColumn]
}

func (s *stubDeps) FetchByID(_ context.Context, target, id string) (map[string]interface{}, error) {
	s.fetchCalls++
	if s.fetchErr != nil {
		return nil, s.fetchErr
	}
	if t, ok := s.records[target]; ok {
		if r, ok := t[id]; ok {
			return map[string]interface{}(r), nil
		}
	}
	return nil, nil
}

// --- ResolveRelationFK — table-driven tests --------------------------------

func TestResolveRelationFK_Tier1_RelationshipNameMatch(t *testing.T) {
	relName := "Acct"
	schema := &models.ObjectMetadata{
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, RelationshipName: &relName},
		},
	}
	// Tier 1 (RelationshipName) wins even though "Acct" would not match
	// any other tier — the FK is the field's APIName.
	assert.Equal(t, "AccountId", ResolveRelationFK(schema, "Acct"))
}

func TestResolveRelationFK_Tier2_CustomSuffix(t *testing.T) {
	// __r → __c is a Salesforce naming convention; works without metadata.
	assert.Equal(t, "Foo__c", ResolveRelationFK(&models.ObjectMetadata{}, "Foo__r"))
	// nil schema is also OK for the convention path.
	assert.Equal(t, "Bar__c", ResolveRelationFK(nil, "Bar__r"))
}

func TestResolveRelationFK_Tier3_StandardRelationshipTable(t *testing.T) {
	// Bare standard names with irregular FKs come from the helper's own
	// standard-relationship table — single source for both callers.
	assert.Equal(t, "AccountId", ResolveRelationFK(nil, "Account"))
	assert.Equal(t, "RecordTypeId", ResolveRelationFK(nil, "RecordType"))
	assert.Equal(t, "OwnerId", ResolveRelationFK(nil, "Owner"))
	assert.Equal(t, "CreatedById", ResolveRelationFK(nil, "CreatedBy"))
}

func TestResolveRelationFK_Tier4_SalesforceConvention(t *testing.T) {
	// Falls back to "<segment>Id" for anything not matched above.
	assert.Equal(t, "WidgetId", ResolveRelationFK(&models.ObjectMetadata{}, "Widget"))
}

func TestResolveRelationFK_EmptySegment(t *testing.T) {
	assert.Equal(t, "", ResolveRelationFK(nil, ""))
	assert.Equal(t, "", ResolveRelationFK(&models.ObjectMetadata{}, ""))
}

func TestResolveRelationFK_TierOrdering_RelationshipNameBeatsCustomSuffix(t *testing.T) {
	// Field has RelationshipName "Parent__r" and APIName "ParentId" — Tier 1
	// must beat Tier 2 (which would otherwise return "Parent__c", a non-
	// existent column on this schema).
	relName := "Parent__r"
	schema := &models.ObjectMetadata{
		Fields: []models.FieldMetadata{
			{APIName: "ParentId", Type: constants.FieldTypeReference, RelationshipName: &relName},
		},
	}
	assert.Equal(t, "ParentId", ResolveRelationFK(schema, "Parent__r"))
}

// --- FetchRelatedRecord — 3-value contract --------------------------------

func TestFetchRelatedRecord_Success(t *testing.T) {
	relName := "Parent__r"
	schema := &models.ObjectMetadata{
		APIName: "Child__c",
		Fields: []models.FieldMetadata{
			{APIName: "ParentId", Type: constants.FieldTypeReference, RelationshipName: &relName, ReferenceTo: []string{"Parent__c"}},
		},
	}
	deps := &stubDeps{
		referenceTo: map[string][]string{"ParentId": {"Parent__c"}},
		records: map[string]map[string]models.SObject{
			"Parent__c": {"par-1": {"Id": "par-1", "Code": "PAR"}},
		},
	}
	rec := map[string]interface{}{"ParentId": "par-1"}

	related, target, resolved := FetchRelatedRecord(context.Background(), deps, schema, "Child__c", rec, "Parent__r")

	assert.True(t, resolved)
	assert.Equal(t, "Parent__c", target)
	require.NotNil(t, related)
	assert.Equal(t, "PAR", related["Code"])
}

func TestFetchRelatedRecord_DefinitiveNull_FKAbsent(t *testing.T) {
	relName := "Parent__r"
	schema := &models.ObjectMetadata{
		APIName: "Child__c",
		Fields: []models.FieldMetadata{
			{APIName: "ParentId", Type: constants.FieldTypeReference, RelationshipName: &relName, ReferenceTo: []string{"Parent__c"}},
		},
	}
	deps := &stubDeps{}
	rec := map[string]interface{}{} // ParentId absent

	related, target, resolved := FetchRelatedRecord(context.Background(), deps, schema, "Child__c", rec, "Parent__r")

	assert.True(t, resolved, "FK absent is a definitive null per SF semantics")
	assert.Equal(t, "", target)
	assert.Nil(t, related)
	assert.Equal(t, 0, deps.fetchCalls, "FK absent must not hit the repo")
}

func TestFetchRelatedRecord_DefinitiveNull_FKEmptyString(t *testing.T) {
	relName := "Parent__r"
	schema := &models.ObjectMetadata{
		APIName: "Child__c",
		Fields: []models.FieldMetadata{
			{APIName: "ParentId", Type: constants.FieldTypeReference, RelationshipName: &relName, ReferenceTo: []string{"Parent__c"}},
		},
	}
	deps := &stubDeps{}
	rec := map[string]interface{}{"ParentId": ""}

	related, target, resolved := FetchRelatedRecord(context.Background(), deps, schema, "Child__c", rec, "Parent__r")

	assert.True(t, resolved)
	assert.Equal(t, "", target)
	assert.Nil(t, related)
	assert.Equal(t, 0, deps.fetchCalls)
}

func TestFetchRelatedRecord_DefinitiveNull_OrphanFK(t *testing.T) {
	// FK present but FetchByID returns (nil, nil) — orphan parent. Per SF
	// semantics this is a definitive null, NOT an indeterminate.
	relName := "Parent__r"
	schema := &models.ObjectMetadata{
		APIName: "Child__c",
		Fields: []models.FieldMetadata{
			{APIName: "ParentId", Type: constants.FieldTypeReference, RelationshipName: &relName, ReferenceTo: []string{"Parent__c"}},
		},
	}
	deps := &stubDeps{
		referenceTo: map[string][]string{"ParentId": {"Parent__c"}},
		records:     map[string]map[string]models.SObject{}, // empty → orphan
	}
	rec := map[string]interface{}{"ParentId": "missing-id"}

	related, target, resolved := FetchRelatedRecord(context.Background(), deps, schema, "Child__c", rec, "Parent__r")

	assert.True(t, resolved, "orphan FK is a definitive null, not indeterminate")
	assert.Equal(t, "Parent__c", target)
	assert.Nil(t, related)
	assert.Equal(t, 1, deps.fetchCalls)
}

func TestFetchRelatedRecord_Indeterminate_RepoError(t *testing.T) {
	// FetchByID returns a transient error → indeterminate.
	relName := "Parent__r"
	schema := &models.ObjectMetadata{
		APIName: "Child__c",
		Fields: []models.FieldMetadata{
			{APIName: "ParentId", Type: constants.FieldTypeReference, RelationshipName: &relName, ReferenceTo: []string{"Parent__c"}},
		},
	}
	deps := &stubDeps{
		referenceTo: map[string][]string{"ParentId": {"Parent__c"}},
		fetchErr:    errors.New("db connection refused"),
	}
	rec := map[string]interface{}{"ParentId": "par-x"}

	related, target, resolved := FetchRelatedRecord(context.Background(), deps, schema, "Child__c", rec, "Parent__r")

	assert.False(t, resolved, "transient repo error must be indeterminate")
	assert.Equal(t, "Parent__c", target)
	assert.Nil(t, related)
}

func TestFetchRelatedRecord_Indeterminate_NoReferenceTo(t *testing.T) {
	// FK present, but the FK field has no ReferenceTo metadata (cache miss).
	// Cannot pick a target object → indeterminate.
	relName := "Parent__r"
	schema := &models.ObjectMetadata{
		APIName: "Child__c",
		Fields: []models.FieldMetadata{
			{APIName: "ParentId", Type: constants.FieldTypeReference, RelationshipName: &relName, ReferenceTo: nil},
		},
	}
	deps := &stubDeps{} // referenceTo map empty → returns nil
	rec := map[string]interface{}{"ParentId": "par-x"}

	related, target, resolved := FetchRelatedRecord(context.Background(), deps, schema, "Child__c", rec, "Parent__r")

	assert.False(t, resolved, "missing ReferenceTo metadata is indeterminate")
	assert.Equal(t, "", target, "no target can be resolved without ReferenceTo")
	assert.Nil(t, related)
	assert.Equal(t, 0, deps.fetchCalls, "indeterminate-no-target must not hit the repo")
}

func TestFetchRelatedRecord_DefinitiveNull_UnknownRelationName(t *testing.T) {
	// Unknown relation name → ResolveRelationFK returns "" → silent skip per
	// the contract automation/buildFormulaFetcher already locks in.
	schema := &models.ObjectMetadata{
		APIName: "Child__c",
		Fields:  []models.FieldMetadata{{APIName: "Name", Type: constants.FieldTypeString}},
	}
	deps := &stubDeps{}
	rec := map[string]interface{}{"Name": "irrelevant"}

	related, target, resolved := FetchRelatedRecord(context.Background(), deps, schema, "Child__c", rec, "Sibling")

	assert.True(t, resolved, "unknown relation name is definitive null (silent skip)")
	assert.Equal(t, "", target)
	assert.Nil(t, related)
	assert.Equal(t, 0, deps.fetchCalls)
}

// --- RecordType — both adapter shapes converge -----------------------------

// TestFetchRelatedRecord_RecordType_CrossAdapterEquivalence is the load-bearing
// R10 cross-caller test: feed identical inputs through two RelationFetchDeps
// implementations that mimic records' adapter (uses FK field metadata) and
// automation's adapter (also uses FK field metadata via fieldMetaLookup) and
// assert the helper produces identical results for a `RecordType` segment.
func TestFetchRelatedRecord_RecordType_CrossAdapterEquivalence(t *testing.T) {
	rtRel := "RecordType"
	schema := &models.ObjectMetadata{
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "RecordTypeId", Type: constants.FieldTypeReference, RelationshipName: &rtRel, ReferenceTo: []string{"RecordType"}},
		},
	}
	rtRow := models.SObject{
		"Id":              "rt-1",
		"DeveloperName":   "Standard_Account",
		"Name":            "Standard Account",
		"SobjectType":     "Account",
	}

	// Records-style adapter: ReferenceTo from schema, FetchByID via "repo".
	recordsDeps := &stubDeps{
		referenceTo: map[string][]string{"RecordTypeId": {"RecordType"}},
		records:     map[string]map[string]models.SObject{"RecordType": {"rt-1": rtRow}},
	}
	// Automation-style adapter: same shape (in production, automation reads
	// the same ReferenceTo via lookupFieldMeta and queries via QueryService).
	automationDeps := &stubDeps{
		referenceTo: map[string][]string{"RecordTypeId": {"RecordType"}},
		records:     map[string]map[string]models.SObject{"RecordType": {"rt-1": rtRow}},
	}
	rec := map[string]interface{}{"RecordTypeId": "rt-1"}

	relA, targetA, resolvedA := FetchRelatedRecord(context.Background(), recordsDeps, schema, "Account", rec, "RecordType")
	relB, targetB, resolvedB := FetchRelatedRecord(context.Background(), automationDeps, schema, "Account", rec, "RecordType")

	// Both adapters produce the same answer for the same input.
	assert.Equal(t, resolvedA, resolvedB)
	assert.Equal(t, targetA, targetB)
	assert.Equal(t, relA, relB)
	// And the answer is "success", target=RecordType, projection includes
	// DeveloperName so cross-object frozen formulas like
	// TEXT($Record.RecordType.DeveloperName) work consistently in both paths.
	assert.True(t, resolvedA)
	assert.Equal(t, "RecordType", targetA)
	require.NotNil(t, relA)
	assert.Equal(t, "Standard_Account", relA["DeveloperName"])
}

// --- ResolveRelationTarget — direct unit tests (PR #2057 round-8) ---------
//
// ResolveRelationTarget is now consumed by THREE call sites (R10 reader
// enumeration):
//   - FetchRelatedRecord (records + automation fetch path)
//   - automation.loadRelatedRecord pre-compute (cache key + attributes.type)
//   - metadata.resolveValidationRelation (validation formula resolver)
//
// These tests pin the contract directly so a regression at the helper level
// is caught before it propagates to any of the three callers. The
// FetchRelatedRecord polymorphic suite below exercises the same gates via
// the integration boundary; both layers are needed because the wrappers
// each have their own call shape.

func TestResolveRelationTarget_NoReferenceTo_Indeterminate(t *testing.T) {
	target, ok := ResolveRelationTarget(map[string]interface{}{}, "AccountId", nil)
	assert.False(t, ok)
	assert.Equal(t, "", target)
}

func TestResolveRelationTarget_SingleReferenceTo_Unambiguous(t *testing.T) {
	target, ok := ResolveRelationTarget(
		map[string]interface{}{"AccountId": "acc-1"},
		"AccountId",
		[]string{"Account"},
	)
	assert.True(t, ok)
	assert.Equal(t, "Account", target)
}

func TestResolveRelationTarget_Polymorphic_DiscriminatorMatch(t *testing.T) {
	// Task.WhoId → [Contact, Lead]; row carries WhoId_type="Lead". The helper
	// must pick Lead, not Contact (refTo[0]). This is the exact bug PR #2057
	// round-8 closed at the validation + automation pre-compute call sites.
	record := map[string]interface{}{
		"WhoId":      "who-1",
		"WhoId_type": "Lead",
	}
	target, ok := ResolveRelationTarget(record, "WhoId", []string{"Contact", "Lead"})
	assert.True(t, ok)
	assert.Equal(t, "Lead", target)
}

func TestResolveRelationTarget_Polymorphic_DiscriminatorMissing_Indeterminate(t *testing.T) {
	record := map[string]interface{}{"WhoId": "who-1"} // no WhoId_type
	target, ok := ResolveRelationTarget(record, "WhoId", []string{"Contact", "Lead"})
	assert.False(t, ok, "missing discriminator MUST be indeterminate, not refTo[0]")
	assert.Equal(t, "", target)
}

func TestResolveRelationTarget_Polymorphic_DiscriminatorEmpty_Indeterminate(t *testing.T) {
	record := map[string]interface{}{"WhoId": "who-1", "WhoId_type": ""}
	target, ok := ResolveRelationTarget(record, "WhoId", []string{"Contact", "Lead"})
	assert.False(t, ok)
	assert.Equal(t, "", target)
}

func TestResolveRelationTarget_Polymorphic_DiscriminatorNonString_Indeterminate(t *testing.T) {
	record := map[string]interface{}{"WhoId": "who-1", "WhoId_type": 42}
	target, ok := ResolveRelationTarget(record, "WhoId", []string{"Contact", "Lead"})
	assert.False(t, ok)
	assert.Equal(t, "", target)
}

func TestResolveRelationTarget_Polymorphic_DiscriminatorNotInReferenceTo_Indeterminate(t *testing.T) {
	// Defense against a stale / legacy / corrupted _type value: the
	// discriminator MUST appear in the declared ReferenceTo set; arbitrary
	// strings never count as a target SObject name.
	record := map[string]interface{}{"WhoId": "who-1", "WhoId_type": "Account"}
	target, ok := ResolveRelationTarget(record, "WhoId", []string{"Contact", "Lead"})
	assert.False(t, ok)
	assert.Equal(t, "", target)
}

func TestResolveRelationTarget_Polymorphic_NilRecord_Indeterminate(t *testing.T) {
	target, ok := ResolveRelationTarget(nil, "WhoId", []string{"Contact", "Lead"})
	assert.False(t, ok, "polymorphic resolution without a record cannot disambiguate")
	assert.Equal(t, "", target)
}

func TestResolveRelationTarget_Polymorphic_EmptyFKColumn_Indeterminate(t *testing.T) {
	target, ok := ResolveRelationTarget(map[string]interface{}{}, "", []string{"Contact", "Lead"})
	assert.False(t, ok)
	assert.Equal(t, "", target)
}

// TestFetchRelatedRecord_Polymorphic_TypeColumnSelectsTarget — round-7 F5
// (Codex Review HIGH): a polymorphic FK (e.g. Task.WhoId → [Contact, Lead])
// MUST consult the polymorphic type column ("<FK>" + "_type") to disambiguate
// the actual target SObject. Pre-round-7 the helper picked refTo[0] for ANY
// length, silently querying the wrong table when the row's actual target
// was the second (or later) ReferenceTo entry.
func TestFetchRelatedRecord_Polymorphic_TypeColumnSelectsTarget(t *testing.T) {
	deps := &stubDeps{
		referenceTo: map[string][]string{
			// Polymorphic: Task.WhoId references either Contact or Lead.
			"WhoId": {"Contact", "Lead"},
		},
		records: map[string]map[string]models.SObject{
			"Lead": {"who-001": models.SObject{"Id": "who-001", "Name": "Lead-Y"}},
		},
	}
	relName := "Who"
	schema := &models.ObjectMetadata{
		APIName: "Task",
		Fields: []models.FieldMetadata{
			{APIName: "WhoId", Type: constants.FieldTypeReference, RelationshipName: &relName, ReferenceTo: []string{"Contact", "Lead"}},
		},
	}
	// Row's WhoId points at "who-001" AND its WhoId_type column says "Lead".
	// The helper MUST honor _type and query Lead, not Contact (refTo[0]).
	record := map[string]interface{}{
		"Id":         "task-1",
		"WhoId":      "who-001",
		"WhoId_type": "Lead",
	}

	related, target, resolved := FetchRelatedRecord(context.Background(), deps, schema, "Task", record, "Who")

	assert.True(t, resolved, "polymorphic FK with valid _type column must resolve")
	assert.Equal(t, "Lead", target,
		"target MUST come from _type column, not refTo[0]; pre-round-7 picked Contact and queried wrong table")
	require.NotNil(t, related)
	assert.Equal(t, "Lead-Y", related["Name"])
}

// TestFetchRelatedRecord_Polymorphic_MissingTypeColumn_Indeterminate locks in
// the fail-closed contract: a polymorphic FK without a polymorphic type
// column is INDETERMINATE (resolved=false), not "pick refTo[0] silently".
// Callers (records turns it into ErrRelationFetch; automation logs flow.r10
// WRN) treat indeterminate as preserve-prior-value, never as a successful
// query against an arbitrary target.
func TestFetchRelatedRecord_Polymorphic_MissingTypeColumn_Indeterminate(t *testing.T) {
	deps := &stubDeps{
		referenceTo: map[string][]string{
			"WhoId": {"Contact", "Lead"},
		},
	}
	relName := "Who"
	schema := &models.ObjectMetadata{
		APIName: "Task",
		Fields: []models.FieldMetadata{
			{APIName: "WhoId", Type: constants.FieldTypeReference, RelationshipName: &relName, ReferenceTo: []string{"Contact", "Lead"}},
		},
	}
	record := map[string]interface{}{
		"Id":    "task-1",
		"WhoId": "who-001",
		// No WhoId_type — caller cannot disambiguate.
	}

	related, target, resolved := FetchRelatedRecord(context.Background(), deps, schema, "Task", record, "Who")

	assert.False(t, resolved, "polymorphic FK with NO _type MUST be indeterminate, not silent refTo[0] pick")
	assert.Nil(t, related)
	assert.Equal(t, "", target,
		"target MUST be empty when we cannot prove which SObject the FK points at")
	assert.Equal(t, 0, deps.fetchCalls, "no DB fetch should fire on indeterminate polymorphic")
}

// TestFetchRelatedRecord_Polymorphic_TypeColumnNotInReferenceTo_Indeterminate
// asserts we never trust an arbitrary string as a target SObject — the
// _type column's value MUST appear in the field's declared ReferenceTo
// set, otherwise we fail-closed (e.g. defense against a stale/legacy
// _type value or a tenant-broken row).
func TestFetchRelatedRecord_Polymorphic_TypeColumnNotInReferenceTo_Indeterminate(t *testing.T) {
	deps := &stubDeps{
		referenceTo: map[string][]string{
			"WhoId": {"Contact", "Lead"},
		},
	}
	relName := "Who"
	schema := &models.ObjectMetadata{
		APIName: "Task",
		Fields: []models.FieldMetadata{
			{APIName: "WhoId", Type: constants.FieldTypeReference, RelationshipName: &relName, ReferenceTo: []string{"Contact", "Lead"}},
		},
	}
	record := map[string]interface{}{
		"Id":         "task-1",
		"WhoId":      "who-001",
		"WhoId_type": "Account", // not in [Contact, Lead]
	}

	related, target, resolved := FetchRelatedRecord(context.Background(), deps, schema, "Task", record, "Who")

	assert.False(t, resolved, "_type value outside declared ReferenceTo MUST fail-closed")
	assert.Nil(t, related)
	assert.Equal(t, "", target)
	assert.Equal(t, 0, deps.fetchCalls)
}
