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

import (
	"context"
	"errors"
	"testing"

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

// TestQuarantineRegistry_Basic covers the in-memory contract — fixpack
// F9: non-crashing, concurrent-safe marking of parse-failed fields.
func TestQuarantineRegistry_Basic(t *testing.T) {
	t.Parallel()

	q := NewQuarantineRegistry()
	ok, _ := q.IsQuarantined("fld_1")
	assert.False(t, ok, "fresh registry should not contain any fields")

	q.Quarantine("fld_1", "parse error: unexpected EOF")
	ok, reason := q.IsQuarantined("fld_1")
	assert.True(t, ok)
	assert.Contains(t, reason, "unexpected EOF")
	assert.Equal(t, 1, q.Len())

	// Clearing removes the record.
	q.Clear("fld_1")
	ok, _ = q.IsQuarantined("fld_1")
	assert.False(t, ok)
	assert.Equal(t, 0, q.Len())
}

// TestQuarantineRegistry_NilSafe covers the (*QuarantineRegistry)(nil)
// contract — methods on a nil receiver must not panic. Engines that
// opt out of quarantine entirely (tests) pass nil.
func TestQuarantineRegistry_NilSafe(t *testing.T) {
	t.Parallel()
	var q *QuarantineRegistry
	// All methods must be safe on nil.
	q.Quarantine("x", "y")
	ok, _ := q.IsQuarantined("x")
	assert.False(t, ok)
	q.Clear("x")
	assert.Equal(t, 0, q.Len())
	assert.Nil(t, q.List())
}

// TestBuildQuarantineErrorFromRegistry — the helper used by AssignOne
// must correctly surface ErrRuleQuarantined with both FieldID and
// Reason populated when the registry has an entry.
func TestBuildQuarantineErrorFromRegistry(t *testing.T) {
	t.Parallel()

	q := NewQuarantineRegistry()
	q.Quarantine("fld_contract", "parse failed: unknown token")

	err, ok := buildQuarantineErrorFromRegistry(q, "fld_contract")
	require.True(t, ok)
	require.Error(t, err)
	assert.True(t, errors.Is(err, ErrRuleQuarantined), "must satisfy errors.Is(ErrRuleQuarantined)")

	id, reason, detailsOK := QuarantineDetails(err)
	assert.True(t, detailsOK)
	assert.Equal(t, "fld_contract", id)
	assert.Contains(t, reason, "unknown token")

	// Miss → nil + false.
	_, miss := buildQuarantineErrorFromRegistry(q, "fld_other")
	assert.False(t, miss)
}

// TestEngineInit_NonCrashOnParseFail — fixpack F9 core property.
// Engine init must NOT panic / NOT abort when a rule has an invalid
// format; it must quarantine the field and continue serving the rest.
func TestEngineInit_NonCrashOnParseFail(t *testing.T) {
	t.Parallel()

	provider := &fakeMetadataProvider{
		rules: []Rule{
			{FieldID: "fld_good", Format: "GOOD-{0000}", Scope: ScopeGlobal, Start: 1},
			// Format with two counter tokens — rule 1 violation.
			{FieldID: "fld_bad_parse", Format: "{0000}-{0000}", Scope: ScopeGlobal, Start: 1},
			// Empty format — defensive: shouldn't be registered at all
			// in practice but engine init must not crash even if fed.
			{FieldID: "fld_empty", Format: "", Scope: ScopeGlobal, Start: 1},
		},
	}

	// Pass a dummy DB — engine init tryForceSessionPessimistic will
	// silently skip on nil, but NewEngine requires non-nil DB.
	// For this unit test we use a no-op *sql.DB substitute: open sql
	// driver that doesn't actually execute anything — skip if not
	// available by using the "nullconn" pattern.
	//
	// Easier: rely on the driver short-circuit. The init path only
	// needs the DB for tryForceSessionPessimistic which is
	// failure-tolerant. Pass a never-opened *sql.DB via OpenDB with
	// a fake connector.
	db := openFakeDB(t)

	e, err := NewEngine(
		WithReservationDB(db),
		WithMetadataProvider(provider),
	)
	require.NoError(t, err, "engine init must not error on parse-fail rules")
	require.NotNil(t, e)

	// Bad-format field must be quarantined.
	q := e.Quarantine()
	ok, reason := q.IsQuarantined("fld_bad_parse")
	assert.True(t, ok, "fld_bad_parse must be quarantined after init")
	assert.Contains(t, reason, "parse")

	ok, _ = q.IsQuarantined("fld_empty")
	assert.True(t, ok, "fld_empty must be quarantined after init (empty format fails parse)")

	// Good field must NOT be quarantined.
	ok, _ = q.IsQuarantined("fld_good")
	assert.False(t, ok, "fld_good must not be quarantined")
}

// TestAssignOne_ReturnsQuarantineError — subsequent calls to a
// quarantined field return ErrRuleQuarantined with structured detail.
func TestAssignOne_ReturnsQuarantineError(t *testing.T) {
	t.Parallel()

	provider := &fakeMetadataProvider{
		rules: []Rule{
			{FieldID: "fld_bad", Format: "{0000}-{0000}", Scope: ScopeGlobal, Start: 1},
		},
	}
	db := openFakeDB(t)
	e, err := NewEngine(
		WithReservationDB(db),
		WithMetadataProvider(provider),
	)
	require.NoError(t, err)

	_, err = e.AssignOne(context.Background(), "fld_bad", nil)
	require.Error(t, err)
	assert.True(t, errors.Is(err, ErrRuleQuarantined),
		"AssignOne on quarantined field must return ErrRuleQuarantined")

	id, _, ok := QuarantineDetails(err)
	assert.True(t, ok)
	assert.Equal(t, "fld_bad", id)
}

// TestReconcileQuarantine_LiveFix pins the Codex round-8 V2 HIGH fix:
// an admin fixing a bad rule via cmd/setup apply + refresh-metadata
// must be able to clear the quarantine without restarting the pod.
// The previous behavior left startup quarantine sticky until restart.
func TestReconcileQuarantine_LiveFix(t *testing.T) {
	t.Parallel()

	// Boot engine with a bad rule → field gets quarantined at init.
	provider := &fakeMetadataProvider{
		rules: []Rule{
			{FieldID: "fld_fixable", Format: "{0000}-{0000}", Scope: ScopeGlobal, Start: 1},
			{FieldID: "fld_good", Format: "GOOD-{0000}", Scope: ScopeGlobal, Start: 1},
		},
	}
	db := openFakeDB(t)
	e, err := NewEngine(
		WithReservationDB(db),
		WithMetadataProvider(provider),
	)
	require.NoError(t, err)

	ok, _ := e.Quarantine().IsQuarantined("fld_fixable")
	require.True(t, ok, "pre-reconcile: fld_fixable must be quarantined after init")
	ok, _ = e.Quarantine().IsQuarantined("fld_good")
	require.False(t, ok, "pre-reconcile: fld_good must NOT be quarantined")

	// Admin fixes the format in metadata (simulates cmd/setup apply +
	// Refresh). Then the admin endpoint triggers ReconcileQuarantine.
	provider.rules[0].Format = "FIXED-{0000}"
	e.ReconcileQuarantine(context.Background())

	ok, _ = e.Quarantine().IsQuarantined("fld_fixable")
	require.False(t, ok,
		"post-reconcile: fld_fixable quarantine must be cleared after live fix")
	ok, _ = e.Quarantine().IsQuarantined("fld_good")
	require.False(t, ok, "post-reconcile: fld_good must still not be quarantined")
}

// TestReconcileQuarantine_NewlyBadRule pins the other side of the fix:
// a newly-malformed rule loaded via Refresh must be quarantined at
// reconcile time rather than crashing at first insert.
func TestReconcileQuarantine_NewlyBadRule(t *testing.T) {
	t.Parallel()

	provider := &fakeMetadataProvider{
		rules: []Rule{
			{FieldID: "fld_good", Format: "GOOD-{0000}", Scope: ScopeGlobal, Start: 1},
		},
	}
	db := openFakeDB(t)
	e, err := NewEngine(
		WithReservationDB(db),
		WithMetadataProvider(provider),
	)
	require.NoError(t, err)

	ok, _ := e.Quarantine().IsQuarantined("fld_good")
	require.False(t, ok, "init: fld_good must not be quarantined")

	// Admin edits the rule through a path that skips engine's init
	// sweep (metadata refresh only). Reconcile must catch the new
	// malformation.
	provider.rules[0].Format = "{0000}-{0000}"
	e.ReconcileQuarantine(context.Background())

	ok, _ = e.Quarantine().IsQuarantined("fld_good")
	require.True(t, ok,
		"post-reconcile: newly-malformed rule must be quarantined")
}

// TestReconcileQuarantine_RemovedField clears quarantine for fields
// that are no longer in the metadata snapshot (field deletion /
// scope change that removes autonumber config).
func TestReconcileQuarantine_RemovedField(t *testing.T) {
	t.Parallel()

	provider := &fakeMetadataProvider{
		rules: []Rule{
			{FieldID: "fld_bad", Format: "{0000}-{0000}", Scope: ScopeGlobal, Start: 1},
		},
	}
	db := openFakeDB(t)
	e, err := NewEngine(
		WithReservationDB(db),
		WithMetadataProvider(provider),
	)
	require.NoError(t, err)
	ok, _ := e.Quarantine().IsQuarantined("fld_bad")
	require.True(t, ok, "init: fld_bad must be quarantined")

	// Admin removes the field from autonumber metadata entirely.
	provider.rules = nil
	e.ReconcileQuarantine(context.Background())

	ok, _ = e.Quarantine().IsQuarantined("fld_bad")
	require.False(t, ok,
		"post-reconcile: removed field must drop out of quarantine "+
			"(future re-create starts from clean state)")
}
