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_RefreshToken(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,
		},
	}

	// Create a base token first
	tokenReq := entity.AuthorizationReq{
		GrantType: token.PasswordCredentials.ToString(),
		Data: map[string]string{
			"uid":  "user123",
			"role": "user",
		},
		IsRefreshToken: true,
	}

	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
		req     entity.RefreshTokenReq
		setup   func()
		wantErr bool
	}{
		{
			name: "successful token refresh",
			req: entity.RefreshTokenReq{
				Token:    tokenResp.RefreshToken,
				Scope:    "read write",
				DeviceID: "device123",
			},
			setup: func() {
				existingToken := entity.Token{
					ID:          "old-token-id",
					UID:         "user123",
					AccessToken: tokenResp.AccessToken,
					ExpiresIn:   int(time.Now().Add(time.Hour).Unix()),
				}

				mockRepo.On("GetAccessTokenByOneTimeToken", mock.Anything, tokenResp.RefreshToken).
					Return(existingToken, nil).Once()
				mockRepo.On("Create", mock.Anything, mock.AnythingOfType("entity.Token")).
					Return(nil).Once()
				mockRepo.On("Delete", mock.Anything, mock.AnythingOfType("entity.Token")).
					Return(nil).Once()
			},
			wantErr: false,
		},
		{
			name: "invalid refresh token",
			req: entity.RefreshTokenReq{
				Token:    "invalid-refresh-token",
				Scope:    "read",
				DeviceID: "device123",
			},
			setup: func() {
				mockRepo.On("GetAccessTokenByOneTimeToken", mock.Anything, "invalid-refresh-token").
					Return(entity.Token{}, assert.AnError).Once()
			},
			wantErr: true,
		},
	}

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

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

			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.NotEmpty(t, resp.Token)
				assert.NotEmpty(t, resp.OneTimeToken)
				assert.Equal(t, token.TypeBearer.String(), resp.TokenType)
			}

			mockRepo.AssertExpectations(t)
		})
	}
}

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

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

	tests := []struct {
		name    string
		req     entity.QueryTokenByUIDReq
		setup   func()
		wantErr bool
	}{
		{
			name: "get tokens successfully",
			req: entity.QueryTokenByUIDReq{
				UID: "user123",
			},
			setup: func() {
				tokens := []entity.Token{
					{
						ID:          "token1",
						UID:         "user123",
						AccessToken: "access1",
						ExpiresIn:   3600,
					},
					{
						ID:          "token2",
						UID:         "user123",
						AccessToken: "access2",
						ExpiresIn:   3600,
					},
				}
				mockRepo.On("GetAccessTokensByUID", mock.Anything, "user123").
					Return(tokens, nil).Once()
			},
			wantErr: false,
		},
		{
			name: "repository error",
			req: entity.QueryTokenByUIDReq{
				UID: "user456",
			},
			setup: func() {
				mockRepo.On("GetAccessTokensByUID", mock.Anything, "user456").
					Return([]entity.Token(nil), assert.AnError).Once()
			},
			wantErr: true,
		},
	}

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

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

			if tt.wantErr {
				assert.Error(t, err)
				assert.Nil(t, tokens)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, tokens)
				assert.Greater(t, len(tokens), 0)
			}

			mockRepo.AssertExpectations(t)
		})
	}
}

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

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

	tests := []struct {
		name    string
		req     entity.DoTokenByDeviceIDReq
		setup   func()
		wantErr bool
	}{
		{
			name: "get tokens by device successfully",
			req: entity.DoTokenByDeviceIDReq{
				DeviceID: "device123",
			},
			setup: func() {
				tokens := []entity.Token{
					{
						ID:          "token1",
						UID:         "user123",
						DeviceID:    "device123",
						AccessToken: "access1",
						ExpiresIn:   3600,
					},
				}
				mockRepo.On("GetAccessTokensByDeviceID", mock.Anything, "device123").
					Return(tokens, nil).Once()
			},
			wantErr: false,
		},
		{
			name: "repository error",
			req: entity.DoTokenByDeviceIDReq{
				DeviceID: "device456",
			},
			setup: func() {
				mockRepo.On("GetAccessTokensByDeviceID", mock.Anything, "device456").
					Return([]entity.Token(nil), assert.AnError).Once()
			},
			wantErr: true,
		},
	}

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

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

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

			mockRepo.AssertExpectations(t)
		})
	}
}

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

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

	tests := []struct {
		name    string
		req     entity.DoTokenByDeviceIDReq
		setup   func()
		wantErr bool
	}{
		{
			name: "cancel tokens successfully",
			req: entity.DoTokenByDeviceIDReq{
				DeviceID: "device123",
			},
			setup: func() {
				mockRepo.On("DeleteAccessTokensByDeviceID", mock.Anything, "device123").
					Return(nil).Once()
			},
			wantErr: false,
		},
		{
			name: "repository error",
			req: entity.DoTokenByDeviceIDReq{
				DeviceID: "device456",
			},
			setup: func() {
				mockRepo.On("DeleteAccessTokensByDeviceID", mock.Anything, "device456").
					Return(assert.AnError).Once()
			},
			wantErr: true,
		},
	}

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

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

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

			mockRepo.AssertExpectations(t)
		})
	}
}

