package analytics

import (
	"context"
	"reflect"
	"testing"

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

// fakeCRTMetadata is a hand-built crtMetadataLookup for resolver unit tests.
// panicOn lets a test drive the C8 panic-recovery path.
type fakeCRTMetadata struct {
	schemas map[string]*models.ObjectMetadata
	panicOn string
}

func (f fakeCRTMetadata) GetSchema(_ context.Context, apiName string) *models.ObjectMetadata {
	if f.panicOn != "" && apiName == f.panicOn {
		panic("fakeCRTMetadata: induced panic for " + apiName)
	}
	return f.schemas[apiName]
}

func crtObj(name string, fields ...string) *models.ObjectMetadata {
	fs := make([]models.FieldMetadata, 0, len(fields))
	for _, f := range fields {
		fs = append(fs, models.FieldMetadata{APIName: f})
	}
	return &models.ObjectMetadata{APIName: name, Fields: fs}
}

func newResolverWith(md crtMetadataLookup) *crtResolver {
	// loader/std/logger nil: no stored-CRT path, no standard-object
	// short-circuit — every input flows to the synthetic parser path.
	return newCRTResolverForTest(nil, md, nil, nil)
}

// TestResolve_FaultMatrix covers the §4.10 synthetic-CRT fault matrix rows that
// the resolver implements (C1–C6, C8). C7 has no representation in current code
// and its design doc is absent from the repo, so it is intentionally omitted.
func TestResolve_FaultMatrix(t *testing.T) {
	ctx := context.Background()

	t.Run("C1 two-table LEFT JOIN", func(t *testing.T) {
		r := newResolverWith(fakeCRTMetadata{schemas: map[string]*models.ObjectMetadata{
			"Contact": crtObj("Contact", "AccountId"),
			"Account": crtObj("Account"),
		}})
		tmpl, w, err := r.Resolve(ctx, "CustomEntity$Contact@Account.AccountId", nil)
		if err != nil {
			t.Fatalf("err = %v", err)
		}
		if tmpl == nil || len(w) != 0 {
			t.Fatalf("tmpl=%v warnings=%v, want template + no warnings", tmpl, w)
		}
		if tmpl.PrimaryTable != "contact" || tmpl.PrimaryAlias != "c" {
			t.Fatalf("primary = %s/%s, want contact/c", tmpl.PrimaryTable, tmpl.PrimaryAlias)
		}
		if len(tmpl.Joins) != 1 {
			t.Fatalf("joins = %#v, want exactly one", tmpl.Joins)
		}
		j := tmpl.Joins[0]
		if j.Table != "account" || j.Alias != "t2" || j.JoinType != "LEFT JOIN" ||
			j.OnLeft != "c.AccountId" || j.OnRight != "t2.Id" {
			t.Fatalf("join = %#v", j)
		}
	})

	t.Run("C2 anchor FK missing → base-only + warning", func(t *testing.T) {
		r := newResolverWith(fakeCRTMetadata{schemas: map[string]*models.ObjectMetadata{
			"Contact": crtObj("Contact", "AccountId"), // lacks "MissingFk"
			"Account": crtObj("Account"),
		}})
		tmpl, w, err := r.Resolve(ctx, "CustomEntity$Contact@Account.MissingFk", nil)
		if err != nil || tmpl == nil {
			t.Fatalf("tmpl=%v err=%v", tmpl, err)
		}
		if len(tmpl.Joins) != 0 {
			t.Fatalf("joins = %#v, want base-only", tmpl.Joins)
		}
		if len(w) != 1 || w[0].Code != WarnCRTAnchorFKMissing {
			t.Fatalf("warnings = %#v, want one WarnCRTAnchorFKMissing", w)
		}
	})

	t.Run("C3 anchor object missing → base-only + warning", func(t *testing.T) {
		r := newResolverWith(fakeCRTMetadata{schemas: map[string]*models.ObjectMetadata{
			"Contact": crtObj("Contact", "GhostId"),
		}})
		tmpl, w, err := r.Resolve(ctx, "CustomEntity$Contact@Ghost.GhostId", nil)
		if err != nil || tmpl == nil {
			t.Fatalf("tmpl=%v err=%v", tmpl, err)
		}
		if len(tmpl.Joins) != 0 {
			t.Fatalf("joins = %#v, want base-only", tmpl.Joins)
		}
		if len(w) != 1 || w[0].Code != WarnCRTAnchorObjectMissing {
			t.Fatalf("warnings = %#v, want one WarnCRTAnchorObjectMissing", w)
		}
	})

	t.Run("C4 base object missing → not a CRT", func(t *testing.T) {
		r := newResolverWith(fakeCRTMetadata{schemas: map[string]*models.ObjectMetadata{}})
		tmpl, w, err := r.Resolve(ctx, "CustomEntity$Ghost", nil)
		if tmpl != nil || w != nil || err != nil {
			t.Fatalf("got (%v,%v,%v), want (nil,nil,nil)", tmpl, w, err)
		}
	})

	t.Run("C5 nested degrades to base-only + warning", func(t *testing.T) {
		r := newResolverWith(fakeCRTMetadata{schemas: map[string]*models.ObjectMetadata{
			"Contact": crtObj("Contact", "AccountId"),
			"Account": crtObj("Account"),
		}})
		tmpl, w, err := r.Resolve(ctx, "CustomEntityCustomEntity$Contact$Foo@Account.AccountId", nil)
		if err != nil || tmpl == nil {
			t.Fatalf("tmpl=%v err=%v", tmpl, err)
		}
		if tmpl.PrimaryTable != "contact" || len(tmpl.Joins) != 0 {
			t.Fatalf("tmpl = %#v, want base-only contact", tmpl)
		}
		if len(w) != 1 || w[0].Code != WarnCRTNestedUnsupported {
			t.Fatalf("warnings = %#v, want one WarnCRTNestedUnsupported", w)
		}
	})

	t.Run("C6 nested base missing → not a CRT", func(t *testing.T) {
		r := newResolverWith(fakeCRTMetadata{schemas: map[string]*models.ObjectMetadata{}})
		tmpl, w, err := r.Resolve(ctx, "CustomEntityCustomEntity$Ghost$Foo", nil)
		if tmpl != nil || w != nil || err != nil {
			t.Fatalf("got (%v,%v,%v), want (nil,nil,nil)", tmpl, w, err)
		}
	})

	t.Run("C8 downstream panic recovered → not a CRT", func(t *testing.T) {
		r := newResolverWith(fakeCRTMetadata{panicOn: "Boom"})
		tmpl, w, err := r.Resolve(ctx, "CustomEntity$Boom", nil)
		if tmpl != nil || w != nil || err != nil {
			t.Fatalf("got (%v,%v,%v), want (nil,nil,nil) after panic recovery", tmpl, w, err)
		}
	})

	t.Run("unknown shape → not a CRT", func(t *testing.T) {
		r := newResolverWith(fakeCRTMetadata{schemas: map[string]*models.ObjectMetadata{}})
		tmpl, w, err := r.Resolve(ctx, "TotallyNotACRT", nil)
		if tmpl != nil || w != nil || err != nil {
			t.Fatalf("got (%v,%v,%v), want (nil,nil,nil)", tmpl, w, err)
		}
	})
}

// TestResolve_PrefixIsBehaviorPreserving proves the generalization changed no
// observable behavior for the formerly-hardcoded Order prefix: a prefixed
// SourceObject resolves to a byte-identical template/warnings/error as its
// unprefixed counterpart, for both bare and scoped forms. A non-CRM prefix
// (Opportunity) is included to show the generalization is object-neutral.
func TestResolve_PrefixIsBehaviorPreserving(t *testing.T) {
	ctx := context.Background()
	r := newResolverWith(fakeCRTMetadata{schemas: map[string]*models.ObjectMetadata{
		"Account": crtObj("Account"),
		"Contact": crtObj("Contact", "AccountId"),
	}})

	pairs := []struct{ prefixed, plain string }{
		{"OrderCustomEntity$Account", "CustomEntity$Account"},
		{"OrderCustomEntity$Contact@Account.AccountId", "CustomEntity$Contact@Account.AccountId"},
		{"OpportunityCustomEntity$Account", "CustomEntity$Account"},
		{"OpportunityCustomEntity$Contact@Account.AccountId", "CustomEntity$Contact@Account.AccountId"},
	}
	for _, p := range pairs {
		pt, pw, pe := r.Resolve(ctx, p.prefixed, nil)
		ut, uw, ue := r.Resolve(ctx, p.plain, nil)
		if !reflect.DeepEqual(pt, ut) || !reflect.DeepEqual(pw, uw) || (pe == nil) != (ue == nil) {
			t.Fatalf("prefixed %q != unprefixed %q:\n  %#v / %#v / %v\n  %#v / %#v / %v",
				p.prefixed, p.plain, pt, pw, pe, ut, uw, ue)
		}
	}
}
