package analytics

import (
	"context"
	"reflect"
	"testing"

	"github.com/nexuscrm/shared/pkg/models"
	"pgregory.net/rapid"
)

// Property 1 (design §7.1): the parser must never panic on any input drawn
// from the allowed alphabet and must always return a valid parsedSynthetic.
func TestProperty_ParserNeverPanics(t *testing.T) {
	p := &syntheticParser{}
	alphabet := []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$@._")

	rapid.Check(t, func(rt *rapid.T) {
		n := rapid.IntRange(0, 200).Draw(rt, "length")
		buf := make([]byte, n)
		for i := 0; i < n; i++ {
			buf[i] = alphabet[rapid.IntRange(0, len(alphabet)-1).Draw(rt, "ch")]
		}
		in := string(buf)

		defer func() {
			if r := recover(); r != nil {
				rt.Fatalf("parser panicked on %q: %v", in, r)
			}
		}()

		got := p.Parse(in)
		// A matched shape must have a non-empty base object; unknown must have empty fields.
		switch got.shape {
		case shapeUnknown:
			if got.baseObject != "" || got.anchorObject != "" || got.anchorField != "" || got.secondary != "" {
				rt.Fatalf("shapeUnknown must have empty fields, got %+v for %q", got, in)
			}
		case shapeCustomEntity, shapeCustomEntityScoped, shapeOrderCustomEntity, shapeNestedCustomEntity:
			if got.baseObject == "" {
				rt.Fatalf("shape %v must have baseObject set, got %+v for %q", got.shape, got, in)
			}
		default:
			rt.Fatalf("unknown shape value %d for %q", got.shape, in)
		}
	})
}

// Property 2 (design §7.1): parser is pure — same input produces the same output.
func TestProperty_ParserIsPure(t *testing.T) {
	p := &syntheticParser{}
	alphabet := []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$@._")

	rapid.Check(t, func(rt *rapid.T) {
		n := rapid.IntRange(0, 200).Draw(rt, "length")
		buf := make([]byte, n)
		for i := 0; i < n; i++ {
			buf[i] = alphabet[rapid.IntRange(0, len(alphabet)-1).Draw(rt, "ch")]
		}
		in := string(buf)

		first := p.Parse(in)
		second := p.Parse(in)
		if !reflect.DeepEqual(first, second) {
			rt.Fatalf("impure parse on %q: %+v vs %+v", in, first, second)
		}
	})
}

// Property 3 (design §7.1): degradation is monotonic under depth clamping.
// For any CRT whose join tree synthesizes at depth d > 0, artificially
// clamping the input to depth d-1 must still produce a non-nil template.
func TestProperty_DegradationIsMonotonic(t *testing.T) {
	synth := synthForProperty()

	rapid.Check(t, func(rt *rapid.T) {
		// Property harness only wires Campaign -> CampaignMember, so depth
		// caps at 1; clamping to 0 must still yield a valid base template.
		depth := rapid.IntRange(1, 1).Draw(rt, "depth")
		join := buildSyntheticJoinChain(depth)
		crt := &models.SystemReportType{
			FullName:   "prop",
			BaseObject: "Campaign",
			Join:       join,
		}
		full, _ := synth.Synthesize(context.Background(), crt)
		if full == nil {
			return
		}
		// Clamp by rewriting the innermost nested join to nil.
		clamped := clampSyntheticJoinChain(depth - 1)
		crt.Join = clamped
		got, _ := synth.Synthesize(context.Background(), crt)
		if got == nil {
			rt.Fatalf("clamped depth=%d produced nil template for base Campaign", depth-1)
		}
	})
}

// Property 5 (design §7.1): warning codes are a closed set.
// Every warning emitted by the synthesizer must carry a code that appears in
// allCRTWarningCodes.
func TestProperty_WarningCodesAreClosedSet(t *testing.T) {
	synth := synthForProperty()
	alphabet := []byte("abcdefghijklmnopqrstuvwxyz0123456789{}\":,._")

	rapid.Check(t, func(rt *rapid.T) {
		n := rapid.IntRange(0, 80).Draw(rt, "length")
		buf := make([]byte, n)
		for i := 0; i < n; i++ {
			buf[i] = alphabet[rapid.IntRange(0, len(alphabet)-1).Draw(rt, "ch")]
		}
		crt := &models.SystemReportType{
			FullName:   "prop-closed",
			BaseObject: "Campaign",
			Join:       string(buf),
			Sections:   string(buf),
		}
		_, warnings := synth.Synthesize(context.Background(), crt)
		for _, w := range warnings {
			if _, ok := allCRTWarningCodes[w.Code]; !ok {
				rt.Fatalf("warning code %q not in closed set (input=%q)", w.Code, string(buf))
			}
		}
	})
}

// TestProperty_SynthesizerNeverPanics hammers the synthesizer with random
// bytes in all three JSON fields and asserts it never panics. Covers the
// defer-recover guard in Synthesize plus every field-level parse path.
func TestProperty_SynthesizerNeverPanics(t *testing.T) {
	synth := synthForProperty()
	alphabet := []byte(`abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}[]":,._/\-`)

	rapid.Check(t, func(rt *rapid.T) {
		rn := func(max int) string {
			n := rapid.IntRange(0, max).Draw(rt, "n")
			b := make([]byte, n)
			for i := 0; i < n; i++ {
				b[i] = alphabet[rapid.IntRange(0, len(alphabet)-1).Draw(rt, "ch")]
			}
			return string(b)
		}
		crt := &models.SystemReportType{
			FullName:   "prop-panic",
			BaseObject: "Campaign",
			Join:       rn(120),
			Sections:   rn(120),
			Metadata:   rn(40),
		}
		defer func() {
			if r := recover(); r != nil {
				rt.Fatalf("synthesizer panicked: %v", r)
			}
		}()
		tmpl, warnings := synth.Synthesize(context.Background(), crt)
		if warnings == nil {
			rt.Fatalf("warnings must never be nil")
		}
		_ = tmpl
	})
}

// buildSyntheticJoinChain produces a JSON join payload of the given depth
// using the CampaignMembers relationship repeatedly on a circular schema the
// property harness wires up. Depth=1 yields a single join step.
func buildSyntheticJoinChain(depth int) string {
	if depth <= 0 {
		return ""
	}
	return `{"outerJoin":"false","relationship":"CampaignMembers"` +
		trailingJoin(depth-1) + `}`
}

func trailingJoin(depth int) string {
	if depth <= 0 {
		return ""
	}
	return `,"join":` + buildSyntheticJoinChain(depth)
}

func clampSyntheticJoinChain(depth int) string {
	return buildSyntheticJoinChain(depth)
}
