package account

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

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
)

// fakeExpiryQuerier records the `since` bound of each call and replays a scripted
// result/error per call, so the sweeper's window and dispatch logic is testable
// without a database.
type fakeExpiryQuerier struct {
	results [][]uuid.UUID
	errs    []error
	sinces  []time.Time
	idx     int
}

func (f *fakeExpiryQuerier) SuspensionsExpiredBetween(_ context.Context, since, _ time.Time) ([]uuid.UUID, error) {
	f.sinces = append(f.sinces, since)
	i := f.idx
	f.idx++
	if i < len(f.errs) && f.errs[i] != nil {
		return nil, f.errs[i]
	}
	if i < len(f.results) {
		return f.results[i], nil
	}
	return nil, nil
}

func newSweeper(store suspensionExpiryQuerier, onExpire func(uuid.UUID)) *SuspensionSweeper {
	return &SuspensionSweeper{
		store:    store,
		onExpire: onExpire,
		log:      zap.NewNop(),
		since:    time.Now().Add(-time.Minute).UTC(),
	}
}

func TestSuspensionSweeperDispatchesAndAdvances(t *testing.T) {
	id1, id2 := uuid.New(), uuid.New()
	fake := &fakeExpiryQuerier{results: [][]uuid.UUID{{id1, id2}, nil}}
	var got []uuid.UUID
	w := newSweeper(fake, func(id uuid.UUID) { got = append(got, id) })

	first := w.since
	w.sweep(context.Background())
	assert.Equal(t, []uuid.UUID{id1, id2}, got, "every expired account is dispatched")
	assert.True(t, w.since.After(first), "the window advances on success")

	// A second sweep opens the next window at the previous upper bound.
	prev := w.since
	w.sweep(context.Background())
	require.Len(t, fake.sinces, 2)
	assert.True(t, fake.sinces[1].After(fake.sinces[0]), "consecutive windows are contiguous and forward")
	assert.True(t, fake.sinces[1].Equal(prev), "the next window starts at the previous upper bound")
}

func TestSuspensionSweeperKeepsWindowOnError(t *testing.T) {
	fake := &fakeExpiryQuerier{errs: []error{errors.New("db down")}}
	w := newSweeper(fake, func(uuid.UUID) { t.Fatal("onExpire must not run when the query fails") })

	before := w.since
	w.sweep(context.Background())
	assert.True(t, w.since.Equal(before), "the window is retained on error so the next tick retries it")
}

func TestNewSuspensionSweeperDefaults(t *testing.T) {
	w := NewSuspensionSweeper(nil, func(uuid.UUID) {}, nil)
	assert.Equal(t, time.Minute, w.Interval())
	assert.NotNil(t, w.log, "a nil logger is tolerated")
	assert.WithinDuration(t, time.Now().UTC(), w.since, time.Second, "the first window opens at construction time")
}
