package totp_test

import (
	"encoding/base32"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"gateway/internal/model/member/totp"
)

// RFC 6238 Appendix B test vector (HMAC-SHA1, 20-byte ASCII "12345678901234567890").
// Time 59 → expected 8-digit value 94287082; for 6 digits the truncation is 287082.
func TestGenerate_RFC6238Vector(t *testing.T) {
	secret := []byte("12345678901234567890")
	code, err := totp.Generate(secret, time.Unix(59, 0), 30*time.Second, 6)
	require.NoError(t, err)
	require.Equal(t, "287082", code)

	code, err = totp.Generate(secret, time.Unix(1111111109, 0), 30*time.Second, 6)
	require.NoError(t, err)
	require.Equal(t, "081804", code)
}

func TestVerify_WindowAccepts(t *testing.T) {
	secret := []byte("12345678901234567890")
	ts := time.Unix(59, 0)
	code, err := totp.Generate(secret, ts, 30*time.Second, 6)
	require.NoError(t, err)

	step, ok := totp.Verify(secret, code, ts.Add(15*time.Second), 30*time.Second, 6, 1)
	require.True(t, ok)
	require.Equal(t, totp.TimeStep(ts, 30*time.Second), step)
}

func TestVerify_WindowRejectsBeyond(t *testing.T) {
	secret := []byte("12345678901234567890")
	ts := time.Unix(59, 0)
	code, err := totp.Generate(secret, ts, 30*time.Second, 6)
	require.NoError(t, err)

	_, ok := totp.Verify(secret, code, ts.Add(2*time.Minute), 30*time.Second, 6, 1)
	require.False(t, ok)
}

func TestVerify_InvalidInputs(t *testing.T) {
	_, ok := totp.Verify(nil, "123456", time.Now(), 30*time.Second, 6, 1)
	require.False(t, ok)
	_, ok = totp.Verify([]byte("k"), "", time.Now(), 30*time.Second, 6, 1)
	require.False(t, ok)
	_, ok = totp.Verify([]byte("k"), "12345", time.Now(), 30*time.Second, 6, 1)
	require.False(t, ok)
}

func TestGenerateSecret(t *testing.T) {
	s1, err := totp.GenerateSecret()
	require.NoError(t, err)
	require.Len(t, s1, totp.SecretBytes)

	s2, err := totp.GenerateSecret()
	require.NoError(t, err)
	require.NotEqual(t, s1, s2)
}

func TestEncodeDecodeSecret_RoundTrip(t *testing.T) {
	secret, err := totp.GenerateSecret()
	require.NoError(t, err)

	encoded := totp.EncodeSecret(secret)
	require.NotContains(t, encoded, "=")

	out, err := totp.DecodeSecret(encoded)
	require.NoError(t, err)
	require.Equal(t, secret, out)

	padded := encoded
	if mod := len(padded) % 8; mod != 0 {
		padded += strings.Repeat("=", 8-mod)
	}
	require.Equal(t, secret, mustDecodeBase32(t, padded))
}

func mustDecodeBase32(t *testing.T, s string) []byte {
	t.Helper()
	b, err := base32.StdEncoding.DecodeString(s)
	require.NoError(t, err)
	return b
}

func TestDecodeSecret_Invalid(t *testing.T) {
	_, err := totp.DecodeSecret("")
	require.ErrorIs(t, err, totp.ErrInvalidSecret)

	_, err = totp.DecodeSecret("not!base32!!")
	require.ErrorIs(t, err, totp.ErrInvalidSecret)
}

func TestBuildOtpauthURL(t *testing.T) {
	secret := []byte("12345678901234567890")
	u, err := totp.BuildOtpauthURL(totp.OtpauthURLInput{
		Issuer:  "CloudEP",
		Account: "ACME:AMEX-10000000",
		Secret:  secret,
	})
	require.NoError(t, err)

	parsed, err := url.Parse(u)
	require.NoError(t, err)
	require.Equal(t, "otpauth", parsed.Scheme)
	require.Equal(t, "totp", parsed.Host)
	require.Contains(t, parsed.Path, "CloudEP:")

	q := parsed.Query()
	require.Equal(t, totp.EncodeSecret(secret), q.Get("secret"))
	require.Equal(t, "CloudEP", q.Get("issuer"))
	require.Equal(t, "SHA1", q.Get("algorithm"))
	require.Equal(t, "6", q.Get("digits"))
	require.Equal(t, "30", q.Get("period"))
}

func TestBuildOtpauthURL_Validation(t *testing.T) {
	_, err := totp.BuildOtpauthURL(totp.OtpauthURLInput{Issuer: "", Account: "a", Secret: []byte("s")})
	require.ErrorIs(t, err, totp.ErrInvalidIssuer)

	_, err = totp.BuildOtpauthURL(totp.OtpauthURLInput{Issuer: "x", Account: "", Secret: []byte("s")})
	require.ErrorIs(t, err, totp.ErrInvalidAccount)

	_, err = totp.BuildOtpauthURL(totp.OtpauthURLInput{Issuer: "x", Account: "a", Secret: nil})
	require.ErrorIs(t, err, totp.ErrInvalidSecret)
}

func TestGenerateBackupCodes(t *testing.T) {
	codes, err := totp.GenerateBackupCodes(10, 12)
	require.NoError(t, err)
	require.Len(t, codes, 10)

	seen := make(map[string]struct{}, len(codes))
	for _, c := range codes {
		require.Len(t, c, 12)
		seen[c] = struct{}{}
	}
	require.Len(t, seen, len(codes), "backup codes should be unique")
}

func TestTimeStep(t *testing.T) {
	period := 30 * time.Second
	require.Equal(t, uint64(2), totp.TimeStep(time.Unix(60, 0), period))
	require.Equal(t, uint64(2), totp.TimeStep(time.Unix(89, 0), period))
	require.Equal(t, uint64(3), totp.TimeStep(time.Unix(90, 0), period))
}
