package usecase_test

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"testing"
	"time"

	"apps/backend/internal/module/member/domain"
	"apps/backend/internal/module/member/usecase"
	tokenUC "apps/backend/internal/module/token/usecase"

	"github.com/stretchr/testify/require"
)

// fakeRepo is test-only (not a production memory store).
type fakeRepo struct {
	mu         sync.Mutex
	byUID      map[int64]*domain.Member
	byEmail    map[string]int64
	byPhone    map[string]int64
	identities map[string]*domain.Identity // platform:login_id
	byUIDIds   map[int64][]string
	seq        int64
	codes      map[string]string
	refresh    map[string]int64
	settings   map[int64]*domain.UserSettings
}

func newFake() *fakeRepo {
	return &fakeRepo{
		byUID: map[int64]*domain.Member{}, byEmail: map[string]int64{}, byPhone: map[string]int64{},
		identities: map[string]*domain.Identity{}, byUIDIds: map[int64][]string{},
		seq: domain.MinMemberUID, codes: map[string]string{}, refresh: map[string]int64{},
		settings: map[int64]*domain.UserSettings{},
	}
}

func idKey(loginID, platform string) string { return platform + ":" + loginID }

func (f *fakeRepo) NextUID(ctx context.Context) (int64, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.seq++
	if f.seq < domain.MinMemberUID {
		f.seq = domain.MinMemberUID + 1
	}
	return f.seq, nil
}
func (f *fakeRepo) CreateMember(ctx context.Context, m *domain.Member) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if m.Email != "" {
		if _, ok := f.byEmail[m.Email]; ok {
			return domain.ErrEmailTaken
		}
		f.byEmail[m.Email] = m.UID
	}
	if m.Phone != "" {
		f.byPhone[m.Phone] = m.UID
	}
	cp := *m
	f.byUID[m.UID] = &cp
	return nil
}
func (f *fakeRepo) FindByUID(ctx context.Context, uid int64) (*domain.Member, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	m, ok := f.byUID[uid]
	if !ok {
		return nil, domain.ErrNotFound
	}
	cp := *m
	return &cp, nil
}
func (f *fakeRepo) FindByEmail(ctx context.Context, email string) (*domain.Member, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	uid, ok := f.byEmail[email]
	if !ok {
		return nil, domain.ErrNotFound
	}
	cp := *f.byUID[uid]
	return &cp, nil
}
func (f *fakeRepo) FindByPhone(ctx context.Context, phone string) (*domain.Member, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	uid, ok := f.byPhone[phone]
	if !ok {
		return nil, domain.ErrNotFound
	}
	cp := *f.byUID[uid]
	return &cp, nil
}
func (f *fakeRepo) UpdateMember(ctx context.Context, m *domain.Member) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if _, ok := f.byUID[m.UID]; !ok {
		return domain.ErrNotFound
	}
	cp := *m
	f.byUID[m.UID] = &cp
	if m.Email != "" {
		f.byEmail[m.Email] = m.UID
	}
	return nil
}
func (f *fakeRepo) ListPage(ctx context.Context, page, pageSize int, query, status string) ([]*domain.Member, int64, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 20
	}
	query = strings.ToLower(strings.TrimSpace(query))
	var all []*domain.Member
	for _, m := range f.byUID {
		if status != "" && m.Status != status {
			continue
		}
		if query != "" {
			uidStr := fmt.Sprintf("%d", m.UID)
			if !strings.Contains(strings.ToLower(m.Email), query) &&
				!strings.Contains(strings.ToLower(m.DisplayName), query) &&
				!strings.Contains(uidStr, query) {
				continue
			}
		}
		cp := *m
		all = append(all, &cp)
	}
	total := int64(len(all))
	start := (page - 1) * pageSize
	if start >= len(all) {
		return []*domain.Member{}, total, nil
	}
	end := start + pageSize
	if end > len(all) {
		end = len(all)
	}
	return all[start:end], total, nil
}
func (f *fakeRepo) CountActiveAdmins(ctx context.Context) (int64, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	var n int64
	for _, m := range f.byUID {
		if m.IsAdmin() && m.Status != domain.StatusSuspended {
			n++
		}
	}
	return n, nil
}

