// Package autonumber implements metadata-driven identifier assignment for NexusCRM.
package autonumber

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestParse_RejectsInvalidFormats covers PRD §10.1 case 5 — each row of
// §6.4 "Fail examples" must be rejected at parse time with a
// structured error. Wave 1: stub returns errNotImplemented, so asserts
// on specific error classification will fail — that is the RED state.
func TestParse_RejectsInvalidFormats(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name   string
		format string
	}{
		{"no counter token (literal only)", "AUTO-"},
		{"two counter tokens", "{0000}-{0000}"},
		{"counter missing, date only", "{YYYY}"},
		{"unknown date token HH", "{HH}-{0000}"},
		{"counter width 19 exceeds cap 18", "{0000000000000000000}"},
		{"empty braces", "{}-{0000}"},
		{"unclosed brace (trailing)", "{0000"},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			_, _, err := Parse(tc.format)
			require.Error(t, err, "format %q must be rejected", tc.format)
			// A format-validation error must NOT be errNotImplemented in
			// the final implementation. Wave 1: this will surface the
			// stub state.
			assert.NotErrorIs(t, err, errNotImplemented,
				"parser should return structured validation error, not stub sentinel")
		})
	}
}

// TestParse_HandlesWhitespace covers PRD §10.1 case 5a — the grammar
// does not permit whitespace inside tokens; the parser must reject
// `{ 0000 }` rather than silently trim.
func TestParse_HandlesWhitespace(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name   string
		format string
	}{
		{"space inside counter token", "{ 0000 }"},
		{"space inside date token", "{ YYYY }"},
		{"space inside cross-record token", "{Account .BillingCountryCode}-{0000}"},
		{"tab inside counter token", "{\t0000\t}"},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			_, _, err := Parse(tc.format)
			require.Error(t, err, "whitespace-contaminated format %q must be rejected", tc.format)
			assert.NotErrorIs(t, err, errNotImplemented)
		})
	}
}

// TestParse_MissingClosingBrace covers PRD §10.1 case 5b — an unclosed
// opening brace must produce an explicit parse error rather than
// silently consuming input to EOF.
func TestParse_MissingClosingBrace(t *testing.T) {
	t.Parallel()

	cases := []string{
		"AUTO-{0000",
		"AUTO-{YYYY",
		"{Account.BillingCountryCode-{0000}",
		"{RecordType.RecordTypeCode-{00}",
	}

	for _, format := range cases {
		format := format
		t.Run(format, func(t *testing.T) {
			t.Parallel()
			_, _, err := Parse(format)
			require.Error(t, err)
			assert.NotErrorIs(t, err, errNotImplemented)
		})
	}
}

// TestParse_CounterWidths covers PRD §10.1 case 6 — counter width 1..18
// must be accepted; 0 and 19+ rejected (§6.2 rule 2). Wave 1: all calls
// return the stub error, so both branches are RED.
func TestParse_CounterWidths(t *testing.T) {
	t.Parallel()

	// Width 1..18: accepted, CounterWidth reports the zero count.
	for width := 1; width <= 18; width++ {
		width := width
		t.Run("accepts width="+itoaForTest(width), func(t *testing.T) {
			t.Parallel()
			format := "AUTO-" + repeat0(width)
			spec, _, err := Parse(format)
			require.NoError(t, err, "width %d must be accepted", width)
			assert.Equal(t, width, spec.CounterWidth,
				"CounterWidth must equal declared zero count for width=%d", width)
		})
	}

	// Width 19, 20: rejected.
	for _, width := range []int{19, 20, 25} {
		width := width
		t.Run("rejects width="+itoaForTest(width), func(t *testing.T) {
			t.Parallel()
			format := "AUTO-" + repeat0(width)
			_, _, err := Parse(format)
			require.Error(t, err)
			assert.NotErrorIs(t, err, errNotImplemented)
		})
	}
}

