package auth

import (
	"context"
	"testing"
	"time"
)

// newService wires a Service over a fake store + miniredis for unit tests.
// It returns the service, the redis client (to read codes directly), the fake
// store, and the miniredis handle (to fast-forward TTLs).
func newService(t *testing.T, cfg ServiceConfig) (*Service, *fakeStore, *captureMailer) {
	t.Helper()
	rdb, _ := newMiniRedis(t)
	rl := NewRateLimiter(rdb, nil)
	tm := newTokenManager(t, rdb, time.Now)
	store := newFakeStore()
	mailer := newCaptureMailer()
	svc := NewService(store, rdb, rl, tm, mailer, cfg, nil)
	return svc, store, mailer
}

// codeInRedis reads the active verification code straight from Redis.
func codeInRedis(t *testing.T, svc *Service, email string) string {
	t.Helper()
	c, err := svc.rdb.Get(context.Background(), codeKey(email)).Result()
	if err != nil {
		t.Fatalf("no code stored for %s: %v", email, err)
	}
	return c
}

func TestService_RegisterFullFlow(t *testing.T) {
	svc, store, _ := newService(t, ServiceConfig{})
	ctx := context.Background()
	const email = "alice@example.com"

	if _, err := svc.SendCode(ctx, email, "1.1.1.1"); err != nil {
		t.Fatalf("SendCode: %v", err)
	}
	code := codeInRedis(t, svc, email)

	pair, apiErr := svc.Register(ctx, email, code, "supersecret")
	if apiErr != nil {
		t.Fatalf("Register: %v", apiErr)
	}
	if pair.AccessToken == "" || pair.RefreshToken == "" || pair.ExpiresIn != 900 {
		t.Fatalf("bad token pair: %+v", pair)
	}

	// User exists.
	u, err := store.GetUserByEmail(ctx, email)
	if err != nil {
		t.Fatalf("user not created: %v", err)
	}
	// Trial subscription exists, PRO, ~7 days.
	tr, ok := store.trials[u.ID]
	if !ok {
		t.Fatal("trial subscription not created")
	}
	if tr.plan != "pro" || tr.source != "trial" {
		t.Fatalf("trial = %+v, want pro/trial", tr)
	}
	days := time.Until(tr.expiresAt).Hours() / 24
	if days < 6.9 || days > 7.1 {
		t.Fatalf("trial length = %.2f days, want ~7", days)
	}

	// Issued access token authenticates.
	claims, perr := svc.tokens.ParseAccess(pair.AccessToken)
	if perr != nil {
		t.Fatalf("ParseAccess: %v", perr)
	}
	if claims.UID != u.ID || claims.Subject != u.UUID {
		t.Fatalf("claims mismatch: %+v", claims)
	}
}

func TestService_DuplicateEmailConflict(t *testing.T) {
	// Higher send limit so the two registration attempts can each request a code.
	svc, _, _ := newService(t, ServiceConfig{EmailPerMinute: 10})
	ctx := context.Background()
	const email = "dup@example.com"

	// First registration.
	_, _ = svc.SendCode(ctx, email, "")
	if _, e := svc.Register(ctx, email, codeInRedis(t, svc, email), "password1"); e != nil {
		t.Fatalf("first register: %v", e)
	}

	// Anti-enumeration: SendCode for a now-registered email must NOT store a
	// verification code (it mails a login reminder instead), so the API response
	// can't be used to discover that the email is registered.
	if _, e := svc.SendCode(ctx, email, ""); e != nil {
		t.Fatalf("second SendCode: %v", e)
	}
	if c := svc.rdb.Get(ctx, codeKey(email)).Val(); c != "" {
		t.Fatalf("registered email must not receive a code, got %q", c)
	}

	// Even if a code is forced (e.g. a race), Register returns the generic code
	// error rather than "email exists" — no enumeration leak.
	if err := svc.rdb.Set(ctx, codeKey(email), "654321", 10*time.Minute).Err(); err != nil {
		t.Fatalf("force code: %v", err)
	}
	_, apiErr := svc.Register(ctx, email, "654321", "password2")
	if apiErr == nil || apiErr.Code != ErrCodeInvalid.Code {
		t.Fatalf("want code_invalid (anti-enumeration), got %v", apiErr)
	}
}

func TestService_CodeWrong(t *testing.T) {
	svc, _, _ := newService(t, ServiceConfig{})
	ctx := context.Background()
	const email = "wrong@example.com"
	_, _ = svc.SendCode(ctx, email, "")

	_, apiErr := svc.Register(ctx, email, "000000", "password1")
	if apiErr == nil || apiErr.Code != ErrCodeInvalid.Code {
		t.Fatalf("want code_invalid, got %v", apiErr)
	}
}

func TestService_CodeExpired(t *testing.T) {
	svc, _, _ := newService(t, ServiceConfig{CodeTTL: time.Minute})
	ctx := context.Background()
	const email = "expired@example.com"
	_, _ = svc.SendCode(ctx, email, "")
	code := codeInRedis(t, svc, email)

	// Expire the code key.
	svc.rdb.Del(ctx, codeKey(email))

	_, apiErr := svc.Register(ctx, email, code, "password1")
	if apiErr == nil || apiErr.Code != ErrCodeInvalid.Code {
		t.Fatalf("want code_invalid after expiry, got %v", apiErr)
	}
}