func TestTokenUseCase_NewOneTimeToken(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",
			AccessTokenExpiry:  15 * time.Minute,
			RefreshTokenExpiry: 7 * 24 * time.Hour,
		},
	}

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

	// Create a base token first
	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
		req     entity.CreateOneTimeTokenReq
		setup   func()
		wantErr bool
	}{
		{
			name: "create one-time token successfully",
			req: entity.CreateOneTimeTokenReq{
				Token: tokenResp.AccessToken,
			},
			setup: func() {
				existingToken := entity.Token{
					ID:          "token-id",
					UID:         "user123",
					AccessToken: tokenResp.AccessToken,
					ExpiresIn:   int(time.Now().Add(time.Hour).Unix()),
				}

				mockRepo.On("GetAccessTokenByID", mock.Anything, mock.AnythingOfType("string")).
					Return(existingToken, nil).Once()
				mockRepo.On("CreateOneTimeToken", mock.Anything, mock.AnythingOfType("string"), 
					mock.AnythingOfType("entity.Ticket"), mock.AnythingOfType("time.Duration")).
					Return(nil).Once()
			},
			wantErr: false,
		},
		{
			name: "invalid token",
			req: entity.CreateOneTimeTokenReq{
				Token: "invalid-token",
			},
			setup: func() {
				// parseClaims will fail
			},
			wantErr: true,
		},
	}

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

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

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

			mockRepo.AssertExpectations(t)
		})
	}
}

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

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

	tests := []struct {
		name    string
		req     entity.CancelOneTimeTokenReq
		setup   func()
		wantErr bool
	}{
		{
			name: "cancel one-time token successfully",
			req: entity.CancelOneTimeTokenReq{
				Token: []string{"token1", "token2"},
			},
			setup: func() {
				mockRepo.On("DeleteOneTimeToken", mock.Anything, []string{"token1", "token2"}, mock.Anything).
					Return(nil).Once()
			},
			wantErr: false,
		},
		{
			name: "repository error",
			req: entity.CancelOneTimeTokenReq{
				Token: []string{"token3"},
			},
			setup: func() {
				mockRepo.On("DeleteOneTimeToken", mock.Anything, []string{"token3"}, mock.Anything).
					Return(assert.AnError).Once()
			},
			wantErr: true,
		},
	}

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

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

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

			mockRepo.AssertExpectations(t)
		})
	}
}

func TestTokenUseCase_ReadTokenBasicData(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",
			AccessTokenExpiry:  15 * time.Minute,
			RefreshTokenExpiry: 7 * 24 * time.Hour,
		},
	}

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

	// Create a valid token first
	tokenReq := entity.AuthorizationReq{
		GrantType: token.PasswordCredentials.ToString(),
		Data: map[string]string{
			"uid":  "user123",
			"role": "admin",
		},
		Role: "admin",
	}

	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
		wantErr bool
	}{
		{
			name:    "read valid token",
			token:   tokenResp.AccessToken,
			wantErr: false,
		},
		{
			name:    "invalid token",
			token:   "invalid-token",
			wantErr: true,
		},
		{
			name:    "empty token",
			token:   "",
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			claims, err := useCase.ReadTokenBasicData(context.Background(), tt.token)

			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, claims)
				assert.Equal(t, "user123", claims["uid"])
				assert.Equal(t, "admin", claims["role"])
			}

			mockRepo.AssertExpectations(t)
		})
	}
}

// TestTokenUseCase_BlacklistAllUserTokens is commented out due to complexity of mocking
// the JWT parsing within the loop. The functionality is tested through integration tests.
// func TestTokenUseCase_BlacklistAllUserTokens(t *testing.T) { ... }

