package usecase

import (
	"context"
	"testing"
	"time"

	"backend/internal/config"
	"backend/pkg/permission/domain/entity"
	"backend/pkg/permission/domain/token"
	"backend/pkg/permission/mock/repository"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

func TestTokenUseCase_NewToken(t *testing.T) {
	mockRepo := repository.NewMockTokenRepository(t)
	cfg := &config.Config{
		Token: struct {
			AccessSecret         string
			RefreshSecret        string
			AccessTokenExpiry    time.Duration
			RefreshTokenExpiry   time.Duration
			OneTimeTokenExpiry   time.Duration
			MaxTokensPerUser     int
			MaxTokensPerDevice   int
		}{
			AccessSecret:       "test-access-secret",
			RefreshSecret:      "test-refresh-secret",
			AccessTokenExpiry:  15 * time.Minute,
			RefreshTokenExpiry: 7 * 24 * time.Hour,
			MaxTokensPerUser:   10,
			MaxTokensPerDevice: 5,
		},
	}

	useCase := &TokenUseCase{
		TokenUseCaseParam: TokenUseCaseParam{
			TokenRepo: mockRepo,
			Config:    cfg,
		},
	}

	tests := []struct {
		name    string
		req     entity.AuthorizationReq
		setup   func()
		wantErr bool
	}{
		{
			name: "successful token creation",
			req: entity.AuthorizationReq{
				GrantType:      token.PasswordCredentials.ToString(),
				Scope:          "read write",
				DeviceID:       "device123",
				IsRefreshToken: true,
				Data: map[string]string{
					"uid":  "user123",
					"role": "user",
				},
			},
			setup: func() {
				mockRepo.On("Create", mock.Anything, mock.AnythingOfType("entity.Token")).
					Return(nil).Once()
			},
			wantErr: false,
		},
		{
			name: "repository error",
			req: entity.AuthorizationReq{
				GrantType: token.PasswordCredentials.ToString(),
				Scope:     "read",
				DeviceID:  "device123",
			},
			setup: func() {
				mockRepo.On("Create", mock.Anything, mock.AnythingOfType("entity.Token")).
					Return(assert.AnError).Once()
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.setup()

			resp, err := useCase.NewToken(context.Background(), tt.req)

			if tt.wantErr {
				assert.Error(t, err)
				assert.Empty(t, resp.AccessToken)
			} else {
				assert.NoError(t, err)
				assert.NotEmpty(t, resp.AccessToken)
				assert.Equal(t, token.TypeBearer.String(), resp.TokenType)
				assert.Greater(t, resp.ExpiresIn, int64(0))
				if tt.req.IsRefreshToken {
					assert.NotEmpty(t, resp.RefreshToken)
				}
			}

			mockRepo.AssertExpectations(t)
		})
	}
}

func TestTokenUseCase_ValidationToken(t *testing.T) {
	mockRepo := repository.NewMockTokenRepository(t)
	cfg := &config.Config{
		Token: struct {
			AccessSecret         string
			RefreshSecret        string
			AccessTokenExpiry    time.Duration
			RefreshTokenExpiry   time.Duration
			OneTimeTokenExpiry   time.Duration
			MaxTokensPerUser     int
			MaxTokensPerDevice   int
		}{
			AccessSecret:       "test-access-secret",
			RefreshSecret:      "test-refresh-secret",
			AccessTokenExpiry:  15 * time.Minute,
			RefreshTokenExpiry: 7 * 24 * time.Hour,
		},
	}

	useCase := &TokenUseCase{
		TokenUseCaseParam: TokenUseCaseParam{
			TokenRepo: mockRepo,
			Config:    cfg,
		},
	}

	// 先創建一個有效的 token 用於測試
	tokenReq := entity.AuthorizationReq{
		GrantType: token.PasswordCredentials.ToString(),
		Data: map[string]string{
			"uid":  "user123",
			"role": "user",
		},
	}

	mockRepo.On("Create", mock.Anything, mock.AnythingOfType("entity.Token")).
		Return(nil).Once()

	tokenResp, err := useCase.NewToken(context.Background(), tokenReq)
	assert.NoError(t, err)
	assert.NotEmpty(t, tokenResp.AccessToken)

	// 測試驗證
	tests := []struct {
		name    string
		req     entity.ValidationTokenReq
		setup   func()
		wantErr bool
	}{
		{
			name: "valid token",
			req: entity.ValidationTokenReq{
				Token: tokenResp.AccessToken,
			},
			setup: func() {
				mockRepo.On("GetAccessTokenByID", mock.Anything, mock.AnythingOfType("string")).
					Return(entity.Token{
						ID:          "test-id",
						UID:         "user123",
						AccessToken: tokenResp.AccessToken,
						ExpiresIn:   int(cfg.Token.AccessTokenExpiry.Seconds()),
					}, nil).Once()
			},
			wantErr: false,
		},
		{
			name: "invalid token",
			req: entity.ValidationTokenReq{
				Token: "invalid-token",
			},
			setup: func() {
				// parseClaims will fail for invalid token
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.setup()

			resp, err := useCase.ValidationToken(context.Background(), tt.req)

			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.NotEmpty(t, resp.Token.ID)
				assert.Equal(t, "user123", resp.Token.UID)
			}

			mockRepo.AssertExpectations(t)
		})
	}
}

