package models

import (
	"testing"

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

func TestResolveFieldRelationshipSemantics_Nil(t *testing.T) {
	sem := ResolveFieldRelationshipSemantics(nil, LifecycleAuthoredWrite)
	if sem.IsRelationship {
		t.Fatal("nil field should not be a relationship")
	}
	if sem.IsPolymorphic {
		t.Fatal("nil field should not be polymorphic")
	}
	if sem.CanonicalType != "" {
		t.Fatalf("nil field canonical type = %q, want empty", sem.CanonicalType)
	}
}

func TestResolveFieldRelationshipSemantics_NonRelationship(t *testing.T) {
	field := &FieldMetadata{
		Type: constants.FieldTypeString,
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecyclePersistedRead)
	if sem.IsRelationship {
		t.Fatal("string field should not be a relationship")
	}
	if sem.IsPolymorphic {
		t.Fatal("string field should not be polymorphic")
	}
	if sem.CanonicalType != constants.FieldTypeString {
		t.Fatalf("canonical type = %q, want %q", sem.CanonicalType, constants.FieldTypeString)
	}
}

func TestResolveFieldRelationshipSemantics_LookupAliasNormalization(t *testing.T) {
	// Authored "lookup" should normalize to "reference" in CanonicalType.
	field := &FieldMetadata{
		Type:        "lookup",
		ReferenceTo: []string{"Account"},
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecycleAuthoredWrite)
	if sem.CanonicalType != constants.FieldTypeReference {
		t.Fatalf("canonical type = %q, want %q", sem.CanonicalType, constants.FieldTypeReference)
	}
	if !sem.IsRelationship {
		t.Fatal("lookup-authored field should be a relationship after normalization")
	}
}

func TestResolveFieldRelationshipSemantics_MasterDetail(t *testing.T) {
	field := &FieldMetadata{
		Type:        constants.FieldTypeMasterDetail,
		ReferenceTo: []string{"Account"},
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecyclePersistedRead)
	if !sem.IsRelationship {
		t.Fatal("masterdetail should be a relationship")
	}
	if !sem.IsMasterDetail {
		t.Fatal("masterdetail should have IsMasterDetail=true")
	}
	if sem.RelationshipKind != constants.FieldTypeMasterDetail {
		t.Fatalf("relationship kind = %q, want %q", sem.RelationshipKind, constants.FieldTypeMasterDetail)
	}
	if sem.IsPolymorphic {
		t.Fatal("single-target masterdetail should not be polymorphic")
	}
}

func TestResolveFieldRelationshipSemantics_ReferenceTargetPassthrough(t *testing.T) {
	field := &FieldMetadata{
		Type:                constants.FieldTypeReference,
		ReferenceTo:         []string{"Account", "Contact"},
		WritableReferenceTo: []string{"Account"},
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecyclePersistedRead)
	if len(sem.ReferenceTargets) != 2 {
		t.Fatalf("reference targets = %v, want 2 entries", sem.ReferenceTargets)
	}
	if len(sem.WritableReferenceTargets) != 1 {
		t.Fatalf("writable reference targets = %v, want 1 entry", sem.WritableReferenceTargets)
	}
	// Ensure copies, not shared slices.
	field.ReferenceTo[0] = "mutated"
	if sem.ReferenceTargets[0] == "mutated" {
		t.Fatal("resolver should copy reference targets, not share the slice")
	}
}

func TestResolveFieldRelationshipSemantics_AuthoredMultiTargetLookupInference(t *testing.T) {
	// Authored multi-target lookup should infer polymorphic ownership.
	field := &FieldMetadata{
		Type:        "lookup",
		ReferenceTo: []string{"Account", "Contact"},
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecycleAuthoredWrite)
	if !sem.IsPolymorphic {
		t.Fatal("authored multi-target lookup should infer IsPolymorphic=true")
	}
	if sem.CanonicalType != constants.FieldTypeReference {
		t.Fatalf("canonical type = %q, want %q", sem.CanonicalType, constants.FieldTypeReference)
	}
}

func TestResolveFieldRelationshipSemantics_AuthoredMultiTargetReferenceInference(t *testing.T) {
	field := &FieldMetadata{
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Account", "Contact"},
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecycleAuthoredWrite)
	if !sem.IsPolymorphic {
		t.Fatal("authored multi-target reference should infer IsPolymorphic=true")
	}
}

func TestResolveFieldRelationshipSemantics_PersistedLegacyLookupNonInference(t *testing.T) {
	// Legacy persisted Type=lookup multi-target row should NOT infer polymorphic
	// ownership on persisted read unless IsPolymorphic is explicitly set.
	field := &FieldMetadata{
		Type:          "lookup",
		ReferenceTo:   []string{"Account", "Contact"},
		IsPolymorphic: false,
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecyclePersistedRead)
	if sem.IsPolymorphic {
		t.Fatal("persisted legacy lookup row should not infer IsPolymorphic from multi-target alone")
	}
}

func TestResolveFieldRelationshipSemantics_PersistedLegacyLookupWithExplicitFlag(t *testing.T) {
	// Legacy Type=lookup row with explicit IsPolymorphic=true (set by backfill)
	// should retain polymorphic ownership on persisted read.
	field := &FieldMetadata{
		Type:          "lookup",
		ReferenceTo:   []string{"Account", "Contact"},
		IsPolymorphic: true,
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecyclePersistedRead)
	if !sem.IsPolymorphic {
		t.Fatal("persisted legacy lookup row with explicit IsPolymorphic=true should be polymorphic")
	}
}

func TestResolveFieldRelationshipSemantics_PersistedReferenceMultiTargetInference(t *testing.T) {
	// Persisted Type=reference multi-target rows infer polymorphic ownership.
	field := &FieldMetadata{
		Type:          constants.FieldTypeReference,
		ReferenceTo:   []string{"Account", "Contact"},
		IsPolymorphic: false,
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecyclePersistedRead)
	if !sem.IsPolymorphic {
		t.Fatal("persisted reference multi-target row should infer IsPolymorphic=true")
	}
}

func TestResolveFieldRelationshipSemantics_ExplicitIsPolymorphic(t *testing.T) {
	// Explicit IsPolymorphic=true is authoritative for single-target fields.
	field := &FieldMetadata{
		Type:          constants.FieldTypeReference,
		ReferenceTo:   []string{"Account"},
		IsPolymorphic: true,
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecyclePersistedRead)
	if !sem.IsPolymorphic {
		t.Fatal("explicit IsPolymorphic=true should be authoritative")
	}
}

func TestResolveFieldRelationshipSemantics_SingleTargetReference(t *testing.T) {
	field := &FieldMetadata{
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Account"},
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecyclePersistedRead)
	if sem.IsPolymorphic {
		t.Fatal("single-target reference without explicit flag should not be polymorphic")
	}
	if !sem.IsRelationship {
		t.Fatal("single-target reference should be a relationship")
	}
}

func TestResolveFieldRelationshipSemantics_EmptyReferenceTo(t *testing.T) {
	field := &FieldMetadata{
		Type:        constants.FieldTypeReference,
		ReferenceTo: nil,
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecyclePersistedRead)
	if sem.IsPolymorphic {
		t.Fatal("empty ReferenceTo should not be polymorphic")
	}
	if len(sem.ReferenceTargets) != 0 {
		t.Fatalf("reference targets = %v, want empty", sem.ReferenceTargets)
	}
}

func TestResolveFieldRelationshipSemantics_ExplicitIsMasterDetail(t *testing.T) {
	// Explicit IsMasterDetail on a non-masterdetail typed field.
	field := &FieldMetadata{
		Type:         constants.FieldTypeReference,
		ReferenceTo:  []string{"Account"},
		IsMasterDetail: true,
	}
	sem := ResolveFieldRelationshipSemantics(field, LifecyclePersistedRead)
	if !sem.IsMasterDetail {
		t.Fatal("explicit IsMasterDetail should be respected")
	}
	if !sem.IsRelationship {
		t.Fatal("explicit IsMasterDetail should make the field a relationship")
	}
}

func TestIsPolymorphicReferenceField_DelegatesToResolver(t *testing.T) {
	// Persisted legacy lookup multi-target: should NOT be polymorphic.
	legacyLookup := &FieldMetadata{
		Type:          "lookup",
		ReferenceTo:   []string{"Account", "Contact"},
		IsPolymorphic: false,
	}
	if IsPolymorphicReferenceField(legacyLookup) {
		t.Fatal("IsPolymorphicReferenceField should not infer polymorphic for legacy lookup rows")
	}

	// Persisted reference multi-target: should be polymorphic.
	refMulti := &FieldMetadata{
		Type:          constants.FieldTypeReference,
		ReferenceTo:   []string{"Account", "Contact"},
		IsPolymorphic: false,
	}
	if !IsPolymorphicReferenceField(refMulti) {
		t.Fatal("IsPolymorphicReferenceField should infer polymorphic for reference multi-target rows")
	}

	// Backfilled legacy lookup with explicit flag: should be polymorphic.
	backfilled := &FieldMetadata{
		Type:          "lookup",
		ReferenceTo:   []string{"Account", "Contact"},
		IsPolymorphic: true,
	}
	if !IsPolymorphicReferenceField(backfilled) {
		t.Fatal("IsPolymorphicReferenceField should respect explicit IsPolymorphic on legacy lookup rows")
	}
}

func TestIsRelationshipField_DelegatesToResolver(t *testing.T) {
	ref := &FieldMetadata{
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Account"},
	}
	if !IsRelationshipField(ref) {
		t.Fatal("reference field should be a relationship")
	}

	str := &FieldMetadata{
		Type: constants.FieldTypeString,
	}
	if IsRelationshipField(str) {
		t.Fatal("string field should not be a relationship")
	}
}

func TestFieldCanonicalType_DelegatesToResolver(t *testing.T) {
	lookup := &FieldMetadata{Type: "lookup"}
	if FieldCanonicalType(lookup) != constants.FieldTypeReference {
		t.Fatalf("lookup canonical type = %q, want %q", FieldCanonicalType(lookup), constants.FieldTypeReference)
	}

	str := &FieldMetadata{Type: constants.FieldTypeString}
	if FieldCanonicalType(str) != constants.FieldTypeString {
		t.Fatalf("string canonical type = %q, want %q", FieldCanonicalType(str), constants.FieldTypeString)
	}
}

func TestNormalizeLookupType(t *testing.T) {
	if NormalizeLookupType("lookup") != constants.FieldTypeReference {
		t.Fatal("lookup should normalize to reference")
	}
	if NormalizeLookupType(constants.FieldTypeReference) != constants.FieldTypeReference {
		t.Fatal("reference should pass through")
	}
	if NormalizeLookupType(constants.FieldTypeString) != constants.FieldTypeString {
		t.Fatal("string should pass through")
	}
	if NormalizeLookupType("  Lookup  ") != constants.FieldTypeReference {
		t.Fatal("trimmed/cased lookup should normalize to reference")
	}
}

// TestResolveFieldRelationshipSemantics_AuthoredLookupRoundTrip verifies the
// core reload-safety contract: a pack/admin-authored multi-target lookup
// resolves through LifecycleAuthoredWrite with IsPolymorphic=true and
// CanonicalType=reference. After persistence (Type normalized to reference,
// IsPolymorphic=true), the same row resolves through LifecyclePersistedRead
// as polymorphic. This is the round-trip that finding 1 identified as
// broken when inline derivations bypassed the resolver.
func TestResolveFieldRelationshipSemantics_AuthoredLookupRoundTrip(t *testing.T) {
	// Step 1: Authored write. Admin/pack authors Type="lookup" with
	// multi-target ReferenceTo. Resolver must set IsPolymorphic=true and
	// CanonicalType=reference so the authoring path persists canonical shape.
	authored := &FieldMetadata{
		Type:        "lookup",
		ReferenceTo: []string{"Account", "Contact"},
	}
	authoredSem := ResolveFieldRelationshipSemantics(authored, LifecycleAuthoredWrite)
	if !authoredSem.IsPolymorphic {
		t.Fatal("authored multi-target lookup should infer IsPolymorphic=true")
	}
	if authoredSem.CanonicalType != constants.FieldTypeReference {
		t.Fatalf("authored canonical type = %q, want %q", authoredSem.CanonicalType, constants.FieldTypeReference)
	}

	// Step 2: Persist. The authoring path stamps field.Type = CanonicalType
	// and field.IsPolymorphic = true. Simulated by constructing a new field
	// as if loaded from _System_Field after restart.
	persisted := &FieldMetadata{
		Type:          authoredSem.CanonicalType,
		ReferenceTo:   []string{"Account", "Contact"},
		IsPolymorphic: authoredSem.IsPolymorphic,
	}

	// Step 3: Persisted read after restart. Must resolve as polymorphic.
	persistedSem := ResolveFieldRelationshipSemantics(persisted, LifecyclePersistedRead)
	if !persistedSem.IsPolymorphic {
		t.Fatal("persisted multi-target reference with IsPolymorphic=true must round-trip as polymorphic after restart")
	}
	if persistedSem.CanonicalType != constants.FieldTypeReference {
		t.Fatalf("persisted canonical type = %q, want %q", persistedSem.CanonicalType, constants.FieldTypeReference)
	}
}

// TestResolveFieldRelationshipSemantics_BackfilledLegacyLookupRoundTrip
// verifies the bootstrap backfill path: a legacy persisted Type=lookup
// multi-target row that the backfill stamps IsPolymorphic=true round-trips
// as polymorphic after restart.
func TestResolveFieldRelationshipSemantics_BackfilledLegacyLookupRoundTrip(t *testing.T) {
	// Legacy row persisted as Type=lookup, IsPolymorphic=false (pre-cutover).
	legacy := &FieldMetadata{
		Type:          "lookup",
		ReferenceTo:   []string{"Account", "Contact"},
		IsPolymorphic: false,
	}
	// Before backfill: persisted-read does NOT infer polymorphic ownership.
	preBackfill := ResolveFieldRelationshipSemantics(legacy, LifecyclePersistedRead)
	if preBackfill.IsPolymorphic {
		t.Fatal("pre-backfill legacy lookup should not infer polymorphic ownership")
	}

	// After backfill stamps IsPolymorphic=true.
	legacy.IsPolymorphic = true
	postBackfill := ResolveFieldRelationshipSemantics(legacy, LifecyclePersistedRead)
	if !postBackfill.IsPolymorphic {
		t.Fatal("post-backfill legacy lookup with IsPolymorphic=true must be polymorphic after restart")
	}
}
