package api_test

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/hhs/camtalk/internal/api"
	"github.com/hhs/camtalk/internal/auth"
)

// mockAuthService 实现 auth.Service 接口，用于 API 测试。
type mockAuthService struct {
	RegisterFunc func(ctx context.Context, req auth.RegisterRequest) (*auth.AuthResponse, error)
	LoginFunc    func(ctx context.Context, req auth.LoginRequest) (*auth.AuthResponse, error)
	RefreshFunc  func(ctx context.Context, req auth.RefreshRequest) (*auth.AuthResponse, error)
	LogoutFunc   func(ctx context.Context, userID, refreshToken string) error
}

func (m *mockAuthService) Register(ctx context.Context, req auth.RegisterRequest) (*auth.AuthResponse, error) {
	return m.RegisterFunc(ctx, req)
}

func (m *mockAuthService) Login(ctx context.Context, req auth.LoginRequest) (*auth.AuthResponse, error) {
	return m.LoginFunc(ctx, req)
}

func (m *mockAuthService) Refresh(ctx context.Context, req auth.RefreshRequest) (*auth.AuthResponse, error) {
	return m.RefreshFunc(ctx, req)
}

func (m *mockAuthService) Logout(ctx context.Context, userID, refreshToken string) error {
	return m.LogoutFunc(ctx, userID, refreshToken)
}

// newTestRouter 创建带 AuthHandler 路由的测试 Gin 引擎。
func newTestRouter(svc auth.Service) *gin.Engine {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	tm := auth.NewTokenManager("test-secret", 15*time.Minute, 7*24*time.Hour)
	h := api.NewAuthHandler(svc, tm)
	h.RegisterRoutes(r.Group("/api"), nil) // 测试时不启用限流
	return r
}

// newTestRouterWithToken 创建带 AuthHandler 路由的测试引擎，同时返回 TokenManager 以便生成测试 token。
func newTestRouterWithToken(svc auth.Service) (*gin.Engine, *auth.TokenManager) {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	tm := auth.NewTokenManager("test-secret", 15*time.Minute, 7*24*time.Hour)
	h := api.NewAuthHandler(svc, tm)
	h.RegisterRoutes(r.Group("/api"), nil) // 测试时不启用限流
	return r, tm
}

func sampleAuthResponse() *auth.AuthResponse {
	return &auth.AuthResponse{
		User: auth.UserResponse{
			ID:       "user-123",
			Username: "alice",
		},
		AccessToken:  "access-token",
		RefreshToken: "refresh-token",
	}
}

// --- Register ---

func TestRegister_Success(t *testing.T) {
	svc := &mockAuthService{
		RegisterFunc: func(_ context.Context, req auth.RegisterRequest) (*auth.AuthResponse, error) {
			assert.Equal(t, "alice", req.Username)
			assert.Equal(t, "password123", req.Password)
			return sampleAuthResponse(), nil
		},
	}
	r := newTestRouter(svc)

	body, _ := json.Marshal(auth.RegisterRequest{Username: "alice", Password: "password123"})
	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusCreated, w.Code)
	var resp auth.AuthResponse
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	assert.Equal(t, "alice", resp.User.Username)
	assert.NotEmpty(t, resp.AccessToken)
}

func TestRegister_InvalidInput_EmptyBody(t *testing.T) {
	svc := &mockAuthService{}
	r := newTestRouter(svc)

	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", nil)
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusBadRequest, w.Code)
	assert.Contains(t, w.Body.String(), "INVALID_INPUT")
}

func TestRegister_InvalidInput_UsernameTooShort(t *testing.T) {
	svc := &mockAuthService{}
	r := newTestRouter(svc)

	body, _ := json.Marshal(auth.RegisterRequest{Username: "ab", Password: "password123"})
	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusBadRequest, w.Code)
	assert.Contains(t, w.Body.String(), "username must be 3-64 characters")
}

func TestRegister_InvalidInput_PasswordTooShort(t *testing.T) {
	svc := &mockAuthService{}
	r := newTestRouter(svc)

	body, _ := json.Marshal(auth.RegisterRequest{Username: "alice", Password: "short"})
	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusBadRequest, w.Code)
	assert.Contains(t, w.Body.String(), "password must be 8-72 characters")
}

func TestRegister_UsernameTaken(t *testing.T) {
	svc := &mockAuthService{
		RegisterFunc: func(_ context.Context, _ auth.RegisterRequest) (*auth.AuthResponse, error) {
			return nil, auth.ErrUsernameTaken
		},
	}
	r := newTestRouter(svc)

	body, _ := json.Marshal(auth.RegisterRequest{Username: "alice", Password: "password123"})
	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusConflict, w.Code)
	assert.Contains(t, w.Body.String(), "USERNAME_TAKEN")
}