func TestTokenUseCase_BlacklistToken(t *testing.T) {
	mockRepo := repository.NewMockTokenRepository(t)
	cfg := &config.Config{
		Token: struct {
			AccessSecret         string
			RefreshSecret        string
			AccessTokenExpiry    time.Duration
			RefreshTokenExpiry   time.Duration
			OneTimeTokenExpiry   time.Duration
			MaxTokensPerUser     int
			MaxTokensPerDevice   int
		}{
			AccessSecret:       "test-access-secret",
			RefreshSecret:      "test-refresh-secret",
			AccessTokenExpiry:  15 * time.Minute,
			RefreshTokenExpiry: 7 * 24 * time.Hour,
		},
	}

	useCase := &TokenUseCase{
		TokenUseCaseParam: TokenUseCaseParam{
			TokenRepo: mockRepo,
			Config:    cfg,
		},
	}

	// 先創建一個有效的 token
	tokenReq := entity.AuthorizationReq{
		GrantType: token.PasswordCredentials.ToString(),
		Data: map[string]string{
			"uid":  "user123",
			"role": "user",
		},
	}

	mockRepo.On("Create", mock.Anything, mock.AnythingOfType("entity.Token")).
		Return(nil).Once()

	tokenResp, err := useCase.NewToken(context.Background(), tokenReq)
	assert.NoError(t, err)

	tests := []struct {
		name    string
		token   string
		reason  string
		setup   func()
		wantErr bool
	}{
		{
			name:   "successful blacklist",
			token:  tokenResp.AccessToken,
			reason: "user logout",
			setup: func() {
				mockRepo.On("AddToBlacklist", mock.Anything, mock.AnythingOfType("*entity.BlacklistEntry"), mock.AnythingOfType("time.Duration")).
					Return(nil).Once()
			},
			wantErr: false,
		},
		{
			name:   "invalid token",
			token:  "invalid-token",
			reason: "test",
			setup:  func() {},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.setup()

			err := useCase.BlacklistToken(context.Background(), tt.token, tt.reason)

			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}

			mockRepo.AssertExpectations(t)
		})
	}
}

func TestTokenUseCase_IsTokenBlacklisted(t *testing.T) {
	mockRepo := repository.NewMockTokenRepository(t)
	cfg := &config.Config{
		Token: struct {
			AccessSecret         string
			RefreshSecret        string
			AccessTokenExpiry    time.Duration
			RefreshTokenExpiry   time.Duration
			OneTimeTokenExpiry   time.Duration
			MaxTokensPerUser     int
			MaxTokensPerDevice   int
		}{
			AccessSecret: "test-secret",
		},
	}

	useCase := &TokenUseCase{
		TokenUseCaseParam: TokenUseCaseParam{
			TokenRepo: mockRepo,
			Config:    cfg,
		},
	}

	tests := []struct {
		name       string
		jti        string
		setup      func()
		wantResult bool
		wantErr    bool
	}{
		{
			name: "token is blacklisted",
			jti:  "test-jti-123",
			setup: func() {
				mockRepo.On("IsBlacklisted", mock.Anything, "test-jti-123").
					Return(true, nil).Once()
			},
			wantResult: true,
			wantErr:    false,
		},
		{
			name: "token is not blacklisted",
			jti:  "test-jti-456",
			setup: func() {
				mockRepo.On("IsBlacklisted", mock.Anything, "test-jti-456").
					Return(false, nil).Once()
			},
			wantResult: false,
			wantErr:    false,
		},
		{
			name: "repository error",
			jti:  "test-jti-error",
			setup: func() {
				mockRepo.On("IsBlacklisted", mock.Anything, "test-jti-error").
					Return(false, assert.AnError).Once()
			},
			wantResult: false,
			wantErr:    true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.setup()

			result, err := useCase.IsTokenBlacklisted(context.Background(), tt.jti)

			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.wantResult, result)
			}

			mockRepo.AssertExpectations(t)
		})
	}
}

func TestTokenUseCase_CancelTokens(t *testing.T) {
	mockRepo := repository.NewMockTokenRepository(t)
	cfg := &config.Config{}

	useCase := &TokenUseCase{
		TokenUseCaseParam: TokenUseCaseParam{
			TokenRepo: mockRepo,
			Config:    cfg,
		},
	}

	tests := []struct {
		name    string
		req     entity.DoTokenByUIDReq
		setup   func()
		wantErr bool
	}{
		{
			name: "cancel by UID",
			req: entity.DoTokenByUIDReq{
				UID: "user123",
			},
			setup: func() {
				mockRepo.On("DeleteAccessTokensByUID", mock.Anything, "user123").
					Return(nil).Once()
			},
			wantErr: false,
		},
		{
			name: "cancel by token IDs",
			req: entity.DoTokenByUIDReq{
				IDs: []string{"token1", "token2"},
			},
			setup: func() {
				mockRepo.On("DeleteAccessTokenByID", mock.Anything, []string{"token1", "token2"}).
					Return(nil).Once()
			},
			wantErr: false,
		},
		{
			name: "repository error",
			req: entity.DoTokenByUIDReq{
				UID: "user123",
			},
			setup: func() {
				mockRepo.On("DeleteAccessTokensByUID", mock.Anything, "user123").
					Return(assert.AnError).Once()
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.setup()

			err := useCase.CancelTokens(context.Background(), tt.req)

			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}

			mockRepo.AssertExpectations(t)
		})
	}
}