package analytics

import (
	"context"
	"testing"

	"github.com/nexuscrm/backend/internal/features/metadata"
	"github.com/nexuscrm/backend/pkg/logging"
	"github.com/nexuscrm/shared/pkg/models"
)

// buildResolverFromNames constructs a StandardObjectResolver directly from a list
// of canonical API names, bypassing metadata. It mirrors what
// BuildStandardObjectResolver produces for a given set of objects and lets the
// tests avoid instantiating a real MetadataService (which has many deps).
func buildResolverFromNames(names []string) *StandardObjectResolver {
	r := &StandardObjectResolver{
		canonical: make(map[string]string, len(names)),
		aliases:   make(map[string]string, len(names)),
	}
	for _, n := range names {
		registerStandardObject(r, n)
	}
	return r
}

func TestStandardObjectResolver_HappyPath(t *testing.T) {
	r := buildResolverFromNames([]string{"Lead", "Opportunity", "OrderItem", "Contract", "Product2", "User"})

	cases := []struct {
		in   string
		want string
	}{
		{"OrderItemList", "orderitem"},
		{"orderitem", "orderitem"},
		{"OpportunityList", "opportunity"},
		{"ContractList", "contract"},
		{"User", "user"},
		{"UserList", "user"},
		{"Product2List", "product2"},
		{"unknown_thing", ""},
		{"", ""},
	}

	for _, tc := range cases {
		got := r.Lookup(tc.in)
		if got != tc.want {
			t.Errorf("Lookup(%q) = %q, want %q", tc.in, got, tc.want)
		}
	}
}

func TestStandardObjectResolver_CaseInsensitiveAlias(t *testing.T) {
	r := buildResolverFromNames([]string{"OrderItem"})

	for _, in := range []string{"OrderItemList", "orderitemlist", "ORDERITEMLIST", "OrDeRiTeMlIsT"} {
		if got := r.Lookup(in); got != "orderitem" {
			t.Errorf("Lookup(%q) = %q, want %q", in, got, "orderitem")
		}
	}
}

func TestStandardObjectResolver_ColdStartEmpty(t *testing.T) {
	ctx := context.Background()
	logger := logging.New(ctx)
	r := BuildStandardObjectResolver(ctx, nil, logger)
	if r == nil {
		t.Fatal("BuildStandardObjectResolver returned nil")
	}
	for _, in := range []string{"Lead", "LeadList", "anything", ""} {
		if got := r.Lookup(in); got != "" {
			t.Errorf("Lookup(%q) = %q on empty resolver, want empty", in, got)
		}
	}
}

func TestStandardObjectResolver_CanonicalCollisionNoPanic(t *testing.T) {
	// Defensive: pass duplicates; resolver must not panic and must accept either.
	defer func() {
		if r := recover(); r != nil {
			t.Fatalf("resolver panicked on duplicate registration: %v", r)
		}
	}()
	r := buildResolverFromNames([]string{"Lead", "Lead", "lead"})
	if got := r.Lookup("LeadList"); got != "lead" {
		t.Errorf("Lookup(LeadList) = %q, want %q", got, "lead")
	}
}

func TestStandardObjectResolver_NoRegressionVsHardcodedMap(t *testing.T) {
	// The wiring agent will consult the resolver first, then the hardcoded map.
	// Guarantee the resolver returns the same value for every key already in the
	// hardcoded map when the underlying object exists in metadata.
	var names []string
	for _, obj := range sfReportTypeToObject {
		names = append(names, obj)
	}
	r := buildResolverFromNames(names)

	for key, want := range sfReportTypeToObject {
		// Only assert for keys that look like legacy "<PascalCase>List" aliases or
		// bare canonical names. Keys like "CampaignLeadLifetime" are report-type
		// shapes not handled by this resolver, so we simply check that if the
		// resolver returns a non-empty value, it matches.
		got := r.Lookup(key)
		if got != "" && got != want {
			t.Errorf("regression: Lookup(%q) = %q, hardcoded map says %q", key, got, want)
		}
	}
}

func TestStandardObjectResolver_ResearchIdentifiedMissingTypes(t *testing.T) {
	// 7 standard report types that the current whitelist misses per crt-research-data.md.
	r := buildResolverFromNames([]string{
		"OrderItem",
		"QuoteLineItem", // QuoteProduct alias
		"OpportunityContactRole",
		"Contract",
		"Product2",
		"User",
		"PartnerFundAllocation",
	})

	cases := []struct {
		in   string
		want string
	}{
		{"OrderItemList", "orderitem"},
		{"QuoteLineItemList", "quotelineitem"},
		{"OpportunityContactRoleList", "opportunitycontactrole"},
		{"ContractList", "contract"},
		{"Product2List", "product2"},
		{"User", "user"},
		{"UserList", "user"},
		{"PartnerFundAllocationList", "partnerfundallocation"},
	}
	for _, tc := range cases {
		if got := r.Lookup(tc.in); got != tc.want {
			t.Errorf("Lookup(%q) = %q, want %q", tc.in, got, tc.want)
		}
	}
}

// --- fake metadata source for the BuildStandardObjectResolver integration test ---

// TestBuildStandardObjectResolver_FromMetadata exercises the real
// BuildStandardObjectResolver via the withSchemaSourceForTest functional
// option. This covers the metadata integration branch without relying on
// a shared mutable package var (HIGH-7).
func TestBuildStandardObjectResolver_FromMetadata(t *testing.T) {
	ctx := context.Background()
	r := BuildStandardObjectResolver(ctx, nil, logging.New(ctx),
		withSchemaSourceForTest(func(ctx context.Context, md *metadata.MetadataService) []*models.ObjectMetadata {
			return []*models.ObjectMetadata{
				{APIName: "Lead"},
				{APIName: "Opportunity"},
				{APIName: "OrderItem"},
				{APIName: "User", IsSystem: true}, // system objects still register
				{APIName: "ListObject"},           // does not end in List — normal canonical key
				{APIName: ""},                     // defensive: skipped
			}
		}))
	cases := map[string]string{
		"LeadList":        "lead",
		"lead":            "lead",
		"OpportunityList": "opportunity",
		"OrderItemList":   "orderitem",
		"orderitem":       "orderitem",
		"User":            "user",
		"UserList":        "user",
		"ListObject":      "listobject",
		"ListObjectList":  "listobject",
		"bogus":           "",
	}
	for in, want := range cases {
		if got := r.Lookup(in); got != want {
			t.Errorf("Lookup(%q) = %q, want %q", in, got, want)
		}
	}
}
