package services

import (
	"context"
	"sync"
	"testing"
	"time"

	"github.com/nexuscrm/backend/internal/features/events"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type testOutboxRepo struct {
	mu           sync.Mutex
	cleanupCalls int
	cleanupCh    chan struct{}
}

func newTestOutboxRepo() *testOutboxRepo {
	return &testOutboxRepo{
		cleanupCh: make(chan struct{}, 8),
	}
}

func (r *testOutboxRepo) Enqueue(context.Context, string, interface{}) (string, error) {
	return "outbox-1", nil
}

func (r *testOutboxRepo) GetPendingEvents(context.Context, int) ([]models.OutboxEvent, error) {
	return []models.OutboxEvent{}, nil
}

func (r *testOutboxRepo) ClaimEvent(context.Context, string) (string, error) {
	return "", nil
}

func (r *testOutboxRepo) UpdateStatus(context.Context, string, string, string) error {
	return nil
}

func (r *testOutboxRepo) IncrementRetry(context.Context, string, int, string) error {
	return nil
}

func (r *testOutboxRepo) CleanupProcessed(context.Context, time.Time) (int64, error) {
	r.mu.Lock()
	r.cleanupCalls++
	r.mu.Unlock()

	select {
	case r.cleanupCh <- struct{}{}:
	default:
	}

	return 1, nil
}

func (r *testOutboxRepo) cleanupCount() int {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.cleanupCalls
}

type testTxManager struct{}

func (testTxManager) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
	return fn(ctx)
}

func (testTxManager) WithRetry(ctx context.Context, fn func(context.Context) error, _ int) error {
	return fn(ctx)
}

func (testTxManager) InjectTx(ctx context.Context, _ interface{}) context.Context {
	return ctx
}

func (testTxManager) ExtractTx(context.Context) interface{} {
	return nil
}

func (testTxManager) IsTransactionActive(context.Context) bool {
	return false
}

func (testTxManager) DetachTx(ctx context.Context) context.Context { return ctx }

func newCleanupTestServiceManager(interval time.Duration) (*ServiceManager, *testOutboxRepo) {
	repo := newTestOutboxRepo()
	sm := &ServiceManager{
		Outbox:                events.NewOutboxService(repo, events.NewEventBus(), testTxManager{}),
		outboxCleanupInterval: interval,
	}
	return sm, repo
}

func TestStartOutboxCleanupWorker_Idempotent(t *testing.T) {
	sm, _ := newCleanupTestServiceManager(time.Hour)

	sm.StartOutboxCleanupWorker()
	firstStopCh := sm.outboxCleanupStopCh
	require.NotNil(t, firstStopCh)

	sm.StartOutboxCleanupWorker()

	assert.True(t, sm.outboxCleanupRunning)
	assert.Equal(t, firstStopCh, sm.outboxCleanupStopCh)

	sm.StopOutboxCleanupWorker()

	assert.False(t, sm.outboxCleanupRunning)
	assert.Nil(t, sm.outboxCleanupStopCh)
}

func TestStopBackgroundWorkers_StopsOutboxCleanupWorker(t *testing.T) {
	sm, repo := newCleanupTestServiceManager(10 * time.Millisecond)

	sm.StartBackgroundWorkers()

	select {
	case <-repo.cleanupCh:
	case <-time.After(250 * time.Millisecond):
		t.Fatal("expected cleanup worker to run at least once")
	}

	sm.StopBackgroundWorkers()

	countAfterStop := repo.cleanupCount()
	time.Sleep(40 * time.Millisecond)

	assert.Equal(t, countAfterStop, repo.cleanupCount())
	assert.False(t, sm.outboxCleanupRunning)
	assert.Nil(t, sm.outboxCleanupStopCh)
}

// TestStartOutboxCleanupWorker_DisabledViaEnv verifies OUTBOX_RETENTION_DAYS=0
// disables the cleanup loop. Contract with deployment: set the env var to 0
// to preserve every outbox event indefinitely.
func TestStartOutboxCleanupWorker_DisabledViaEnv(t *testing.T) {
	t.Setenv("OUTBOX_RETENTION_DAYS", "0")

	sm, repo := newCleanupTestServiceManager(10 * time.Millisecond)
	sm.StartOutboxCleanupWorker()

	assert.False(t, sm.outboxCleanupRunning, "worker must not start when OUTBOX_RETENTION_DAYS=0")
	assert.Nil(t, sm.outboxCleanupStopCh)

	time.Sleep(40 * time.Millisecond)
	assert.Equal(t, 0, repo.cleanupCount(), "CleanupProcessed must not be invoked")
}

