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

import (
	"context"
	"errors"
	"math/rand"
	"testing"
	"time"

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

// TestDeriveScopeKey ensures scope → scope_key mapping matches the
// schema contract (system_tables.json description: "" for global,
// 'YYYY-MM-DD' for per_day). PRD §4.4.
func TestDeriveScopeKey(t *testing.T) {
	t.Parallel()

	at := time.Date(2026, 4, 17, 9, 0, 0, 0, time.UTC)
	assert.Equal(t, "", deriveScopeKey(ScopeGlobal, at))
	assert.Equal(t, "2026-04-17", deriveScopeKey(ScopePerDay, at))
	// Unknown scope degrades to "" (global equivalence).
	assert.Equal(t, "", deriveScopeKey("unknown", at))

	// UTC normalization — non-UTC locations produce the same key.
	loc, _ := time.LoadLocation("America/Los_Angeles")
	atLA := at.In(loc) // 2026-04-17 02:00 PDT = same UTC moment
	assert.Equal(t, "2026-04-17", deriveScopeKey(ScopePerDay, atLA))

	// per_prefix is data-derived, not time-derived. deriveScopeKey
	// returns "" for it because the per_prefix key is computed by the
	// engine from the rendered format (see RenderScopePrefix) and
	// plumbed into reserveInput.scopeKeyOverride before the reserver
	// runs.
	assert.Equal(t, "", deriveScopeKey(ScopePerPrefix, at))
}

// TestScopeKeyFor documents the precedence rule in scopeKeyFor:
// scopeKeyFn wins when non-nil, else fall back to the time-derived
// key. This is the single entry point all reserver code paths use to
// compute scope_key — no call site should invoke deriveScopeKey
// directly on a reserveInput.
func TestScopeKeyFor(t *testing.T) {
	t.Parallel()

	at := time.Date(2026, 4, 18, 9, 0, 0, 0, time.UTC)

	// Nil closure, global scope → "".
	in := reserveInput{fieldID: "fld_1", scope: ScopeGlobal}
	got, err := scopeKeyFor(in, at)
	require.NoError(t, err)
	assert.Equal(t, "", got)

	// Nil closure, per_day scope → time-derived YYYY-MM-DD.
	in = reserveInput{fieldID: "fld_1", scope: ScopePerDay}
	got, err = scopeKeyFor(in, at)
	require.NoError(t, err)
	assert.Equal(t, "2026-04-18", got)

	// per_prefix scope with closure → closure output wins, using the
	// CALLER-supplied assignedAt (not time.Now()).
	in = reserveInput{
		fieldID: "fld_1",
		scope:   ScopePerPrefix,
		scopeKeyFn: func(t time.Time) (string, error) {
			return "JP-QT-" + t.UTC().Format("060102") + "-", nil
		},
	}
	got, err = scopeKeyFor(in, at)
	require.NoError(t, err)
	assert.Equal(t, "JP-QT-260418-", got)

	// per_prefix with closure fed a DIFFERENT assignedAt produces a
	// DIFFERENT scope key — pins the "closure uses caller's timestamp"
	// invariant that guarantees scope_key / Name date-component
	// consistency (Codex adversarial finding #1).
	atNextDay := time.Date(2026, 4, 19, 0, 0, 1, 0, time.UTC)
	got, err = scopeKeyFor(in, atNextDay)
	require.NoError(t, err)
	assert.Equal(t, "JP-QT-260419-", got,
		"scope_key must re-derive against the caller's assignedAt, not a captured one")

	// per_prefix with nil closure → "" (caller responsibility to set
	// closure; empty means no partitioning, logically equivalent to
	// global for a format with no dynamic prefix tokens).
	in = reserveInput{fieldID: "fld_1", scope: ScopePerPrefix}
	got, err = scopeKeyFor(in, at)
	require.NoError(t, err)
	assert.Equal(t, "", got)

	// Closure set even when scope is global — closure still wins.
	// Defensive: allows the engine to compute a partition key for any
	// scope without changing the scope enum.
	in = reserveInput{
		fieldID: "fld_1",
		scope:   ScopeGlobal,
		scopeKeyFn: func(time.Time) (string, error) {
			return "custom-partition", nil
		},
	}
	got, err = scopeKeyFor(in, at)
	require.NoError(t, err)
	assert.Equal(t, "custom-partition", got)

	// Closure error propagates unchanged (fail-closed).
	sentinel := errors.New("boom")
	in = reserveInput{
		fieldID: "fld_1",
		scope:   ScopePerPrefix,
		scopeKeyFn: func(time.Time) (string, error) {
			return "", sentinel
		},
	}
	_, err = scopeKeyFor(in, at)
	require.ErrorIs(t, err, sentinel)
}

// TestInitialNextFor documents the race-closure contract for per_prefix
// lazy-init (Codex round-3 CRITICAL follow-up, Option B): when
// liveMaxFn is set, the initial NextValue absorbs any existing
// live-table records in the bucket; when nil, falls back to
// in.startValue.
func TestInitialNextFor(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	at := time.Date(2026, 4, 18, 9, 0, 0, 0, time.UTC)

	// No liveMaxFn → startValue wins.
	in := reserveInput{fieldID: "fld_1", startValue: 1}
	got, err := initialNextFor(ctx, in, at)
	require.NoError(t, err)
	assert.Equal(t, int64(1), got)

	// startValue < 1 → clamped to 1.
	in = reserveInput{fieldID: "fld_1", startValue: 0}
	got, err = initialNextFor(ctx, in, at)
	require.NoError(t, err)
	assert.Equal(t, int64(1), got)

	// liveMaxFn returns live_max < startValue-1 → startValue wins.
	in = reserveInput{
		fieldID:    "fld_1",
		startValue: 100,
		liveMaxFn: func(_ context.Context, _ time.Time) (int64, error) {
			return 5, nil
		},
	}
	got, err = initialNextFor(ctx, in, at)
	require.NoError(t, err)
	assert.Equal(t, int64(100), got, "startValue must win when liveMax+1 ≤ startValue")

	// liveMaxFn returns live_max ≥ startValue → live_max+1 wins.
	// This is THE race-closure case: bucket has existing records
	// (e.g. legacy Apex "JP-QT-260418-07"), lazy-init absorbs them.
	//
	// Also pins that assignedAt is passed through (closures rendering
	// date-token prefixes rely on this for date-component consistency
	// with scope_key and the renderer).
	in = reserveInput{
		fieldID:    "fld_1",
		startValue: 1,
		liveMaxFn: func(_ context.Context, forwarded time.Time) (int64, error) {
			if !forwarded.Equal(at) {
				t.Errorf("assignedAt passed to liveMaxFn: got %v, want %v", forwarded, at)
			}
			return 7, nil
		},
	}
	got, err = initialNextFor(ctx, in, at)
	require.NoError(t, err)
	assert.Equal(t, int64(8), got,
		"live_max=7 + 1 = 8 must win over startValue=1 (race-closure)")

	// liveMaxFn error propagates fail-closed (duplicate prevention).
	sentinel := errors.New("DB drop mid-query")
	in = reserveInput{
		fieldID:    "fld_1",
		startValue: 1,
		liveMaxFn: func(_ context.Context, _ time.Time) (int64, error) {
			return 0, sentinel
		},
	}
	_, err = initialNextFor(ctx, in, at)
	require.ErrorIs(t, err, sentinel)

	// Defensive: liveMax == MaxInt64 → startValue (no overflow).
	in = reserveInput{
		fieldID:    "fld_1",
		startValue: 1,
		liveMaxFn: func(_ context.Context, _ time.Time) (int64, error) {
			return 1<<62 + (1<<62 - 1), nil // math.MaxInt64
		},
	}
	got, err = initialNextFor(ctx, in, at)
	require.NoError(t, err)
	assert.Equal(t, int64(1), got, "MaxInt64 liveMax falls back to startValue")
}

// TestWiderJitter covers PRD §12.1 R3: `rand(0, 2^(attempt+3)) ms`
// upper bound. Distribution isn't checked (that's statistical) — just
// the range.
func TestWiderJitter(t *testing.T) {
	t.Parallel()

	rng := rand.New(rand.NewSource(1))
	for attempt := 0; attempt < 6; attempt++ {
		shift := attempt + 3
		upper := time.Duration(1<<shift) * time.Millisecond
		for i := 0; i < 100; i++ {
			got := widerJitter(attempt, rng)
			require.GreaterOrEqual(t, got, time.Duration(0))
			require.Less(t, got, upper)
		}
	}
}

// TestGenerateCounterRowID — deterministic Id for a given
// (fieldID, scopeKey). Re-running the seed for the same key must
// collide on UNIQUE(FieldId,ScopeKey) → ON DUPLICATE KEY UPDATE hits.
func TestGenerateCounterRowID(t *testing.T) {
	t.Parallel()

	a := generateCounterRowID("fld_1", "")
	b := generateCounterRowID("fld_1", "")
	c := generateCounterRowID("fld_1", "2026-04-17")
	d := generateCounterRowID("fld_2", "")
	assert.Equal(t, a, b)
	assert.NotEqual(t, a, c)
	assert.NotEqual(t, a, d)
	assert.Contains(t, c, "fld_1")
	assert.Contains(t, c, "2026-04-17")
}

// TestCrossValueFingerprint — AssignBatch bucketing correctness sanity
// check. Records with identical cross-record values share the same
// fingerprint; differing values produce different fingerprints.
func TestCrossValueFingerprint(t *testing.T) {
	t.Parallel()

	refs := []CrossRecordRef{{
		TokenLiteral: "{Account.BillingCountryCode}",
		TargetObject: "Account",
		TargetField:  "BillingCountryCode",
	}}
	a := crossValueFingerprint(map[string]any{"{Account.BillingCountryCode}": "SG"}, refs)
	b := crossValueFingerprint(map[string]any{"{Account.BillingCountryCode}": "SG"}, refs)
	c := crossValueFingerprint(map[string]any{"{Account.BillingCountryCode}": "US"}, refs)
	assert.Equal(t, a, b)
	assert.NotEqual(t, a, c)

	// No refs → stable zero-value fingerprint.
	assert.Equal(t, "", crossValueFingerprint(nil, nil))
}