func (f *fakeRepo) FindByInviteCode(ctx context.Context, code string) (*domain.Member, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	code = strings.ToUpper(strings.TrimSpace(code))
	for _, m := range f.byUID {
		if strings.ToUpper(strings.TrimSpace(m.InviteCode)) == code {
			cp := *m
			return &cp, nil
		}
	}
	return nil, domain.ErrNotFound
}

func (f *fakeRepo) ListAllMembers(ctx context.Context) ([]*domain.Member, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	out := make([]*domain.Member, 0, len(f.byUID))
	for _, m := range f.byUID {
		cp := *m
		out = append(out, &cp)
	}
	return out, nil
}

func (f *fakeRepo) CreateIdentity(ctx context.Context, id *domain.Identity) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	k := idKey(id.LoginID, id.Platform)
	if _, ok := f.identities[k]; ok {
		return domain.ErrIdentityTaken
	}
	cp := *id
	f.identities[k] = &cp
	f.byUIDIds[id.UID] = append(f.byUIDIds[id.UID], k)
	return nil
}
func (f *fakeRepo) FindIdentity(ctx context.Context, loginID, platform string) (*domain.Identity, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	id, ok := f.identities[idKey(loginID, platform)]
	if !ok {
		return nil, domain.ErrIdentityNotFound
	}
	cp := *id
	return &cp, nil
}
func (f *fakeRepo) ListIdentitiesByUID(ctx context.Context, uid int64) ([]*domain.Identity, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	var out []*domain.Identity
	for _, k := range f.byUIDIds[uid] {
		if id, ok := f.identities[k]; ok {
			cp := *id
			out = append(out, &cp)
		}
	}
	return out, nil
}
func (f *fakeRepo) DeleteIdentity(ctx context.Context, loginID, platform string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	k := idKey(loginID, platform)
	id, ok := f.identities[k]
	if !ok {
		return domain.ErrIdentityNotFound
	}
	delete(f.identities, k)
	var rest []string
	for _, x := range f.byUIDIds[id.UID] {
		if x != k {
			rest = append(rest, x)
		}
	}
	f.byUIDIds[id.UID] = rest
	return nil
}
func (f *fakeRepo) UpdateIdentityPassword(ctx context.Context, loginID, platform, passwordHash string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	id, ok := f.identities[idKey(loginID, platform)]
	if !ok {
		return domain.ErrIdentityNotFound
	}
	id.PasswordHash = passwordHash
	return nil
}

func (f *fakeRepo) SetCode(kind, key, code string, ttl time.Duration) bool {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.codes[kind+":"+key] = code
	return true
}
func (f *fakeRepo) CheckCode(kind, key, code string) bool {
	f.mu.Lock()
	defer f.mu.Unlock()
	k := kind + ":" + key
	v, ok := f.codes[k]
	if !ok || v != code {
		return false
	}
	delete(f.codes, k)
	return true
}
func (f *fakeRepo) PeekCode(kind, key, code string) bool {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.codes[kind+":"+key] == code
}
func (f *fakeRepo) SaveRefresh(jti string, uid int64, exp time.Time) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.refresh[jti] = uid
}
func (f *fakeRepo) ConsumeRefresh(jti string) (int64, bool) {
	f.mu.Lock()
	defer f.mu.Unlock()
	uid, ok := f.refresh[jti]
	if !ok {
		return 0, false
	}
	delete(f.refresh, jti)
	return uid, true
}
func (f *fakeRepo) RevokeRefresh(jti string) {
	f.mu.Lock()
	defer f.mu.Unlock()
	delete(f.refresh, jti)
}
func (f *fakeRepo) RevokeAllRefreshByUID(ctx context.Context, uid int64) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	for j, u := range f.refresh {
		if u == uid {
			delete(f.refresh, j)
		}
	}
	return nil
}
func (f *fakeRepo) GetSettings(ctx context.Context, uid int64) (*domain.UserSettings, error) {
	return domain.DefaultSettings(uid), nil
}
func (f *fakeRepo) SaveSettings(ctx context.Context, uid int64, s *domain.UserSettings) error {
	return nil
}