// TestStartOutboxCleanupWorker_EnvOverridesRetention verifies a positive
// OUTBOX_RETENTION_DAYS overrides the default 14-day retention.
func TestStartOutboxCleanupWorker_EnvOverridesRetention(t *testing.T) {
	t.Setenv("OUTBOX_RETENTION_DAYS", "30")

	sm, _ := newCleanupTestServiceManager(time.Hour)
	sm.StartOutboxCleanupWorker()
	defer sm.StopOutboxCleanupWorker()

	assert.True(t, sm.outboxCleanupRunning)
	assert.Equal(t, 30*24*time.Hour, sm.outboxCleanupRetention)
}

// TestStartOutboxCleanupWorker_InvalidEnvFallsBack verifies that garbage input
// does not crash the worker and does not toggle the disabled flag — it warns
// and proceeds with defaults.
func TestStartOutboxCleanupWorker_InvalidEnvFallsBack(t *testing.T) {
	t.Setenv("OUTBOX_RETENTION_DAYS", "not-a-number")

	sm, _ := newCleanupTestServiceManager(time.Hour)
	sm.StartOutboxCleanupWorker()
	defer sm.StopOutboxCleanupWorker()

	assert.True(t, sm.outboxCleanupRunning, "invalid env var must not disable worker")
	assert.False(t, sm.outboxCleanupDisabled)
}

// TestStartOutboxCleanupWorker_NegativeEnvFallsBack verifies negative values
// are rejected (falls back to default) rather than disabling the worker.
func TestStartOutboxCleanupWorker_NegativeEnvFallsBack(t *testing.T) {
	t.Setenv("OUTBOX_RETENTION_DAYS", "-5")

	sm, _ := newCleanupTestServiceManager(time.Hour)
	sm.StartOutboxCleanupWorker()
	defer sm.StopOutboxCleanupWorker()

	assert.True(t, sm.outboxCleanupRunning, "negative env var must not disable worker")
	assert.False(t, sm.outboxCleanupDisabled)
}

// TestStartOutboxCleanupWorker_WhitespaceEnvHandled verifies trailing newlines
// from shell env exports are handled transparently.
func TestStartOutboxCleanupWorker_WhitespaceEnvHandled(t *testing.T) {
	t.Setenv("OUTBOX_RETENTION_DAYS", "  7\n")

	sm, _ := newCleanupTestServiceManager(time.Hour)
	sm.StartOutboxCleanupWorker()
	defer sm.StopOutboxCleanupWorker()

	assert.True(t, sm.outboxCleanupRunning)
	assert.Equal(t, 7*24*time.Hour, sm.outboxCleanupRetention)
}

// TestStartOutboxCleanupWorker_OverflowRejected verifies a retention value
// large enough to overflow time.Duration (int64 ns) is rejected with a log
// rather than silently wrapping to a small-positive or negative duration.
// "Use 0 to disable" is the documented escape hatch for "keep forever".
func TestStartOutboxCleanupWorker_OverflowRejected(t *testing.T) {
	t.Setenv("OUTBOX_RETENTION_DAYS", "200000") // exceeds maxOutboxRetentionDays (36524)

	sm, _ := newCleanupTestServiceManager(time.Hour)
	sm.StartOutboxCleanupWorker()
	defer sm.StopOutboxCleanupWorker()

	assert.True(t, sm.outboxCleanupRunning, "overflow must fall back to default, not disable")
	assert.False(t, sm.outboxCleanupDisabled)
	assert.Equal(t, time.Duration(0), sm.outboxCleanupRetention,
		"retention field should remain zero so the worker's own default (14d) kicks in")
}

// TestStartOutboxCleanupWorker_DisabledFlagResetsOnRestart ensures Stop/Start
// cycles respect a changed env var. Without the reset, an initial
// OUTBOX_RETENTION_DAYS=0 run would leave outboxCleanupDisabled=true forever
// and silently block cleanup after any env change.
func TestStartOutboxCleanupWorker_DisabledFlagResetsOnRestart(t *testing.T) {
	sm, _ := newCleanupTestServiceManager(time.Hour)

	// Phase 1: env=0 → disabled.
	t.Setenv("OUTBOX_RETENTION_DAYS", "0")
	sm.StartOutboxCleanupWorker()
	require.True(t, sm.outboxCleanupDisabled)
	require.False(t, sm.outboxCleanupRunning)

	// Phase 2: operator fixes env to a positive value, restart.
	t.Setenv("OUTBOX_RETENTION_DAYS", "7")
	sm.StopOutboxCleanupWorker() // no-op (worker never started)
	sm.StartOutboxCleanupWorker()
	defer sm.StopOutboxCleanupWorker()

	assert.False(t, sm.outboxCleanupDisabled, "disabled flag must reset on restart")
	assert.True(t, sm.outboxCleanupRunning)
	assert.Equal(t, 7*24*time.Hour, sm.outboxCleanupRetention)
}
