package auth

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"sync"
	"testing"
	"time"

	"github.com/alicebob/miniredis/v2"
	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
)

// newMiniRedis spins up an in-memory Redis and returns a connected client.
func newMiniRedis(t *testing.T) (*redis.Client, *miniredis.Miniredis) {
	t.Helper()
	mr, err := miniredis.Run()
	if err != nil {
		t.Fatalf("miniredis: %v", err)
	}
	t.Cleanup(mr.Close)
	rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
	t.Cleanup(func() { _ = rdb.Close() })
	return rdb, mr
}

// newRSAKey generates a 2048-bit RSA key for signing test tokens.
func newRSAKey(t *testing.T) *rsa.PrivateKey {
	t.Helper()
	k, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("rsa key: %v", err)
	}
	return k
}

// newTokenManager builds a TokenManager with the given clock and a single kid.
func newTokenManager(t *testing.T, rdb *redis.Client, now func() time.Time) *TokenManager {
	t.Helper()
	key := newRSAKey(t)
	tm, err := NewTokenManager(rdb, TokenConfig{
		SignKey:    key,
		SignKID:    "k1",
		AccessTTL:  15 * time.Minute,
		RefreshTTL: 30 * 24 * time.Hour,
		Now:        now,
	})
	if err != nil {
		t.Fatalf("NewTokenManager: %v", err)
	}
	return tm
}

// --------------------------------------------------------------------------
// fakeStore — in-memory UserStore for unit tests.
// --------------------------------------------------------------------------

type trialRecord struct {
	plan      string
	expiresAt time.Time
	source    string
}

type fakeStore struct {
	mu      sync.Mutex
	byEmail map[string]*User
	trials  map[int64]trialRecord
	nextID  int64
}

func newFakeStore() *fakeStore {
	return &fakeStore{byEmail: map[string]*User{}, trials: map[int64]trialRecord{}}
}

func (f *fakeStore) CreateUserWithTrial(_ context.Context, email, pwHash string, trialDays int) (*User, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if _, ok := f.byEmail[email]; ok {
		return nil, ErrEmailTaken
	}
	f.nextID++
	u := &User{
		ID:     f.nextID,
		UUID:   uuid.NewString(),
		Email:  email,
		PwHash: pwHash,
		DpUUID: uuid.NewString(),
		Status: "active",
	}
	f.byEmail[email] = u
	f.trials[u.ID] = trialRecord{
		plan:      "pro",
		expiresAt: time.Now().UTC().AddDate(0, 0, trialDays),
		source:    "trial",
	}
	return u, nil
}

func (f *fakeStore) GetUserByEmail(_ context.Context, email string) (*User, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	u, ok := f.byEmail[email]
	if !ok {
		return nil, ErrNotFound
	}
	cp := *u
	return &cp, nil
}

// setStatus mutates a stored user's status (e.g. to "banned") for tests.
func (f *fakeStore) setStatus(email, status string) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if u, ok := f.byEmail[email]; ok {
		u.Status = status
	}
}

// captureMailer records the last code it was asked to send.
type captureMailer struct {
	mu   sync.Mutex
	last map[string]string
}

func newCaptureMailer() *captureMailer { return &captureMailer{last: map[string]string{}} }

func (m *captureMailer) SendCode(_ context.Context, to, code string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.last[to] = code
	return nil
}

func (m *captureMailer) SendAlreadyRegistered(_ context.Context, _ string) error { return nil }

func (m *captureMailer) codeFor(to string) string {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.last[to]
}
