package bootstrap

import (
	"bytes"
	"context"
	"errors"
	"log"
	"regexp"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"

	"github.com/nexuscrm/backend/internal/features/automation/autonumber"
)

// TestDeriveCounterRegex_MatchesExpectedShapes spot-checks regex
// derivation for a representative subset of the 132 seeded formats.
// Each assertion pins the full anchored pattern so regressions in the
// token → regex translation surface immediately.
func TestDeriveCounterRegex_MatchesExpectedShapes(t *testing.T) {
	tests := []struct {
		name     string
		format   string
		wantRe   string
		wantMatch map[string]int64 // input → captured counter
		wantNoMatch []string       // inputs that MUST NOT match
	}{
		{
			name:    "simple counter only",
			format:  "{00000000}",
			wantRe:  `^(\d{8,})$`,
			wantMatch: map[string]int64{
				"00000001":   1,
				"12345678":   12345678,
				"1234567890": 1234567890, // grows past declared width
			},
			wantNoMatch: []string{"1234567", "abcd1234", "12345678x"},
		},
		{
			name:   "literal prefix + counter",
			format: "CN-{0000}",
			wantRe: `^CN-(\d{4,})$`,
			wantMatch: map[string]int64{
				"CN-0001":  1,
				"CN-9999":  9999,
				"CN-99999": 99999, // overflow grows naturally in renderer
			},
			wantNoMatch: []string{"0001", "CN-1", "cn-0001"},
		},
		{
			name:   "single-zero counter (unbounded growth)",
			format: "{0}",
			wantRe: `^(\d{1,})$`,
			wantMatch: map[string]int64{
				"1":    1,
				"1234": 1234,
			},
			wantNoMatch: []string{"", "abc", "1a"},
		},
		{
			name:   "Account IBG format (date + counter)",
			format: "IBG-B-{YYYY}{MM}{DD}-{000000}",
			wantRe: `^IBG-B-\d{4}\d{2}\d{2}-(\d{6,})$`,
			wantMatch: map[string]int64{
				"IBG-B-20260418-000042": 42,
				"IBG-B-20260101-999999": 999999,
			},
			wantNoMatch: []string{
				"IBG-B-2026-04-18-000042", // hyphens in date
				"IBG-B-20260418-00042",    // 5 digits, below minimum
				"IBG-B-custom-override",   // literal midfix
			},
		},
		{
			name:   "compound contract name (cross-record + RT + date)",
			format: "{Account.BillingCountryCode}-{RecordType.RecordTypeCode}-{YY}{MM}{DD}-{00}",
			// Cross-record and record-type tokens collapse to `.*`
			wantRe: `^.*-.*-\d{2}\d{2}\d{2}-(\d{2,})$`,
			wantMatch: map[string]int64{
				"CN-CSA-260418-07": 7,
				"US-NDA-260101-99": 99,
				"-ORD-260418-42":   42, // empty CountryCode resolves to empty; `.*` matches
			},
			wantNoMatch: []string{
				"CN-CSA-260418-7", // 1 digit, below minimum
				"CN-CSA-2604-42",  // date too short
			},
		},
		{
			name:   "YY-only date prefix",
			format: "ARR#2026-04-13-{00000}",
			// The `2026-04-13` bytes are literal (QuoteMeta-escaped).
			wantRe: `^ARR#2026-04-13-(\d{5,})$`,
			wantMatch: map[string]int64{
				"ARR#2026-04-13-00001": 1,
			},
			wantNoMatch: []string{"2026-04-13-00001"},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			spec, _, err := autonumber.Parse(tt.format)
			if err != nil {
				t.Fatalf("Parse(%q) unexpected error: %v", tt.format, err)
			}
			re, err := deriveCounterRegex(spec)
			if err != nil {
				t.Fatalf("deriveCounterRegex: %v", err)
			}
			if re.String() != tt.wantRe {
				t.Errorf("regex mismatch:\n  got  %q\n  want %q", re.String(), tt.wantRe)
			}
			for in, wantN := range tt.wantMatch {
				m := re.FindStringSubmatch(in)
				if m == nil {
					t.Errorf("regex %q did not match %q (expected counter=%d)", re.String(), in, wantN)
					continue
				}
				if len(m) < 2 {
					t.Errorf("regex %q matched %q but no capture group", re.String(), in)
					continue
				}
				got := m[1]
				// Parse to int to avoid string-compare drift when
				// the format emits leading zeros.
				gotN := parseIntOrFatal(t, got)
				if gotN != wantN {
					t.Errorf("counter: got=%d want=%d (full match=%v)", gotN, wantN, m)
				}
			}
			for _, in := range tt.wantNoMatch {
				if re.FindStringSubmatch(in) != nil {
					t.Errorf("regex %q UNEXPECTEDLY matched %q", re.String(), in)
				}
			}
		})
	}
}

// parseIntOrFatal keeps the test DRY — no fancy error handling.
func parseIntOrFatal(t *testing.T, s string) int64 {
	t.Helper()
	var n int64
	for _, c := range s {
		if c < '0' || c > '9' {
			t.Fatalf("not a digit in %q", s)
		}
		n = n*10 + int64(c-'0')
	}
	return n
}

// TestDeriveCounterRegex_NoCounterIsError — the parser will reject a
// format without a counter token, so we never hit this path via Parse.
// The unit test pokes the spec struct directly to make sure the guard
// fires instead of silently producing a bogus regex.
func TestDeriveCounterRegex_NoCounterIsError(t *testing.T) {
	spec := autonumber.FormatSpec{
		Raw: "literal-only",
		Tokens: []autonumber.Token{
			{Kind: autonumber.TokenKindLiteral, Lit: "X-"},
			{Kind: autonumber.TokenKindDate, DateName: "YYYY"},
		},
		CounterWidth: 0,
	}
	_, err := deriveCounterRegex(spec)
	if err == nil {
		t.Fatal("expected error when spec has no counter token, got nil")
	}
}