// --- Login ---

func TestLogin_Success(t *testing.T) {
	svc := &mockAuthService{
		LoginFunc: func(_ context.Context, req auth.LoginRequest) (*auth.AuthResponse, error) {
			assert.Equal(t, "alice", req.Username)
			assert.Equal(t, "password123", req.Password)
			return sampleAuthResponse(), nil
		},
	}
	r := newTestRouter(svc)

	body, _ := json.Marshal(auth.LoginRequest{Username: "alice", Password: "password123"})
	req := httptest.NewRequest(http.MethodPost, "/api/auth/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	var resp auth.AuthResponse
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	assert.Equal(t, "alice", resp.User.Username)
}

func TestLogin_InvalidCredentials(t *testing.T) {
	svc := &mockAuthService{
		LoginFunc: func(_ context.Context, _ auth.LoginRequest) (*auth.AuthResponse, error) {
			return nil, auth.ErrInvalidCredentials
		},
	}
	r := newTestRouter(svc)

	body, _ := json.Marshal(auth.LoginRequest{Username: "alice", Password: "wrong-password"})
	req := httptest.NewRequest(http.MethodPost, "/api/auth/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusUnauthorized, w.Code)
	assert.Contains(t, w.Body.String(), "INVALID_CREDENTIALS")
}

// --- Refresh ---

func TestRefresh_Success(t *testing.T) {
	svc := &mockAuthService{
		RefreshFunc: func(_ context.Context, req auth.RefreshRequest) (*auth.AuthResponse, error) {
			assert.Equal(t, "some-refresh-token", req.RefreshToken)
			return sampleAuthResponse(), nil
		},
	}
	r := newTestRouter(svc)

	body, _ := json.Marshal(auth.RefreshRequest{RefreshToken: "some-refresh-token"})
	req := httptest.NewRequest(http.MethodPost, "/api/auth/refresh", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
}

func TestRefresh_MissingToken(t *testing.T) {
	svc := &mockAuthService{}
	r := newTestRouter(svc)

	body, _ := json.Marshal(auth.RefreshRequest{RefreshToken: ""})
	req := httptest.NewRequest(http.MethodPost, "/api/auth/refresh", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusBadRequest, w.Code)
	assert.Contains(t, w.Body.String(), "refresh_token is required")
}

func TestRefresh_UsedToken(t *testing.T) {
	svc := &mockAuthService{
		RefreshFunc: func(_ context.Context, _ auth.RefreshRequest) (*auth.AuthResponse, error) {
			return nil, auth.ErrRefreshTokenUsed
		},
	}
	r := newTestRouter(svc)

	body, _ := json.Marshal(auth.RefreshRequest{RefreshToken: "used-token"})
	req := httptest.NewRequest(http.MethodPost, "/api/auth/refresh", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusUnauthorized, w.Code)
	assert.Contains(t, w.Body.String(), "INVALID_TOKEN")
}

// --- Logout ---

func TestLogout_Success(t *testing.T) {
	logoutCalled := false
	svc := &mockAuthService{
		LogoutFunc: func(_ context.Context, userID, refreshToken string) error {
			assert.Equal(t, "user-123", userID)
			assert.Equal(t, "refresh-token-to-revoke", refreshToken)
			logoutCalled = true
			return nil
		},
	}
	r, tm := newTestRouterWithToken(svc)

	// 生成有效 token
	access, _, err := tm.GeneratePair("user-123", "alice")
	require.NoError(t, err)

	body, _ := json.Marshal(map[string]string{"refresh_token": "refresh-token-to-revoke"})
	req := httptest.NewRequest(http.MethodPost, "/api/auth/logout", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+access)
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	assert.True(t, logoutCalled)
	assert.Contains(t, w.Body.String(), "logged out successfully")
}

func TestLogout_MissingAuth(t *testing.T) {
	svc := &mockAuthService{}
	r := newTestRouter(svc)

	body, _ := json.Marshal(map[string]string{"refresh_token": "some-token"})
	req := httptest.NewRequest(http.MethodPost, "/api/auth/logout", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusUnauthorized, w.Code)
}

func TestLogout_MissingRefreshToken(t *testing.T) {
	svc := &mockAuthService{}
	r, tm := newTestRouterWithToken(svc)

	access, _, err := tm.GeneratePair("user-123", "alice")
	require.NoError(t, err)

	body, _ := json.Marshal(map[string]string{"refresh_token": ""})
	req := httptest.NewRequest(http.MethodPost, "/api/auth/logout", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+access)
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusBadRequest, w.Code)
	assert.Contains(t, w.Body.String(), "refresh_token is required")
}
