package admin

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

	"github.com/wangjia/pangolin/server/internal/totp"
)

// newTestAdmin seeds an active admin and returns its TOTP secret.
func newTestAdmin(t *testing.T, store *fakeStore, key []byte, username, password string) string {
	t.Helper()
	hash, err := HashPassword(password)
	if err != nil {
		t.Fatal(err)
	}
	secret, err := totp.GenerateSecret()
	if err != nil {
		t.Fatal(err)
	}
	enc, err := EncryptSecret(key, secret)
	if err != nil {
		t.Fatal(err)
	}
	store.admins[username] = &Admin{ID: 1, Username: username, PwHash: hash, TOTPSecretEnc: enc, Status: "active"}
	return secret
}

func newTestAuth(t *testing.T) (*Authenticator, *fakeStore, []byte) {
	t.Helper()
	rdb, _ := newTestRedis(t)
	key := make([]byte, 32)
	if _, err := rand.Read(key); err != nil {
		t.Fatal(err)
	}
	store := newFakeStore()
	cfg := &Config{SecretKey: key, LoginFailMax: 3, LoginLockDuration: time.Minute, SessionTTL: 30 * time.Minute}
	sessions := NewSessionStore(rdb, cfg.SessionTTL)
	sec := NewSecurityLog(store, nil)
	return NewAuthenticator(store, sessions, rdb, cfg, sec), store, key
}

func TestLogin_Success(t *testing.T) {
	auth, store, key := newTestAuth(t)
	secret := newTestAdmin(t, store, key, "alice", "s3cret-pass")
	code, _ := totp.Code(secret, time.Now().UTC())

	sid, sess, err := auth.Login(context.Background(), "alice", "s3cret-pass", code, "127.0.0.1")
	if err != nil {
		t.Fatalf("login failed: %v", err)
	}
	if sid == "" || sess == nil {
		t.Fatal("no session returned")
	}
	if _, ok := store.lastLogin[1]; !ok {
		t.Error("last login not recorded")
	}
	if len(store.auditFor("admin_login_ok")) != 1 {
		t.Error("successful login not audited")
	}
}

func TestLogin_WrongPassword(t *testing.T) {
	auth, store, key := newTestAuth(t)
	secret := newTestAdmin(t, store, key, "alice", "s3cret-pass")
	code, _ := totp.Code(secret, time.Now().UTC())

	_, _, err := auth.Login(context.Background(), "alice", "WRONG", code, "127.0.0.1")
	if err != ErrInvalidCredentials {
		t.Fatalf("err = %v; want ErrInvalidCredentials", err)
	}
	if len(store.auditFor("admin_login_fail")) != 1 {
		t.Error("failed login not audited")
	}
}

func TestLogin_WrongTOTP(t *testing.T) {
	auth, store, key := newTestAuth(t)
	_ = newTestAdmin(t, store, key, "alice", "s3cret-pass")

	_, _, err := auth.Login(context.Background(), "alice", "s3cret-pass", "000000", "127.0.0.1")
	if err != ErrInvalidCredentials {
		t.Fatalf("err = %v; want ErrInvalidCredentials", err)
	}
}

func TestLogin_UnknownUser(t *testing.T) {
	auth, _, _ := newTestAuth(t)
	_, _, err := auth.Login(context.Background(), "ghost", "x", "000000", "127.0.0.1")
	if err != ErrInvalidCredentials {
		t.Fatalf("err = %v; want ErrInvalidCredentials", err)
	}
}

func TestLogin_LockoutAfterFailures(t *testing.T) {
	auth, store, key := newTestAuth(t)
	secret := newTestAdmin(t, store, key, "alice", "s3cret-pass")
	ctx := context.Background()

	// 3 failures hit the cap.
	for i := 0; i < 3; i++ {
		if _, _, err := auth.Login(ctx, "alice", "WRONG", "000000", "127.0.0.1"); err != ErrInvalidCredentials {
			t.Fatalf("attempt %d err = %v", i, err)
		}
	}
	// Now even a correct credential is locked out.
	code, _ := totp.Code(secret, time.Now().UTC())
	if _, _, err := auth.Login(ctx, "alice", "s3cret-pass", code, "127.0.0.1"); err != ErrLockedOut {
		t.Fatalf("err = %v; want ErrLockedOut", err)
	}
	if len(store.auditFor("admin_login_locked")) == 0 {
		t.Error("lockout not audited")
	}
}
