package dialectsfcrm

import (
	"context"
	"reflect"
	"testing"

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

// A polymorphic reference (Task.WhatId "Related To") has many possible
// referenceTo targets; the concrete object varies per record, so ReferenceTo[0]
// (Account here) is the wrong lookup target. resolveRecordLookups must resolve
// the real object from the id, query THAT object for the display name, and
// publish a "<field>_type" companion so the frontend renders a navigable link.
func TestUIAPIRecordUIResolvesPolymorphicLookupToConcreteType(t *testing.T) {
	stub := newUIAPITestContracts()
	stub.schemas = map[string]*models.ObjectMetadata{
		"Opportunity": uiAPILookupTestSchema("Opportunity"),
	}
	stub.objectType = "Opportunity" // ObjectTypeForID resolves the WhatId target
	stub.records["006-what"] = contract.Record{constants.FieldName: "Acme Expansion Opportunity"}

	uiRecord := &uiAPIRecord{
		ApiName: "Task",
		Fields:  map[string]uiAPIFieldValue{"WhatId": {Value: "006-what"}},
	}
	// targetObj starts at ReferenceTo[0] (Account); the polymorphic flag defers
	// the real target to resolveRecordLookups, which must accept it only if it is
	// one of the declared referenceTo targets.
	lookups := []uiAPILookupTarget{
		{fieldName: "WhatId", relName: "What", sourceObj: "Task", targetObj: "Account", targetID: "006-what", polymorphic: true, allowedTargets: []string{"Account", "Opportunity", "Case"}},
	}

	service := &uiAPIService{records: stub, metadata: stub, catalog: stub}
	if err := service.resolveRecordLookups(context.Background(), lookups, uiRecord, &models.UserSession{ID: "005-user"}); err != nil {
		t.Fatalf("resolveRecordLookups() error = %v", err)
	}

	// Display-name retrieval must hit the concrete type, never ReferenceTo[0].
	wantBatchCalls := []uiAPIRetrieveManyCall{{objectName: "Opportunity", ids: []string{"006-what"}}}
	if !reflect.DeepEqual(stub.retrieveManyCalls, wantBatchCalls) {
		t.Fatalf("RetrieveMany calls = %#v, want %#v", stub.retrieveManyCalls, wantBatchCalls)
	}
	// The "Related To" cell shows the Opportunity name…
	if got := uiRecord.Fields["WhatId"].DisplayValue; got == nil || *got != "Acme Expansion Opportunity" {
		t.Fatalf("WhatId display value = %v, want the Opportunity name", got)
	}
	// …and the concrete type is published so the frontend renders a link.
	if got := uiRecord.Fields["WhatId"+lookupTypeSuffix].Value; got != "Opportunity" {
		t.Fatalf("WhatId%s = %v, want %q", lookupTypeSuffix, got, "Opportunity")
	}
	// The Salesforce-shaped nested relationship is present under relName.
	if _, ok := uiRecord.Fields["What"]; !ok {
		t.Fatalf("expected nested What relationship to be set")
	}
}

// When the concrete target cannot be classified at all, the lookup must not
// publish a "<field>_type" — the frontend then renders plain text rather than a
// broken link, matching the pre-fix raw-id behavior (no regression).
func TestUIAPIRecordUIPolymorphicLookupUnresolvedPublishesNoType(t *testing.T) {
	stub := newUIAPITestContracts()
	stub.schemas = map[string]*models.ObjectMetadata{"Account": uiAPILookupTestSchema("Account")}
	stub.objectType = "" // id-type resolver cannot classify the target

	uiRecord := &uiAPIRecord{
		ApiName: "Task",
		Fields:  map[string]uiAPIFieldValue{"WhatId": {Value: "006-what"}},
	}
	lookups := []uiAPILookupTarget{
		{fieldName: "WhatId", relName: "What", sourceObj: "Task", targetObj: "Account", targetID: "006-what", polymorphic: true, allowedTargets: []string{"Account", "Opportunity"}},
	}

	service := &uiAPIService{records: stub, metadata: stub, catalog: stub}
	if err := service.resolveRecordLookups(context.Background(), lookups, uiRecord, &models.UserSession{ID: "005-user"}); err != nil {
		t.Fatalf("resolveRecordLookups() error = %v", err)
	}
	if _, ok := uiRecord.Fields["WhatId"+lookupTypeSuffix]; ok {
		t.Fatalf("WhatId%s must not be published when the concrete type is unresolved", lookupTypeSuffix)
	}
}

// A resolved type that is NOT one of the field's declared referenceTo targets
// (stale/renamed object, data drift) must be ignored: never hydrate it and never
// publish a link to it.
func TestUIAPIRecordUIPolymorphicLookupRejectsTypeOutsideReferenceTo(t *testing.T) {
	stub := newUIAPITestContracts()
	stub.schemas = map[string]*models.ObjectMetadata{"Account": uiAPILookupTestSchema("Account")}
	stub.objectType = "Contact" // id resolves to a type the field does NOT declare

	uiRecord := &uiAPIRecord{
		ApiName: "Task",
		Fields:  map[string]uiAPIFieldValue{"WhatId": {Value: "003-who"}},
	}
	lookups := []uiAPILookupTarget{
		{fieldName: "WhatId", relName: "What", sourceObj: "Task", targetObj: "Account", targetID: "003-who", polymorphic: true, allowedTargets: []string{"Account", "Opportunity"}},
	}

	service := &uiAPIService{records: stub, metadata: stub, catalog: stub}
	if err := service.resolveRecordLookups(context.Background(), lookups, uiRecord, &models.UserSession{ID: "005-user"}); err != nil {
		t.Fatalf("resolveRecordLookups() error = %v", err)
	}
	for _, call := range stub.retrieveManyCalls {
		if call.objectName == "Contact" {
			t.Fatalf("must not hydrate Contact: it is not a declared referenceTo target")
		}
	}
	if _, ok := uiRecord.Fields["WhatId"+lookupTypeSuffix]; ok {
		t.Fatalf("WhatId%s must not be published for a type outside referenceTo", lookupTypeSuffix)
	}
}

// The type companion is published only after the target record and its schema
// resolve. If retrieval yields nothing, a stale companion the record carried
// must be cleared so the frontend renders plain text, not a link to the raw id.
func TestUIAPIRecordUIPolymorphicLookupClearsStaleTypeWhenTargetUnresolvable(t *testing.T) {
	stub := newUIAPITestContracts()
	stub.schemas = map[string]*models.ObjectMetadata{"Opportunity": uiAPILookupTestSchema("Opportunity")}
	stub.objectType = "Opportunity" // classification succeeds…
	// …but stub.records has no "006-what", so retrieval yields nothing.

	uiRecord := &uiAPIRecord{
		ApiName: "Task",
		Fields: map[string]uiAPIFieldValue{
			"WhatId":                    {Value: "006-what"},
			"WhatId" + lookupTypeSuffix: {Value: "Opportunity"}, // stale companion carried on the record
		},
	}
	lookups := []uiAPILookupTarget{
		{fieldName: "WhatId", relName: "What", sourceObj: "Task", targetObj: "Account", targetID: "006-what", polymorphic: true, allowedTargets: []string{"Account", "Opportunity"}},
	}

	service := &uiAPIService{records: stub, metadata: stub, catalog: stub}
	if err := service.resolveRecordLookups(context.Background(), lookups, uiRecord, &models.UserSession{ID: "005-user"}); err != nil {
		t.Fatalf("resolveRecordLookups() error = %v", err)
	}
	// Hydration still targets the concrete (declared) type…
	wantBatchCalls := []uiAPIRetrieveManyCall{{objectName: "Opportunity", ids: []string{"006-what"}}}
	if !reflect.DeepEqual(stub.retrieveManyCalls, wantBatchCalls) {
		t.Fatalf("RetrieveMany calls = %#v, want %#v", stub.retrieveManyCalls, wantBatchCalls)
	}
	// …but since the target did not resolve, the stale companion must be gone.
	if _, ok := uiRecord.Fields["WhatId"+lookupTypeSuffix]; ok {
		t.Fatalf("WhatId%s must be cleared when the target record does not resolve", lookupTypeSuffix)
	}
}
