package autonumber

import (
	"context"
	"errors"
	"regexp"
	"testing"
	"time"

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

// TestRefreshWithBackoff_RetryThenFail verifies the exponential-backoff
// retry loop surfaces the terminal error after maxAttempts. Round-5
// Codex V2-C2: the caller (service_manager_init) depends on this
// returning a non-nil error so the pod init aborts rather than serving
// traffic with an empty snapshot.
func TestRefreshWithBackoff_RetryThenFail(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	boom := errors.New("simulated DB failure")
	// Match the query via a permissive regex — SELECT ... FROM _System_Field ...
	rx := regexp.MustCompile(`SELECT f\.Id, o\.ApiName, f\.ApiName`)
	for i := 0; i < 3; i++ {
		mock.ExpectQuery(rx.String()).WillReturnError(boom)
	}

	src := NewMetadataSource(db, nil)
	start := time.Now()
	// 3 attempts, 1 ms base → retries after 1 ms + 2 ms = 3 ms total
	// extra wait; plenty small for the test.
	err = src.RefreshWithBackoff(context.Background(), 3, 1*time.Millisecond)
	if err == nil {
		t.Fatal("expected non-nil error after maxAttempts exhausted")
	}
	if !errors.Is(err, boom) {
		t.Errorf("want wrapped boom; got: %v", err)
	}
	if time.Since(start) > 500*time.Millisecond {
		t.Errorf("total wait looks too long: %v (expected ~3ms)", time.Since(start))
	}
	if e := mock.ExpectationsWereMet(); e != nil {
		t.Errorf("unmet expectations: %v", e)
	}
}

// TestRefreshWithBackoff_RetryThenSucceed — the i'th attempt succeeds
// (i<max). Earlier attempts fail; we expect the final Refresh() call
// to populate the snapshot and the function to return nil.
func TestRefreshWithBackoff_RetryThenSucceed(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	boom := errors.New("transient DB failure")
	rx := `SELECT f\.Id, o\.ApiName, f\.ApiName`
	mock.ExpectQuery(rx).WillReturnError(boom) // attempt 1 fails
	mock.ExpectQuery(rx).WillReturnRows(       // attempt 2 succeeds (empty)
		sqlmock.NewRows([]string{
			"Id", "ApiName", "ApiName2", "AutoNumberFormat",
			"Scope", "Start", "TokenRefs",
		}),
	)

	src := NewMetadataSource(db, nil)
	if err := src.RefreshWithBackoff(context.Background(), 3, 1*time.Millisecond); err != nil {
		t.Fatalf("expected nil error on eventual success; got: %v", err)
	}
	if e := mock.ExpectationsWereMet(); e != nil {
		t.Errorf("unmet expectations: %v", e)
	}
}

// TestMetadataSource_LookupByObject_CaseInsensitive — Round-6 V1-Codex-P1.
// Callers may pass an object API name that differs in case from the value
// stored in _System_Object.ApiName (for example "account" vs "Account").
// Previously LookupByObject was case-sensitive and silently returned nil,
// which caused the persistence layer to skip autonumber assignment without
// a visible error. The fix normalizes keys at Refresh time and normalizes
// the lookup here; this test exercises multiple casings.
func TestMetadataSource_LookupByObject_CaseInsensitive(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	rows := sqlmock.NewRows([]string{
		"Id", "ApiName", "ApiName2", "AutoNumberFormat",
		"Scope", "Start", "TokenRefs",
	}).AddRow(
		"fld-1", "Account", "AccountNumber", "A-{NNNN}", "global", int64(1), "",
	)
	mock.ExpectQuery(`SELECT f\.Id`).WillReturnRows(rows)

	src := NewMetadataSource(db, nil)
	if err := src.Refresh(context.Background()); err != nil {
		t.Fatalf("Refresh: %v", err)
	}

	cases := []string{"Account", "account", "ACCOUNT", "aCcOuNt"}
	for _, in := range cases {
		got := src.LookupByObject(in)
		if len(got) != 1 {
			t.Errorf("LookupByObject(%q): want 1 FieldMeta, got %d", in, len(got))
			continue
		}
		if got[0].FieldID != "fld-1" || got[0].FieldAPIName != "AccountNumber" {
			t.Errorf("LookupByObject(%q): unexpected meta %+v", in, got[0])
		}
	}

	// Miss on a genuinely unknown object must still return nil.
	if got := src.LookupByObject("NonExistent"); got != nil {
		t.Errorf("LookupByObject unknown: want nil, got %+v", got)
	}

	if e := mock.ExpectationsWereMet(); e != nil {
		t.Errorf("unmet expectations: %v", e)
	}
}

// TestRefreshWithBackoff_ContextCancelled — honors ctx.Done between
// retries and returns a wrapped ctx error, not "exhausted".
func TestRefreshWithBackoff_ContextCancelled(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	boom := errors.New("boom")
	mock.ExpectQuery(`SELECT f\.Id`).WillReturnError(boom)
	// Second query should never run — cancel fires during the sleep.

	ctx, cancel := context.WithCancel(context.Background())
	src := NewMetadataSource(db, nil)

	// Schedule cancel just after the first failure is recorded.
	go func() {
		time.Sleep(5 * time.Millisecond)
		cancel()
	}()

	// baseDelay 50 ms → first sleep covers the cancel window.
	err = src.RefreshWithBackoff(ctx, 3, 50*time.Millisecond)
	if err == nil {
		t.Fatal("expected error on cancelled context")
	}
	if !errors.Is(err, context.Canceled) {
		t.Errorf("want wrapped context.Canceled; got: %v", err)
	}
	_ = mock.ExpectationsWereMet()
}