func TestAU_01_LoginOk(t *testing.T) {
	store := newFake()
	svc := usecase.NewAuthService(store, tokenUC.NewJWTIssuer("a", "b", 3600, 86400), 10)
	const strong = "Admin123!@#x"
	m, tokens, err := svc.Register(context.Background(), "admin@haixun.local", strong, "Admin")
	require.NoError(t, err)
	m.Roles = []string{"admin", "member"}
	m.Status = domain.StatusActive
	m.EmailVerified = true
	require.NoError(t, store.UpdateMember(context.Background(), m))

	m2, tokens2, err := svc.Login(context.Background(), "admin@haixun.local", strong)
	require.NoError(t, err)
	require.Equal(t, m.UID, m2.UID)
	require.True(t, m.UID >= domain.MinMemberUID)
	require.NotEmpty(t, tokens.AccessToken)
	require.NotEmpty(t, tokens2.AccessToken)
}

func TestAU_02_LoginBadPassword(t *testing.T) {
	store := newFake()
	svc := usecase.NewAuthService(store, tokenUC.NewJWTIssuer("a", "b", 3600, 86400), 10)
	const strong = "Admin123!@#x"
	_, _, err := svc.Register(context.Background(), "admin@haixun.local", strong, "Admin")
	require.NoError(t, err)
	_, _, err = svc.Login(context.Background(), "admin@haixun.local", "wrong")
	require.ErrorIs(t, err, domain.ErrBadPassword)
}

func TestAR_02_RegisterOk(t *testing.T) {
	store := newFake()
	svc := usecase.NewAuthService(store, tokenUC.NewJWTIssuer("a", "b", 3600, 86400), 10)
	m, tokens, err := svc.Register(context.Background(), "new@example.com", "Secret12!@#a", "New")
	require.NoError(t, err)
	require.True(t, m.UID >= domain.MinMemberUID)
	require.NotEmpty(t, tokens.AccessToken)
	ids, err := svc.ListIdentities(context.Background(), m.UID)
	require.NoError(t, err)
	require.Len(t, ids, 1)
}

func TestAU_07_Refresh(t *testing.T) {
	store := newFake()
	svc := usecase.NewAuthService(store, tokenUC.NewJWTIssuer("sec-a", "sec-r", 3600, 86400), 10)
	_, tokens, err := svc.Register(context.Background(), "r@example.com", "Secret12!@#a", "R")
	require.NoError(t, err)
	next, err := svc.Refresh(context.Background(), tokens.RefreshToken)
	require.NoError(t, err)
	require.NotEmpty(t, next.AccessToken)
	_, err = svc.Refresh(context.Background(), tokens.RefreshToken)
	require.Error(t, err)
}

func TestBind_Unbind(t *testing.T) {
	store := newFake()
	svc := usecase.NewAuthService(store, tokenUC.NewJWTIssuer("a", "b", 3600, 86400), 10)
	m, _, err := svc.Register(context.Background(), "b@example.com", "Secret12!@#a", "B")
	require.NoError(t, err)
	id, err := svc.BindAccount(context.Background(), m.UID, "g-sub-1", domain.PlatformGoogle, domain.AccountTypePlatform, "")
	require.NoError(t, err)
	require.Equal(t, "g-sub-1", id.LoginID)
	ids, _ := svc.ListIdentities(context.Background(), m.UID)
	require.Len(t, ids, 2)
	require.NoError(t, svc.UnbindAccount(context.Background(), m.UID, "g-sub-1", domain.PlatformGoogle))
	err = svc.UnbindAccount(context.Background(), m.UID, "b@example.com", domain.PlatformPassword)
	require.Error(t, err)
}

func TestLoginOAuth_TrustedProviderIdentity(t *testing.T) {
	store := newFake()
	svc := usecase.NewAuthService(store, tokenUC.NewJWTIssuer("a", "b", 3600, 86400), 10)
	m, pair, err := svc.LoginOAuth(context.Background(), domain.PlatformGoogle, "g123", "u@g.com", "G123")
	require.NoError(t, err)
	require.NotEmpty(t, pair.AccessToken)
	require.True(t, m.UID >= domain.MinMemberUID)
	m2, _, err := svc.LoginOAuth(context.Background(), domain.PlatformGoogle, "g123", "u@g.com", "G123")
	require.NoError(t, err)
	require.Equal(t, m.UID, m2.UID)
}

func TestMember_UidIs8DigitsFrom1M(t *testing.T) {
	require.Equal(t, int64(1_000_000), domain.MinMemberUID)
	// 1_000_000 是 7 位；顯示補零成 8 位：01000000
	require.Equal(t, "01000000", fmt.Sprintf("%08d", domain.MinMemberUID))
	require.Equal(t, "01000001", fmt.Sprintf("%08d", domain.MinMemberUID+1))
}