func TestService_CodeReuseRejected(t *testing.T) {
	svc, _, _ := newService(t, ServiceConfig{})
	ctx := context.Background()
	const email = "reuse@example.com"
	_, _ = svc.SendCode(ctx, email, "")
	code := codeInRedis(t, svc, email)

	if _, e := svc.Register(ctx, email, code, "password1"); e != nil {
		t.Fatalf("first register: %v", e)
	}
	// Re-using the consumed code must fail.
	_, apiErr := svc.Register(ctx, "other@example.com", code, "password1")
	if apiErr == nil || apiErr.Code != ErrCodeInvalid.Code {
		t.Fatalf("want code_invalid on reuse, got %v", apiErr)
	}
}

func TestService_CodeBruteForceBurned(t *testing.T) {
	svc, _, _ := newService(t, ServiceConfig{CodeMaxAttempts: 3})
	ctx := context.Background()
	const email = "brute@example.com"
	_, _ = svc.SendCode(ctx, email, "")
	good := codeInRedis(t, svc, email)

	// 3 wrong attempts burn the code.
	for i := 0; i < 3; i++ {
		if _, e := svc.Register(ctx, email, "999999", "password1"); e == nil {
			t.Fatal("wrong code should fail")
		}
	}
	// Even the correct code no longer works.
	if _, e := svc.Register(ctx, email, good, "password1"); e == nil || e.Code != ErrCodeInvalid.Code {
		t.Fatalf("burned code should reject correct value, got %v", e)
	}
}

func TestService_SendCodeRateLimited(t *testing.T) {
	svc, _, _ := newService(t, ServiceConfig{EmailPerMinute: 1})
	ctx := context.Background()
	const email = "rl@example.com"

	if _, e := svc.SendCode(ctx, email, "9.9.9.9"); e != nil {
		t.Fatalf("first send: %v", e)
	}
	ra, apiErr := svc.SendCode(ctx, email, "9.9.9.9")
	if apiErr == nil || apiErr.Code != ErrRateLimited.Code {
		t.Fatalf("want rate_limited, got %v", apiErr)
	}
	if ra <= 0 {
		t.Fatalf("expected positive retry-after, got %v", ra)
	}
}

func TestService_SendCodeDisposableBlocked(t *testing.T) {
	svc, _, _ := newService(t, ServiceConfig{})
	_, apiErr := svc.SendCode(context.Background(), "x@mailinator.com", "")
	if apiErr == nil || apiErr.Code != ErrEmailDisposable.Code {
		t.Fatalf("want email_disposable, got %v", apiErr)
	}
}

func TestService_LoginAndLockout(t *testing.T) {
	svc, _, _ := newService(t, ServiceConfig{LoginFailMax: 3, LoginLockWindow: 15 * time.Minute})
	ctx := context.Background()
	const email = "login@example.com"
	const pw = "rightpassword"

	_, _ = svc.SendCode(ctx, email, "")
	if _, e := svc.Register(ctx, email, codeInRedis(t, svc, email), pw); e != nil {
		t.Fatalf("register: %v", e)
	}

	// Correct login works.
	pair, _, apiErr := svc.Login(ctx, email, pw, "")
	if apiErr != nil || pair == nil {
		t.Fatalf("login should succeed: %v", apiErr)
	}

	// 3 wrong attempts.
	for i := 0; i < 3; i++ {
		_, _, e := svc.Login(ctx, email, "wrong", "")
		if e == nil || e.Code != ErrInvalidCredentials.Code {
			t.Fatalf("attempt %d want invalid_credentials, got %v", i, e)
		}
	}
	// Now locked, even with the correct password.
	_, ra, e := svc.Login(ctx, email, pw, "")
	if e == nil || e.Code != ErrAccountLocked.Code {
		t.Fatalf("want account_locked, got %v", e)
	}
	if ra <= 0 {
		t.Fatalf("expected positive retry-after on lock, got %v", ra)
	}
}

func TestService_LoginUnknownUser(t *testing.T) {
	svc, _, _ := newService(t, ServiceConfig{})
	_, _, apiErr := svc.Login(context.Background(), "ghost@example.com", "whatever", "")
	if apiErr == nil || apiErr.Code != ErrInvalidCredentials.Code {
		t.Fatalf("want invalid_credentials for unknown user, got %v", apiErr)
	}
}

func TestService_BannedUserRejected(t *testing.T) {
	svc, store, _ := newService(t, ServiceConfig{})
	ctx := context.Background()
	const email = "banned@example.com"
	const pw = "password1"

	_, _ = svc.SendCode(ctx, email, "")
	if _, e := svc.Register(ctx, email, codeInRedis(t, svc, email), pw); e != nil {
		t.Fatalf("register: %v", e)
	}
	store.setStatus(email, "banned")

	_, _, apiErr := svc.Login(ctx, email, pw, "")
	if apiErr == nil || apiErr.Code != ErrAccountBanned.Code {
		t.Fatalf("want account_banned, got %v", apiErr)
	}
}

func TestService_RefreshRotation(t *testing.T) {
	svc, _, _ := newService(t, ServiceConfig{})
	ctx := context.Background()
	const email = "refresh@example.com"

	_, _ = svc.SendCode(ctx, email, "")
	pair, e := svc.Register(ctx, email, codeInRedis(t, svc, email), "password1")
	if e != nil {
		t.Fatalf("register: %v", e)
	}

	rotated, apiErr := svc.Refresh(ctx, pair.RefreshToken)
	if apiErr != nil {
		t.Fatalf("refresh: %v", apiErr)
	}
	// Old refresh token now invalid.
	if _, e := svc.Refresh(ctx, pair.RefreshToken); e == nil || e.Code != ErrInvalidToken.Code {
		t.Fatalf("want invalid_token for rotated-out refresh, got %v", e)
	}
	// New one works.
	if _, e := svc.Refresh(ctx, rotated.RefreshToken); e != nil {
		t.Fatalf("new refresh should work: %v", e)
	}
}
