package usecase_test

import (
	"context"
	"sync"
	"testing"
	"time"

	authconfig "gateway/internal/model/auth/config"
	domrepo "gateway/internal/model/auth/domain/repository"
	domusecase "gateway/internal/model/auth/domain/usecase"
	authusecase "gateway/internal/model/auth/usecase"

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

const (
	testTenantDev = "dev-tenant"
	testUIDDev    = "DEV-10000001"
)

func TestTokenUseCaseIssueAndRefresh(t *testing.T) {
	t.Parallel()
	uc := newTokenUC(t, nil)

	pair, err := uc.IssuePair(context.Background(), &domusecase.IssuePairRequest{
		TenantID: testTenantDev,
		UID:      testUIDDev,
	})
	require.NoError(t, err)
	require.NotEmpty(t, pair.AccessToken)
	require.NotEmpty(t, pair.RefreshToken)
	require.Equal(t, int64(900), pair.ExpiresIn)

	claims, err := uc.ParseAccessToken(context.Background(), pair.AccessToken)
	require.NoError(t, err)
	require.Equal(t, testTenantDev, claims.TenantID)
	require.Equal(t, testUIDDev, claims.UID)

	refreshed, err := uc.Refresh(context.Background(), pair.RefreshToken)
	require.NoError(t, err)
	require.NotEmpty(t, refreshed.AccessToken)
	require.NotEqual(t, pair.AccessToken, refreshed.AccessToken)
}

func TestTokenUseCaseInvalidRefresh(t *testing.T) {
	t.Parallel()
	uc := newTokenUC(t, nil)
	_, err := uc.Refresh(context.Background(), "not-a-jwt")
	require.Error(t, err)
}

func TestTokenUseCaseLogoutRevokesPair(t *testing.T) {
	t.Parallel()
	store := newMemRevokeStore()
	uc := newTokenUC(t, store)

	pair, err := uc.IssuePair(context.Background(), &domusecase.IssuePairRequest{
		TenantID: testTenantDev,
		UID:      testUIDDev,
	})
	require.NoError(t, err)

	err = uc.Logout(context.Background(), &domusecase.LogoutRequest{AccessToken: pair.AccessToken})
	require.NoError(t, err)

	_, err = uc.ParseAccessToken(context.Background(), pair.AccessToken)
	require.Error(t, err)

	_, err = uc.Refresh(context.Background(), pair.RefreshToken)
	require.Error(t, err)
}

func TestTokenUseCaseRefreshRotatesAndRevokesOldRefresh(t *testing.T) {
	t.Parallel()
	store := newMemRevokeStore()
	uc := newTokenUC(t, store)

	pair, err := uc.IssuePair(context.Background(), &domusecase.IssuePairRequest{
		TenantID: testTenantDev,
		UID:      testUIDDev,
	})
	require.NoError(t, err)

	refreshed, err := uc.Refresh(context.Background(), pair.RefreshToken)
	require.NoError(t, err)
	require.NotEqual(t, pair.RefreshToken, refreshed.RefreshToken)

	_, err = uc.Refresh(context.Background(), pair.RefreshToken)
	require.Error(t, err)

	claims, err := uc.ParseAccessToken(context.Background(), refreshed.AccessToken)
	require.NoError(t, err)
	require.Equal(t, testUIDDev, claims.UID)
}

func newTokenUC(t *testing.T, revoke domrepo.TokenRevokeStore) domusecase.TokenUseCase {
	t.Helper()
	return authusecase.MustTokenUseCase(authusecase.TokenUseCaseParam{
		Config: authconfig.Config{
			AccessSecret:  "access-secret-32-bytes-minimum!!",
			RefreshSecret: "refresh-secret-32-bytes-minimum!",
			AccessExpire:  900,
			RefreshExpire: 604800,
			ActiveKID:     "v1",
		},
		Revoke: revoke,
	})
}

type memRevokeStore struct {
	mu    sync.Mutex
	pairs map[string]string
	bl    map[string]time.Time
}

func newMemRevokeStore() *memRevokeStore {
	return &memRevokeStore{
		pairs: make(map[string]string),
		bl:    make(map[string]time.Time),
	}
}

func (s *memRevokeStore) SavePair(_ context.Context, accessJTI, refreshJTI string, _, _ time.Duration) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.pairs[accessJTI] = refreshJTI
	s.pairs[refreshJTI] = accessJTI
	return nil
}

func (s *memRevokeStore) GetPairedJTI(_ context.Context, jti string) (string, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.pairs[jti], nil
}

func (s *memRevokeStore) DeletePair(_ context.Context, accessJTI, refreshJTI string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	delete(s.pairs, accessJTI)
	delete(s.pairs, refreshJTI)
	return nil
}

func (s *memRevokeStore) Blacklist(_ context.Context, jti string, ttl time.Duration) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.bl[jti] = time.Now().Add(ttl)
	return nil
}

func (s *memRevokeStore) IsBlacklisted(_ context.Context, jti string) (bool, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	exp, ok := s.bl[jti]
	if !ok {
		return false, nil
	}
	if time.Now().After(exp) {
		delete(s.bl, jti)
		return false, nil
	}
	return true, nil
}