// TestScanLiveMaxCounter_FailsClosedOnUnparseableRow verifies PRD §8.2
// step 3.c fail-closed behavior — any non-NULL / non-empty row that
// does not match the derived regex aborts the whole seed.
// Post-v2: match query returns only the matching rows (TiDB REGEXP
// pushdown); the strict abort-on-first-legacy query surfaces the
// unparseable row with deterministic ORDER BY Id.
func TestScanLiveMaxCounter_FailsClosedOnUnparseableRow(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Account", "Account_ID__c").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))

	// Match query: 2 matching rows.
	mock.ExpectQuery("SELECT `Id`, `Account_ID__c` FROM `Account`.*REGEXP").
		WithArgs(`^IBG-B-[0-9]{4}[0-9]{2}[0-9]{2}-([0-9]{6})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Account_ID__c"}).
			AddRow("a001", "IBG-B-20260418-000001").
			AddRow("a003", "IBG-B-20260418-000003"))

	// Strict abort-on-first-legacy: surfaces the unparseable row.
	mock.ExpectQuery("SELECT `Id`, `Account_ID__c` FROM `Account`.*NOT REGEXP.*ORDER BY `Id` LIMIT 1").
		WithArgs(`^IBG-B-[0-9]{4}[0-9]{2}[0-9]{2}-([0-9]{6})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Account_ID__c"}).
			AddRow("a002", "IBG-B-2021-custom-override"))

	derived := regexp.MustCompile(`^IBG-B-\d{4}\d{2}\d{2}-(\d{6})$`)
	_, err = scanLiveMaxCounter(
		context.Background(), db,
		"Account", "Account_ID__c",
		derived, "fld_account_id",
	)
	if err == nil {
		t.Fatal("expected fail-closed error, got nil")
	}
	msg := err.Error()
	mustContain := []string{
		"autonumber seed",
		"Account.Account_ID__c",
		"fld_account_id",
		"a002",
		"IBG-B-2021-custom-override",
		"Fix the row",
	}
	for _, want := range mustContain {
		if !strings.Contains(msg, want) {
			t.Errorf("error message missing %q\n  got: %s", want, msg)
		}
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestScanLiveMaxCounter_EmptyColumnIsZero verifies the no-data fast
// path — a column that exists but has zero non-NULL non-empty rows
// returns liveMax=0, no error. Post-v2: match query returns 0 rows AND
// strict abort-query returns sql.ErrNoRows.
func TestScanLiveMaxCounter_EmptyColumnIsZero(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Case", "CaseNumber").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	mock.ExpectQuery("SELECT `Id`, `CaseNumber` FROM `Case`.*REGEXP").
		WithArgs(`^([0-9]{8})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "CaseNumber"}))
	mock.ExpectQuery("SELECT `Id`, `CaseNumber` FROM `Case`.*NOT REGEXP.*ORDER BY `Id` LIMIT 1").
		WithArgs(`^([0-9]{8})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "CaseNumber"}))

	derived := regexp.MustCompile(`^(\d{8})$`)
	got, err := scanLiveMaxCounter(
		context.Background(), db,
		"Case", "CaseNumber",
		derived, "fld_case",
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != 0 {
		t.Errorf("liveMax: got %d, want 0", got)
	}
}

// TestScanLiveMaxCounter_MissingTableIsZero — applyAutoNumberFormats
// silently skips fields whose objects aren't physically present yet.
// Seed must do the same (returns 0, no error) — PRD §8.2 + matches
// schema.go:1855+ pattern.
func TestScanLiveMaxCounter_MissingTableIsZero(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("MissingObject", "Name").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(0))

	derived := regexp.MustCompile(`^(\d{8})$`)
	got, err := scanLiveMaxCounter(
		context.Background(), db,
		"MissingObject", "Name",
		derived, "fld_missing",
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != 0 {
		t.Errorf("liveMax: got %d, want 0", got)
	}
}

// TestScanLivePerPrefixCounters_GroupsByPrefix pins the per_prefix
// cutover-absorption contract (Codex adversarial round-2 finding):
// live data with multiple (country, date) prefix buckets must produce
// one counter row per bucket, keyed by the exact rendered prefix, with
// NextValue = max_counter_in_bucket + 1.
func TestScanLivePerPrefixCounters_GroupsByPrefix(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Quote", "Name").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	// Match query returns all 6 matching rows.
	mock.ExpectQuery("SELECT `Id`, `Name` FROM `Quote`.*REGEXP").
		WithArgs(`^.*-QT-[0-9]{2}[0-9]{2}[0-9]{2}-([0-9]{2,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Name"}).
			AddRow("r1", "JP-QT-260418-01").
			AddRow("r2", "JP-QT-260418-07").
			AddRow("r3", "JP-QT-260418-05").
			AddRow("r4", "US-QT-260418-02").
			AddRow("r5", "US-QT-260419-01").
			AddRow("r6", "JP-QT-260417-99"))
	// Strict abort-on-first-legacy: no legacy rows present.
	mock.ExpectQuery("SELECT `Id`, `Name` FROM `Quote`.*NOT REGEXP.*ORDER BY `Id` LIMIT 1").
		WithArgs(`^.*-QT-[0-9]{2}[0-9]{2}[0-9]{2}-([0-9]{2,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Name"}))

	// Regex matching the Quote.Name format.
	derived := regexp.MustCompile(`^.*-QT-\d{2}\d{2}\d{2}-(\d{2,})$`)

	got, err := scanLivePerPrefixCounters(
		context.Background(), db,
		"Quote", "Name",
		derived, "fld_quote_name",
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	want := map[string]int64{
		"JP-QT-260418-": 7,  // rows r1 (01), r2 (07), r3 (05) → max 7
		"US-QT-260418-": 2,  // row r4
		"US-QT-260419-": 1,  // row r5
		"JP-QT-260417-": 99, // row r6
	}
	if len(got) != len(want) {
		t.Errorf("bucket count: got %d, want %d (got=%v)", len(got), len(want), got)
	}
	for prefix, wantMax := range want {
		if got[prefix] != wantMax {
			t.Errorf("bucket %q: got max=%d, want %d", prefix, got[prefix], wantMax)
		}
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestScanLiveMaxCounter_TolerantSkipsLegacyRows — production-migration
// escape hatch: when AUTONUMBER_SEED_TOLERATE_LEGACY=true is set
// (typically in the deploy environment's entrypoint), unparseable rows
// are logged as WARN and skipped instead of aborting the whole seed.
// max comes from the rows that DO match the derived regex. Required
// because staging/prod legacy data (e.g. Contract.ContractNumber
// containing "CON-00000694" from a pre-v2 listener) would otherwise
// crash-loop every backend pod at entrypoint apply.
func TestScanLiveMaxCounter_TolerantSkipsLegacyRows(t *testing.T) {
	t.Setenv("AUTONUMBER_SEED_TOLERATE_LEGACY", "true")

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Contract", "ContractNumber").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))

	// Match query returns 2 matching rows (c1=692, c3=695). c3 is
	// the max under tolerant mode.
	mock.ExpectQuery("SELECT `Id`, `ContractNumber` FROM `Contract`.*REGEXP").
		WithArgs(`^([0-9]{8,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ContractNumber"}).
			AddRow("c1", "00000692").
			AddRow("c3", "00000695"))
	// Tolerant legacy sample query returns the 1 legacy row (c2,
	// CON-00000694). Under limit so no COUNT(*) query fires.
	mock.ExpectQuery("SELECT `Id`, `ContractNumber` FROM `Contract`.*NOT REGEXP.*ORDER BY `Id` LIMIT").
		WithArgs(`^([0-9]{8,})$`, legacySampleLimit+1).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ContractNumber"}).
			AddRow("c2", "CON-00000694"))

	derived := regexp.MustCompile(`^(\d{8,})$`)
	got, err := scanLiveMaxCounter(
		context.Background(), db,
		"Contract", "ContractNumber",
		derived, "fld_contract_contractnumber",
	)
	if err != nil {
		t.Fatalf("tolerant mode should not error on legacy row, got: %v", err)
	}
	if got != 695 {
		t.Errorf("liveMax under tolerant mode: got %d, want 695 (max of matched rows)", got)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestScanLivePerPrefixCounters_FailsClosedOnUnparseable mirrors
// scanLiveMaxCounter's fail-closed behavior for per_prefix.
func TestScanLivePerPrefixCounters_FailsClosedOnUnparseable(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Quote", "Name").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	// Match query: 1 matching row.
	mock.ExpectQuery("SELECT `Id`, `Name` FROM `Quote`.*REGEXP").
		WithArgs(`^.*-QT-[0-9]{2}[0-9]{2}[0-9]{2}-([0-9]{2,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Name"}).
			AddRow("r1", "JP-QT-260418-01"))
	// Strict abort-on-first-legacy: returns the malformed row.
	mock.ExpectQuery("SELECT `Id`, `Name` FROM `Quote`.*NOT REGEXP.*ORDER BY `Id` LIMIT 1").
		WithArgs(`^.*-QT-[0-9]{2}[0-9]{2}[0-9]{2}-([0-9]{2,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Name"}).
			AddRow("r_bad", "MALFORMED-VALUE"))

	derived := regexp.MustCompile(`^.*-QT-\d{2}\d{2}\d{2}-(\d{2,})$`)
	_, err = scanLivePerPrefixCounters(
		context.Background(), db,
		"Quote", "Name",
		derived, "fld_quote_name",
	)
	if err == nil {
		t.Fatal("expected fail-closed error on unparseable row, got nil")
	}
	if !strings.Contains(err.Error(), "MALFORMED-VALUE") {
		t.Errorf("error should cite the offending value: %v", err)
	}
}

// TestScanLivePerPrefixCounters_TolerantSkipsLegacyRows — mirrors
// TestScanLiveMaxCounter_TolerantSkipsLegacyRows for the per_prefix
// scan path. Under AUTONUMBER_SEED_TOLERATE_LEGACY=true the malformed
// row must NOT abort; the surviving per-prefix max must still be
// reported.
func TestScanLivePerPrefixCounters_TolerantSkipsLegacyRows(t *testing.T) {
	t.Setenv("AUTONUMBER_SEED_TOLERATE_LEGACY", "true")

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Quote", "Name").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))

	// Match query: 2 matching rows in bucket "JP-QT-260418-" (max 07).
	mock.ExpectQuery("SELECT `Id`, `Name` FROM `Quote`.*REGEXP").
		WithArgs(`^.*-QT-[0-9]{2}[0-9]{2}[0-9]{2}-([0-9]{2,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Name"}).
			AddRow("r1", "JP-QT-260418-01").
			AddRow("r2", "JP-QT-260418-07"))
	// Tolerant sample query: 1 legacy row (under limit — no COUNT follows).
	mock.ExpectQuery("SELECT `Id`, `Name` FROM `Quote`.*NOT REGEXP.*ORDER BY `Id` LIMIT").
		WithArgs(`^.*-QT-[0-9]{2}[0-9]{2}[0-9]{2}-([0-9]{2,})$`, legacySampleLimit+1).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Name"}).
			AddRow("r_bad", "LEGACY-QUOTE-NAME"))

	derived := regexp.MustCompile(`^.*-QT-\d{2}\d{2}\d{2}-(\d{2,})$`)
	got, err := scanLivePerPrefixCounters(
		context.Background(), db,
		"Quote", "Name",
		derived, "fld_quote_name",
	)
	if err != nil {
		t.Fatalf("tolerant mode should not error on legacy row, got: %v", err)
	}
	// The capture-group regex reconstructs the prefix as
	// everything-matched minus the counter capture. For
	// "JP-QT-260418-01" → prefix "JP-QT-260418-" (plus empty tail).
	const wantPrefix = "JP-QT-260418-"
	if len(got) != 1 {
		t.Fatalf("got %d prefix buckets, want 1 (legacy row should have been skipped): %v", len(got), got)
	}
	if m, ok := got[wantPrefix]; !ok || m != 7 {
		t.Errorf("bucket %q: got %d, want 7 (max of matched rows)", wantPrefix, m)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// captureLog redirects the default logger to a buffer for the duration
// of the test. Restores the previous output on cleanup.
func captureLog(t *testing.T) *bytes.Buffer {
	t.Helper()
	buf := &bytes.Buffer{}
	prev := log.Writer()
	log.SetOutput(buf)
	t.Cleanup(func() { log.SetOutput(prev) })
	return buf
}

// TestSqlRegexFromGoRegex_TranslatesDerivedShapes pins the translation
// for every distinct regex shape deriveCounterRegex emits today. If a
// new format token is added upstream, one of these assertions will fail
// and prompt extending sqlRegexFromGoRegex in the same PR rather than
// silently diverging from TiDB REGEXP at boot time.
func TestSqlRegexFromGoRegex_TranslatesDerivedShapes(t *testing.T) {
	tests := []struct {
		name, in, want string
	}{
		{"counter-only", `^(\d{8,})$`, `^([0-9]{8,})$`},
		{"literal-prefix-date", `^2026-04-17-(\d{7,})$`, `^2026-04-17-([0-9]{7,})$`},
		{"literal-with-hash", `^ARR#2026-04-13-(\d{5,})$`, `^ARR#2026-04-13-([0-9]{5,})$`},
		{"record-type-then-date", `^.*-OF-\d{2}\d{2}\d{2}(\d{2,})$`, `^.*-OF-[0-9]{2}[0-9]{2}[0-9]{2}([0-9]{2,})$`},
		{"field-then-literal-date", `^.*-QT-\d{6}-(\d{2,})$`, `^.*-QT-[0-9]{6}-([0-9]{2,})$`},
		{"escaped-literal-dot", `^PREFIX\.(\d{4,})$`, `^PREFIX\.([0-9]{4,})$`},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got, err := sqlRegexFromGoRegex(regexp.MustCompile(tc.in))
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if got != tc.want {
				t.Errorf("input %q\n  got:  %q\n  want: %q", tc.in, got, tc.want)
			}
		})
	}
}

// TestSqlRegexFromGoRegex_RejectsUnknownEscape — Round-1 Pass A LOW.
// Guards against a future format token adding `\s` / `\w` / etc. and
// silently diverging across engines. Defensive-fail posture means boot
// fails loud rather than tolerating silent semantic drift.
func TestSqlRegexFromGoRegex_RejectsUnknownEscape(t *testing.T) {
	re := regexp.MustCompile(`^\w+(\d{5,})$`)
	_, err := sqlRegexFromGoRegex(re)
	if err == nil {
		t.Fatal("expected error for unsupported escape \\w, got nil")
	}
	if !strings.Contains(err.Error(), `\w`) {
		t.Errorf("error should cite the offending escape: %v", err)
	}
	if !strings.Contains(err.Error(), "extend the translator") {
		t.Errorf("error should guide the fix: %v", err)
	}
}

// TestMatchCountCeiling_DefaultAndOverride — Round-2 HIGH-N2.
// Env override must honor AUTONUMBER_SEED_MATCH_CEILING for ops
// configurability; invalid / zero values fall back to the default.
func TestMatchCountCeiling_DefaultAndOverride(t *testing.T) {
	t.Run("default when unset", func(t *testing.T) {
		t.Setenv(envMatchCountCeiling, "")
		if got := matchCountCeiling(); got != defaultMatchCountCeiling {
			t.Errorf("unset: got %d, want %d", got, defaultMatchCountCeiling)
		}
	})
	t.Run("override honored", func(t *testing.T) {
		t.Setenv(envMatchCountCeiling, "50000")
		if got := matchCountCeiling(); got != 50000 {
			t.Errorf("override: got %d, want 50000", got)
		}
	})
	t.Run("garbage falls back to default", func(t *testing.T) {
		t.Setenv(envMatchCountCeiling, "not-a-number")
		if got := matchCountCeiling(); got != defaultMatchCountCeiling {
			t.Errorf("garbage: got %d, want %d", got, defaultMatchCountCeiling)
		}
	})
	t.Run("zero falls back to default", func(t *testing.T) {
		t.Setenv(envMatchCountCeiling, "0")
		if got := matchCountCeiling(); got != defaultMatchCountCeiling {
			t.Errorf("zero: got %d, want %d", got, defaultMatchCountCeiling)
		}
	})
}

// TestScanLiveMaxCounter_TolerantCapsLogOutput — production-migration
// hotfix: when a large table (e.g. Interface_Log__c at 2.87M rows)
// contains many pre-cutover legacy rows, the tolerant-mode seed must
// cap per-row WARN output to avoid a boot-time log storm that keeps
// HTTP listen from binding before the ALB health check fails the task.
// Contract: ≤ legacySampleLimit per-row samples + 1 summary line
// citing the total skipped count.
func TestScanLiveMaxCounter_TolerantCapsLogOutput(t *testing.T) {
	t.Setenv("AUTONUMBER_SEED_TOLERATE_LEGACY", "true")
	logBuf := captureLog(t)

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Interface_Log__c", "Name").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))

	// Match query returns the single matching row (cur-1=42).
	mock.ExpectQuery("SELECT `Id`, `Name` FROM `Interface_Log__c`.*REGEXP").
		WithArgs(`^2026-04-17-([0-9]{7,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Name"}).
			AddRow("cur-1", "2026-04-17-0000042"))

	// Tolerant sample query returns LIMIT+1 (=6) rows, signaling
	// the legacy tail exceeds the sample cap. Summary is reported as
	// a lower bound via `skipped at least N` — no follow-up COUNT(*)
	// query is fired because that query cannot early-terminate on
	// NOT REGEXP over a large legacy tail and cancels the fast-path
	// gain (see logLegacyTolerant doc comment for the perf trade).
	sampleRows := sqlmock.NewRows([]string{"Id", "Name"})
	for i := 1; i <= legacySampleLimit+1; i++ {
		sampleRows.AddRow("legacy-"+string(rune('a'+i-1)), "2022-08-17-0014760")
	}
	mock.ExpectQuery("SELECT `Id`, `Name` FROM `Interface_Log__c`.*NOT REGEXP.*ORDER BY `Id` LIMIT").
		WithArgs(`^2026-04-17-([0-9]{7,})$`, legacySampleLimit+1).
		WillReturnRows(sampleRows)

	derived := regexp.MustCompile(`^2026-04-17-(\d{7,})$`)
	got, err := scanLiveMaxCounter(
		context.Background(), db,
		"Interface_Log__c", "Name",
		derived, "fld_interface_log_name",
	)
	if err != nil {
		t.Fatalf("tolerant mode should not error, got: %v", err)
	}
	if got != 42 {
		t.Errorf("liveMax: got %d, want 42 (from matched row)", got)
	}

	out := logBuf.String()
	perRow := strings.Count(out, "does not match derived regex")
	if perRow != legacySampleLimit {
		t.Errorf("per-row WARN samples: got %d, want %d (capped); full log:\n%s",
			perRow, legacySampleLimit, out)
	}
	if !strings.Contains(out, "skipped at least 6 legacy rows") {
		t.Errorf("summary line missing \"skipped at least 6 legacy rows\"; full log:\n%s", out)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestScanLiveMaxCounter_TolerantNoSummaryWhenUnderLimit — when the
// legacy row count is ≤ legacySampleLimit, every row is logged as a
// sample and NO summary line is emitted (it would double-count the
// samples and confuse operators).
func TestScanLiveMaxCounter_TolerantNoSummaryWhenUnderLimit(t *testing.T) {
	t.Setenv("AUTONUMBER_SEED_TOLERATE_LEGACY", "true")
	logBuf := captureLog(t)

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Contract", "ContractNumber").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	// Match query: 1 matching row.
	mock.ExpectQuery("SELECT `Id`, `ContractNumber` FROM `Contract`.*REGEXP").
		WithArgs(`^([0-9]{8,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ContractNumber"}).
			AddRow("c1", "00000700"))
	// Sample query: 1 legacy row → under limit → no COUNT query
	// follows; summary suppressed.
	mock.ExpectQuery("SELECT `Id`, `ContractNumber` FROM `Contract`.*NOT REGEXP.*ORDER BY `Id` LIMIT").
		WithArgs(`^([0-9]{8,})$`, legacySampleLimit+1).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ContractNumber"}).
			AddRow("c_legacy", "CON-00000694"))

	derived := regexp.MustCompile(`^(\d{8,})$`)
	if _, err := scanLiveMaxCounter(
		context.Background(), db,
		"Contract", "ContractNumber",
		derived, "fld_contract_contractnumber",
	); err != nil {
		t.Fatalf("tolerant mode should not error: %v", err)
	}

	out := logBuf.String()
	if strings.Contains(out, "skipped") {
		t.Errorf("summary line must not appear when count ≤ limit; full log:\n%s", out)
	}
	if strings.Count(out, "does not match derived regex") != 1 {
		t.Errorf("expected 1 per-row sample; full log:\n%s", out)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestScanLiveMaxCounter_TolerantFlushesOnMidScanError — regression
// guard for Pass B adversarial MEDIUM: if rows.Err() or a row-Scan
// error fires AFTER some legacy rows have been observed, operators
// must still see the summary line so they know the iteration was
// truncated. Without a deferred flush, ops would see 5 samples and
// silently lose count of the skipped tail.
func TestScanLiveMaxCounter_TolerantFlushesOnMidScanError(t *testing.T) {
	t.Setenv("AUTONUMBER_SEED_TOLERATE_LEGACY", "true")
	logBuf := captureLog(t)

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Interface_Log__c", "Name").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))

	// Match query returns 0 matching rows (simulates pre-cutover
	// state where only legacy rows exist).
	mock.ExpectQuery("SELECT `Id`, `Name` FROM `Interface_Log__c`.*REGEXP").
		WithArgs(`^2026-04-17-([0-9]{7,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Name"}))

	// Tolerant sample query: 6 legacy rows streamed, then
	// CloseError simulates a mid-stream driver failure. Deferred
	// sampler.flush() inside logLegacyTolerant must still emit the
	// summary for the rows it DID observe.
	sampleRows := sqlmock.NewRows([]string{"Id", "Name"})
	for i := 1; i <= legacySampleLimit+1; i++ {
		sampleRows.AddRow("legacy-"+string(rune('a'+i-1)), "2022-08-17-0014760")
	}
	sampleRows.CloseError(errors.New("simulated driver failure"))
	mock.ExpectQuery("SELECT `Id`, `Name` FROM `Interface_Log__c`.*NOT REGEXP.*ORDER BY `Id` LIMIT").
		WithArgs(`^2026-04-17-([0-9]{7,})$`, legacySampleLimit+1).
		WillReturnRows(sampleRows)

	derived := regexp.MustCompile(`^2026-04-17-(\d{7,})$`)
	_, err = scanLiveMaxCounter(
		context.Background(), db,
		"Interface_Log__c", "Name",
		derived, "fld_interface_log_name",
	)
	if err == nil {
		t.Fatal("expected driver-failure error, got nil")
	}
	if !strings.Contains(err.Error(), "legacy iteration") {
		t.Errorf("error should be the legacy-iteration wrap: %v", err)
	}

	// Summary MUST be emitted even though iteration errored —
	// deferred flush guarantees it. sampler.count equals the rows
	// observed before CloseError fired (legacySampleLimit+1), and
	// atLeast=true marks the count as a lower bound (Round-1 code
	// review Pass B HIGH-2 — "skipped 6" alone would mislead ops
	// into thinking 6 is the true total).
	out := logBuf.String()
	if !strings.Contains(out, "skipped at least 6 legacy rows") {
		t.Errorf("summary should be marked as lower bound on mid-scan error path; full log:\n%s", out)
	}
}

// TestLogLegacyTolerant_DoesNotFireCountQuery — performance regression
// guard. The legacy-tail COUNT(*) NOT REGEXP query was removed (it
// could not early-terminate on large legacy tails and cancelled the
// fast-path perf gain — staging's 4.47M-row Interface_Log__c saw
// ApplyPacks regress from 67s → 92s with it). Sampler summary now
// reports `skipped at least N` whenever the sample query hits its
// LIMIT cap. If a future change re-introduces the COUNT query,
// sqlmock's unmet-expectation guard is one-sided, so pin the forbidden
// behavior with an explicit count of executed queries.
func TestLogLegacyTolerant_DoesNotFireCountQuery(t *testing.T) {
	t.Setenv("AUTONUMBER_SEED_TOLERATE_LEGACY", "true")

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Contract", "ContractNumber").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	mock.ExpectQuery("SELECT `Id`, `ContractNumber` FROM `Contract`.*REGEXP").
		WithArgs(`^([0-9]{8,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ContractNumber"}).
			AddRow("c1", "00000700"))
	// Sample query returns 6 legacy rows (hits LIMIT+1).
	sampleRows := sqlmock.NewRows([]string{"Id", "ContractNumber"})
	for i := 1; i <= legacySampleLimit+1; i++ {
		sampleRows.AddRow("legacy-"+string(rune('a'+i-1)), "CON-00000694")
	}
	mock.ExpectQuery("SELECT `Id`, `ContractNumber` FROM `Contract`.*NOT REGEXP.*ORDER BY `Id` LIMIT").
		WithArgs(`^([0-9]{8,})$`, legacySampleLimit+1).
		WillReturnRows(sampleRows)
	// CRITICAL: do NOT register a COUNT(*) expectation. With
	// sqlmock's default strict mode, any query hitting the DB that
	// was not registered fails the test loudly ("query not matched"
	// on QueryRowContext). This gives us a regression guard.

	logBuf := captureLog(t)
	derived := regexp.MustCompile(`^(\d{8,})$`)
	if _, err := scanLiveMaxCounter(
		context.Background(), db,
		"Contract", "ContractNumber",
		derived, "fld_contract_contractnumber",
	); err != nil {
		t.Fatalf("tolerant mode should not error: %v", err)
	}

	out := logBuf.String()
	if !strings.Contains(out, "skipped at least 6 legacy rows") {
		t.Errorf("summary should report lower-bound `skipped at least 6`; full log:\n%s", out)
	}
	// The load-bearing regression guard is mock.ExpectationsWereMet()
	// combined with the DELIBERATE absence of any registered COUNT(*)
	// expectation: if a future change re-introduces the COUNT query,
	// sqlmock's strict mode (ordered=true by default) rejects it as
	// "query not expected", failing the test loudly.
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestScanLivePerPrefixCounters_TolerantCapsLogOutput — mirror of
// TestScanLiveMaxCounter_TolerantCapsLogOutput for the per_prefix
// scan path. Same boot-time log-storm pressure applies.
func TestScanLivePerPrefixCounters_TolerantCapsLogOutput(t *testing.T) {
	t.Setenv("AUTONUMBER_SEED_TOLERATE_LEGACY", "true")
	logBuf := captureLog(t)

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Quote", "Name").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))

	// Match query: 1 matching row. Tolerant sample query returns
	// LIMIT+1 legacy rows (=6), triggering the COUNT query which
	// reports the true total (8).
	mock.ExpectQuery("SELECT `Id`, `Name` FROM `Quote`.*REGEXP").
		WithArgs(`^.*-QT-[0-9]{2}[0-9]{2}[0-9]{2}-([0-9]{2,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Name"}).
			AddRow("cur-1", "JP-QT-260418-07"))
	sampleRows := sqlmock.NewRows([]string{"Id", "Name"})
	for i := 1; i <= legacySampleLimit+1; i++ {
		sampleRows.AddRow("legacy-"+string(rune('a'+i-1)), "LEGACY-QUOTE-NAME")
	}
	mock.ExpectQuery("SELECT `Id`, `Name` FROM `Quote`.*NOT REGEXP.*ORDER BY `Id` LIMIT").
		WithArgs(`^.*-QT-[0-9]{2}[0-9]{2}[0-9]{2}-([0-9]{2,})$`, legacySampleLimit+1).
		WillReturnRows(sampleRows)
	// No COUNT(*) query is fired — summary reports "skipped at
	// least 6" as a lower bound. See TestLogLegacyTolerant_DoesNotFireCountQuery.

	derived := regexp.MustCompile(`^.*-QT-\d{2}\d{2}\d{2}-(\d{2,})$`)
	if _, err := scanLivePerPrefixCounters(
		context.Background(), db,
		"Quote", "Name",
		derived, "fld_quote_name",
	); err != nil {
		t.Fatalf("tolerant mode should not error: %v", err)
	}

	out := logBuf.String()
	if got := strings.Count(out, "does not match derived regex"); got != legacySampleLimit {
		t.Errorf("per-row WARN samples: got %d, want %d (capped); full log:\n%s",
			got, legacySampleLimit, out)
	}
	if !strings.Contains(out, "skipped at least 6 legacy rows") {
		t.Errorf("summary line missing \"skipped at least 6 legacy rows\"; full log:\n%s", out)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestScanLiveMaxCounter_FastPath_AppliesCollateAndBoundParam pins the
// Round-2 CRITICAL-1 + CRITICAL-2 fixes: REGEXP must be wrapped in
// `COLLATE utf8mb4_bin` (case-sensitive parity with Go regexp) AND the
// pattern must be a bound `?` parameter (not string-interpolated).
// sqlmock.WithArgs asserts the latter — the call would fail with
// "args mismatch" if the regex were fmt.Sprintf'd into the query.
func TestScanLiveMaxCounter_FastPath_AppliesCollateAndBoundParam(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Contract", "ContractNumber").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	// Query must contain BOTH `COLLATE utf8mb4_bin` AND `REGEXP ?`.
	mock.ExpectQuery(".*COLLATE utf8mb4_bin REGEXP \\?").
		WithArgs(`^([0-9]{8,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ContractNumber"}).
			AddRow("c1", "00000700"))
	mock.ExpectQuery(".*COLLATE utf8mb4_bin NOT REGEXP \\?.*ORDER BY `Id` LIMIT 1").
		WithArgs(`^([0-9]{8,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ContractNumber"}))

	derived := regexp.MustCompile(`^(\d{8,})$`)
	got, err := scanLiveMaxCounter(
		context.Background(), db,
		"Contract", "ContractNumber",
		derived, "fld_contract_contractnumber",
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != 700 {
		t.Errorf("liveMax: got %d, want 700", got)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestScanLiveMaxCounter_FallsBackWhenMatchingExceedsCeiling pins the
// Round-2 HIGH-N2 fix: when the match count crosses the configured
// ceiling, the fast path aborts and routes to the iterate-all fallback,
// logging a WARN so ops can raise the ceiling or optimize.
func TestScanLiveMaxCounter_FallsBackWhenMatchingExceedsCeiling(t *testing.T) {
	// Lower the ceiling so we don't need thousands of mock rows.
	t.Setenv(envMatchCountCeiling, "3")
	logBuf := captureLog(t)

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Contract", "ContractNumber").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	// Fast-path match query returns 4 matching rows (> ceiling=3).
	mock.ExpectQuery("SELECT `Id`, `ContractNumber` FROM `Contract`.*REGEXP").
		WithArgs(`^([0-9]{8,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ContractNumber"}).
			AddRow("c1", "00000700").
			AddRow("c2", "00000701").
			AddRow("c3", "00000702").
			AddRow("c4", "00000703"))
	// Fallback path: iterate-all query (no REGEXP predicate).
	mock.ExpectQuery("SELECT `Id`, `ContractNumber` FROM `Contract` WHERE `ContractNumber` IS NOT NULL AND `ContractNumber` != ''$").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ContractNumber"}).
			AddRow("c1", "00000700").
			AddRow("c2", "00000701").
			AddRow("c3", "00000702").
			AddRow("c4", "00000703"))

	derived := regexp.MustCompile(`^(\d{8,})$`)
	got, err := scanLiveMaxCounter(
		context.Background(), db,
		"Contract", "ContractNumber",
		derived, "fld_contract_contractnumber",
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != 703 {
		t.Errorf("liveMax: got %d, want 703 (from fallback iterate-all)", got)
	}

	out := logBuf.String()
	if !strings.Contains(out, "matching rows exceeds ceiling") {
		t.Errorf("fallback WARN line missing; full log:\n%s", out)
	}
	if !strings.Contains(out, envMatchCountCeiling) {
		t.Errorf("fallback WARN should cite %s for ops guidance; full log:\n%s",
			envMatchCountCeiling, out)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestScanLiveMaxCounter_FallsBackOnEngineParityDivergence pins the
// Round-2 HIGH-3 residual: a row that TiDB's REGEXP returns but Go's
// regexp rejects (engine semantic drift) triggers the iterate-all
// fallback rather than producing a bogus max value.
func TestScanLiveMaxCounter_FallsBackOnEngineParityDivergence(t *testing.T) {
	logBuf := captureLog(t)

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Contract", "ContractNumber").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	// Fast-path match query returns a row that (in this mock)
	// pretends TiDB matched but Go's regexp rejects. In real life
	// this would be an exotic format/collation edge case; here
	// sqlmock simulates the divergence by returning a non-matching
	// value to the match-query expectation.
	mock.ExpectQuery("SELECT `Id`, `ContractNumber` FROM `Contract`.*REGEXP").
		WithArgs(`^([0-9]{8,})$`).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ContractNumber"}).
			AddRow("c1", "00000700").
			AddRow("c_bogus", "NOT-A-NUMBER"))
	// Fallback path: iterate-all query (strict mode would
	// fail-closed on NOT-A-NUMBER, so we also set tolerant).
	t.Setenv("AUTONUMBER_SEED_TOLERATE_LEGACY", "true")
	mock.ExpectQuery("SELECT `Id`, `ContractNumber` FROM `Contract` WHERE `ContractNumber` IS NOT NULL AND `ContractNumber` != ''$").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "ContractNumber"}).
			AddRow("c1", "00000700").
			AddRow("c_bogus", "NOT-A-NUMBER"))

	derived := regexp.MustCompile(`^(\d{8,})$`)
	got, err := scanLiveMaxCounter(
		context.Background(), db,
		"Contract", "ContractNumber",
		derived, "fld_contract_contractnumber",
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != 700 {
		t.Errorf("liveMax: got %d, want 700 (fallback computed max correctly)", got)
	}

	out := logBuf.String()
	if !strings.Contains(out, "engine parity divergence") {
		t.Errorf("fallback WARN line missing; full log:\n%s", out)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestReadLegacyCurrent_AutoNumberSource covers the scope=global
// auto_number-source path.
func TestReadLegacyCurrent_AutoNumberSource(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.TABLES").
		WithArgs("_System_AutoNumber").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	mock.ExpectQuery("SELECT `CurrentNumber` FROM `_System_AutoNumber`").
		WithArgs("Account", "Account_ID__c").
		WillReturnRows(sqlmock.NewRows([]string{"CurrentNumber"}).AddRow(4100))

	got, err := readLegacyCurrent(
		context.Background(), db,
		"Account", "Account_ID__c",
		map[string]string{"Account.Account_ID__c": LegacySourceAutoNumber},
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != 4100 {
		t.Errorf("legacyCurrent: got %d, want 4100", got)
	}
}

// TestReadLegacyCurrent_CompoundSource covers the compound path — seed
// takes MAX across prefix rows for the object.
func TestReadLegacyCurrent_CompoundSource(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.TABLES").
		WithArgs("_System_CompoundAutoNumber").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	mock.ExpectQuery("SELECT MAX\\(`CurrentNumber`\\) FROM `_System_CompoundAutoNumber`").
		WithArgs("Contract").
		WillReturnRows(sqlmock.NewRows([]string{"max"}).AddRow(87))

	got, err := readLegacyCurrent(
		context.Background(), db,
		"Contract", "Name",
		map[string]string{"Contract.Name": LegacySourceCompound},
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != 87 {
		t.Errorf("legacyCurrent: got %d, want 87", got)
	}
}

// TestReadLegacyCurrent_NoneSourceIsZero verifies the "none" + missing-
// key case returns 0 without querying anything.
func TestReadLegacyCurrent_NoneSourceIsZero(t *testing.T) {
	db, _, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	for _, source := range []string{"", LegacySourceNone} {
		got, err := readLegacyCurrent(
			context.Background(), db,
			"Foo", "Bar",
			map[string]string{"Foo.Bar": source},
		)
		if err != nil {
			t.Errorf("source=%q: unexpected error: %v", source, err)
		}
		if got != 0 {
			t.Errorf("source=%q: got %d, want 0", source, got)
		}
	}
}

// TestReadLegacyCurrent_MissingLegacyTableIsZero — on a fresh DB, the
// legacy table is absent; seed must degrade to 0 (falls back to
// live_max ∨ start).
func TestReadLegacyCurrent_MissingLegacyTableIsZero(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.TABLES").
		WithArgs("_System_AutoNumber").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(0))

	got, err := readLegacyCurrent(
		context.Background(), db,
		"Account", "Account_ID__c",
		map[string]string{"Account.Account_ID__c": LegacySourceAutoNumber},
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != 0 {
		t.Errorf("legacyCurrent: got %d, want 0", got)
	}
}

// TestAssertRecordTypeCodeSeeded_FailsWhenEmpty verifies the
// P1-R3-RT-ordering precondition: if any seed format uses
// {RecordType.*} and _System_RecordType has no row with
// RecordTypeCode IS NOT NULL AND != '', the seed aborts with a
// structured error.
func TestAssertRecordTypeCodeSeeded_FailsWhenEmpty(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.TABLES").
		WithArgs("_System_RecordType").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
	mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM `_System_RecordType`").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(0))

	seeds := []AutoNumberFormatSeed{
		{
			ObjectName:   "Contract",
			FieldApiName: "Name",
			Format:       "{Account.BillingCountryCode}-{RecordType.RecordTypeCode}-{YY}{MM}{DD}-{00}",
			Scope:        autonumber.ScopeGlobal,
			Start:        1,
			LegacySource: legacyTableCompound,
		},
	}
	err = assertRecordTypeCodeSeeded(context.Background(), db, seeds)
	if err == nil {
		t.Fatal("expected RT-ordering error, got nil")
	}
	if !strings.Contains(err.Error(), "RecordTypeCode not seeded") {
		t.Errorf("error message missing marker string: %s", err)
	}
}

// TestAssertRecordTypeCodeSeeded_PassesWhenNoRecordTypeTokens — if the
// seed batch doesn't reference {RecordType.*}, the precondition is a
// no-op even if RecordTypeCode is all NULL.
func TestAssertRecordTypeCodeSeeded_PassesWhenNoRecordTypeTokens(t *testing.T) {
	db, _, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	seeds := []AutoNumberFormatSeed{
		{ObjectName: "Case", FieldApiName: "CaseNumber", Format: "{00000000}", Scope: autonumber.ScopeGlobal, Start: 1},
	}
	if err := assertRecordTypeCodeSeeded(context.Background(), db, seeds); err != nil {
		t.Errorf("unexpected error: %v", err)
	}
}

// TestSeedAutoNumberCounters_NilDB — minimal guard.
func TestSeedAutoNumberCounters_NilDB(t *testing.T) {
	err := SeedAutoNumberCounters(context.Background(), nil, nil, nil)
	if err == nil {
		t.Fatal("expected error on nil DB")
	}
}

// TestBuildScopeSourceMapFromSeeds — pins the generator → runner
// translator. Catches typos (e.g. a new LegacySource value being added
// to the generator without a mapping here).
func TestBuildScopeSourceMapFromSeeds(t *testing.T) {
	seeds := []AutoNumberFormatSeed{
		{ObjectName: "Account", FieldApiName: "Account_ID__c", LegacySource: legacyTableAutoNumber},
		{ObjectName: "Contract", FieldApiName: "Name", LegacySource: legacyTableCompound},
		{ObjectName: "Future", FieldApiName: "Name", LegacySource: ""},
		{ObjectName: "Future2", FieldApiName: "Name", LegacySource: LegacySourceNone},
	}
	got := BuildScopeSourceMapFromSeeds(seeds)
	want := map[string]string{
		"Account.Account_ID__c": LegacySourceAutoNumber,
		"Contract.Name":         LegacySourceCompound,
		"Future.Name":           LegacySourceNone,
		"Future2.Name":          LegacySourceNone,
	}
	for k, v := range want {
		if got[k] != v {
			t.Errorf("scopeSourceMap[%q]: got %q, want %q", k, got[k], v)
		}
	}
}

// Compile-time guard: ensure our LegacySource* constants match the
// literal values used by the generator (keeps the two surfaces in
// sync without relying on a runtime test).
var (
	_ = LegacySourceAutoNumber
	_ = LegacySourceCompound
	_ = LegacySourceNone
	_ = legacyTableAutoNumber
	_ = legacyTableCompound
)

// TestScanLiveMaxCounter_ColumnProbeError — pass-through of DB errors
// on the initial INFORMATION_SCHEMA probe.
func TestScanLiveMaxCounter_ColumnProbeError(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("INFORMATION_SCHEMA.COLUMNS").
		WithArgs("Account", "Account_ID__c").
		WillReturnError(errors.New("mock: probe failed"))

	derived := regexp.MustCompile(`^(\d{6})$`)
	_, err = scanLiveMaxCounter(context.Background(), db, "Account", "Account_ID__c", derived, "fld_1")
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if !strings.Contains(err.Error(), "probe column") {
		t.Errorf("error missing probe-column marker: %v", err)
	}
}