// TestParse_DateTokens covers PRD §10.1 case 7 — {YYYY}/{YY}/{MM}/{DD}
// must be recognized; anything else is rejected.
func TestParse_DateTokens(t *testing.T) {
	t.Parallel()

	validCases := []struct {
		name   string
		format string
	}{
		{"YYYY only", "PFX-{YYYY}-{0000}"},
		{"YY only", "PFX-{YY}-{0000}"},
		{"MM only", "PFX-{MM}-{0000}"},
		{"DD only", "PFX-{DD}-{0000}"},
		{"full date concatenation", "PFX-{YYYY}{MM}{DD}-{0000}"},
	}

	for _, tc := range validCases {
		tc := tc
		t.Run("accepts/"+tc.name, func(t *testing.T) {
			t.Parallel()
			_, _, err := Parse(tc.format)
			require.NoError(t, err, "valid date-containing format %q rejected", tc.format)
		})
	}

	invalidCases := []struct {
		name   string
		format string
	}{
		{"lowercase yyyy", "PFX-{yyyy}-{0000}"},
		{"unknown HH", "PFX-{HH}-{0000}"},
		{"unknown mm mixed case", "PFX-{Mm}-{0000}"},
	}

	for _, tc := range invalidCases {
		tc := tc
		t.Run("rejects/"+tc.name, func(t *testing.T) {
			t.Parallel()
			_, _, err := Parse(tc.format)
			require.Error(t, err)
			assert.NotErrorIs(t, err, errNotImplemented)
		})
	}
}

// TestParse_RejectsDepth3FkChain covers PRD §10.1 case 14 — depth-3
// cross-record chains must be rejected at parse time
// (§1.2 non-goal; §6.2 rule 8).
//
// This test targets the nested-FK rejection path. The input format
// itself uses depth-1 syntax; the depth check is applied when the
// parser cross-walks the FK chain against metadata. In Wave 1 the stub
// returns errNotImplemented. The deeper rule exercise lives in the
// integration suite, but the unit contract is: a CrossRecordRef whose
// `FkChains` contains a 3-hop entry must not be accepted by any parser
// validation helper.
func TestParse_RejectsDepth3FkChain(t *testing.T) {
	t.Parallel()

	// The parser cannot discover chain depth from the format literal
	// alone (`{Account.BillingCountryCode}` always looks depth-1). The
	// enforcement happens when the parser validates the resolved
	// `CrossRecordRef.FkChains`. We exercise the contract by asserting
	// Parse rejects a format that would require a depth-3 chain in a
	// realistic schema; because Wave 1 is a stub, both arms fail RED.
	format := "{Account.VeryDeeplyNested.SomeField}-{0000}"
	_, _, err := Parse(format)
	require.Error(t, err, "depth>2 cross-record syntax must be rejected")
	assert.NotErrorIs(t, err, errNotImplemented)
}

// TestParse_RejectsUnknownToken covers PRD §10.1 case 15 — unknown
// bare identifiers inside braces (not a counter, not a date, not
// `{Object.Field}` shape) must be rejected (§6.2 collectively).
func TestParse_RejectsUnknownToken(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name   string
		format string
	}{
		{"bare word token", "{UNKNOWN}-{0000}"},
		{"numeric token", "{1234}-{0000}"},
		{"object-only (missing field)", "{Account.}-{0000}"},
		{"field-only (missing object)", "{.BillingCountryCode}-{0000}"},
		{"double dot", "{Account..Field}-{0000}"},
		{"mixed digits in identifier start", "{9Foo.Bar}-{0000}"},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			_, _, err := Parse(tc.format)
			require.Error(t, err)
			assert.NotErrorIs(t, err, errNotImplemented)
		})
	}
}

// --- test-local helpers (not exported) ---

// itoaForTest avoids pulling strconv into the main package under test
// and keeps test output readable in `-run` output.
func itoaForTest(n int) string {
	if n == 0 {
		return "0"
	}
	neg := n < 0
	if neg {
		n = -n
	}
	var buf [20]byte
	i := len(buf)
	for n > 0 {
		i--
		buf[i] = byte('0' + n%10)
		n /= 10
	}
	if neg {
		i--
		buf[i] = '-'
	}
	return string(buf[i:])
}

// repeat0 returns a string of `width` zero characters wrapped in braces,
// e.g. repeat0(4) → "{0000}".
func repeat0(width int) string {
	b := make([]byte, width+2)
	b[0] = '{'
	for i := 1; i <= width; i++ {
		b[i] = '0'
	}
	b[width+1] = '}'
	return string(b)
}
