//go:build integration || error_handling
// +build integration error_handling

package error_handling

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zaptest"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"

	apperrors "veza-backend-api/internal/errors"
	"veza-backend-api/internal/middleware"
	"veza-backend-api/internal/models"
	"veza-backend-api/internal/response"
)

// setupErrorHandlingTestRouter crée un router de test pour les tests de gestion d'erreurs
func setupErrorHandlingTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, func()) {
	gin.SetMode(gin.TestMode)
	logger := zaptest.NewLogger(t)

	// Setup in-memory SQLite database
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	require.NoError(t, err)
	db.Exec("PRAGMA foreign_keys = ON")

	// Auto-migrate models
	err = db.AutoMigrate(
		&models.User{},
		&models.Track{},
	)
	require.NoError(t, err)

	// Create router with error handler
	router := gin.New()
	router.Use(middleware.ErrorHandler(logger, nil, false))

	// Test endpoints that return different error types
	router.GET("/test/validation-error", func(c *gin.Context) {
		c.Error(apperrors.NewValidationError("validation failed", apperrors.ErrorDetail{
			Field:   "email",
			Message: "invalid email format",
		}))
		c.Abort()
	})

	router.GET("/test/not-found", func(c *gin.Context) {
		c.Error(apperrors.NewNotFoundError("resource"))
		c.Abort()
	})

	router.GET("/test/unauthorized", func(c *gin.Context) {
		c.Error(apperrors.NewUnauthorizedError("unauthorized"))
		c.Abort()
	})

	router.GET("/test/forbidden", func(c *gin.Context) {
		c.Error(apperrors.NewForbiddenError("forbidden"))
		c.Abort()
	})

	router.GET("/test/internal-error", func(c *gin.Context) {
		c.Error(apperrors.New(apperrors.ErrCodeInternal, "internal server error"))
		c.Abort()
	})

	router.GET("/test/database-error", func(c *gin.Context) {
		c.Error(apperrors.New(apperrors.ErrCodeDatabase, "database error"))
		c.Abort()
	})

	router.GET("/test/conflict", func(c *gin.Context) {
		c.Error(apperrors.New(apperrors.ErrCodeConflict, "resource conflict"))
		c.Abort()
	})

	router.GET("/test/rate-limit", func(c *gin.Context) {
		c.Error(apperrors.New(apperrors.ErrCodeRateLimitExceeded, "rate limit exceeded"))
		c.Abort()
	})

	router.GET("/test/quota-exceeded", func(c *gin.Context) {
		c.Error(apperrors.New(apperrors.ErrCodeQuotaExceeded, "quota exceeded"))
		c.Abort()
	})

	// Endpoint that recovers from error
	router.GET("/test/recovery", func(c *gin.Context) {
		// Simulate error
		if c.Query("error") == "true" {
			c.Error(apperrors.New(apperrors.ErrCodeInternal, "simulated error"))
			c.Abort()
			return
		}
		// Success path
		response.Success(c, gin.H{"message": "recovered"}, "success")
	})

	// Endpoint with error details
	router.POST("/test/validation-details", func(c *gin.Context) {
		var req struct {
			Email    string `json:"email"`
			Password string `json:"password"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			details := []apperrors.ErrorDetail{
				{Field: "email", Message: "email is required"},
				{Field: "password", Message: "password is required"},
			}
			c.Error(apperrors.NewValidationError("validation failed", details...))
			c.Abort()
			return
		}
		response.Success(c, gin.H{"email": req.Email}, "valid")
	})

	cleanup := func() {
		// Cleanup handled by in-memory DB
	}

	return router, db, cleanup
}

// TestErrorResponse_Format teste que les réponses d'erreur sont au format standardisé
func TestErrorResponse_Format(t *testing.T) {
	router, _, cleanup := setupErrorHandlingTestRouter(t)
	defer cleanup()

	tests := []struct {
		name           string
		endpoint       string
		expectedStatus int
		expectedCode   apperrors.ErrorCode
		validateError  func(t *testing.T, body []byte)
	}{
		{
			name:           "Validation Error",
			endpoint:       "/test/validation-error",
			expectedStatus: http.StatusBadRequest,
			expectedCode:   apperrors.ErrCodeValidation,
			validateError: func(t *testing.T, body []byte) {
				var resp map[string]interface{}
				err := json.Unmarshal(body, &resp)
				require.NoError(t, err)

				errorData, ok := resp["error"].(map[string]interface{})
				require.True(t, ok, "Error should be a map")
				assert.Equal(t, float64(apperrors.ErrCodeValidation), errorData["code"])
				assert.Contains(t, errorData, "message")
				assert.Contains(t, errorData, "details")
			},
		},
		{
			name:           "Not Found Error",
			endpoint:       "/test/not-found",
			expectedStatus: http.StatusNotFound,
			expectedCode:   apperrors.ErrCodeNotFound,
			validateError: func(t *testing.T, body []byte) {
				var resp map[string]interface{}
				err := json.Unmarshal(body, &resp)
				require.NoError(t, err)

				errorData, ok := resp["error"].(map[string]interface{})
				require.True(t, ok)
				assert.Equal(t, float64(apperrors.ErrCodeNotFound), errorData["code"])
			},
		},
		{
			name:           "Unauthorized Error",
			endpoint:       "/test/unauthorized",
			expectedStatus: http.StatusUnauthorized,
			expectedCode:   apperrors.ErrCodeUnauthorized,
			validateError: func(t *testing.T, body []byte) {
				var resp map[string]interface{}
				err := json.Unmarshal(body, &resp)
				require.NoError(t, err)

				errorData, ok := resp["error"].(map[string]interface{})
				require.True(t, ok)
				assert.Equal(t, float64(apperrors.ErrCodeUnauthorized), errorData["code"])
			},
		},
		{
			name:           "Forbidden Error",
			endpoint:       "/test/forbidden",
			expectedStatus: http.StatusForbidden,
			expectedCode:   apperrors.ErrCodeForbidden,
			validateError: func(t *testing.T, body []byte) {
				var resp map[string]interface{}
				err := json.Unmarshal(body, &resp)
				require.NoError(t, err)

				errorData, ok := resp["error"].(map[string]interface{})
				require.True(t, ok)
				assert.Equal(t, float64(apperrors.ErrCodeForbidden), errorData["code"])
			},
		},
		{
			name:           "Internal Server Error",
			endpoint:       "/test/internal-error",
			expectedStatus: http.StatusInternalServerError,
			expectedCode:   apperrors.ErrCodeInternal,
			validateError: func(t *testing.T, body []byte) {
				var resp map[string]interface{}
				err := json.Unmarshal(body, &resp)
				require.NoError(t, err)

				errorData, ok := resp["error"].(map[string]interface{})
				require.True(t, ok)
				assert.Equal(t, float64(apperrors.ErrCodeInternal), errorData["code"])
			},
		},
		{
			name:           "Database Error",
			endpoint:       "/test/database-error",
			expectedStatus: http.StatusInternalServerError,
			expectedCode:   apperrors.ErrCodeDatabase,
			validateError: func(t *testing.T, body []byte) {
				var resp map[string]interface{}
				err := json.Unmarshal(body, &resp)
				require.NoError(t, err)

				errorData, ok := resp["error"].(map[string]interface{})
				require.True(t, ok)
				assert.Equal(t, float64(apperrors.ErrCodeDatabase), errorData["code"])
			},
		},
		{
			name:           "Conflict Error",
			endpoint:       "/test/conflict",
			expectedStatus: http.StatusConflict,
			expectedCode:   apperrors.ErrCodeConflict,
			validateError: func(t *testing.T, body []byte) {
				var resp map[string]interface{}
				err := json.Unmarshal(body, &resp)
				require.NoError(t, err)

				errorData, ok := resp["error"].(map[string]interface{})
				require.True(t, ok)
				assert.Equal(t, float64(apperrors.ErrCodeConflict), errorData["code"])
			},
		},
		{
			name:           "Rate Limit Error",
			endpoint:       "/test/rate-limit",
			expectedStatus: http.StatusTooManyRequests,
			expectedCode:   apperrors.ErrCodeRateLimitExceeded,
			validateError: func(t *testing.T, body []byte) {
				var resp map[string]interface{}
				err := json.Unmarshal(body, &resp)
				require.NoError(t, err)

				errorData, ok := resp["error"].(map[string]interface{})
				require.True(t, ok)
				assert.Equal(t, float64(apperrors.ErrCodeRateLimitExceeded), errorData["code"])
			},
		},
		{
			name:           "Quota Exceeded Error",
			endpoint:       "/test/quota-exceeded",
			expectedStatus: http.StatusForbidden,
			expectedCode:   apperrors.ErrCodeQuotaExceeded,
			validateError: func(t *testing.T, body []byte) {
				var resp map[string]interface{}
				err := json.Unmarshal(body, &resp)
				require.NoError(t, err)

				errorData, ok := resp["error"].(map[string]interface{})
				require.True(t, ok)
				assert.Equal(t, float64(apperrors.ErrCodeQuotaExceeded), errorData["code"])
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, tt.endpoint, nil)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			assert.Equal(t, tt.expectedStatus, w.Code,
				fmt.Sprintf("Expected status %d, got %d", tt.expectedStatus, w.Code))

			if tt.validateError != nil {
				tt.validateError(t, w.Body.Bytes())
			}
		})
	}
}

// TestErrorResponse_Details teste que les détails d'erreur sont présents quand nécessaire
func TestErrorResponse_Details(t *testing.T) {
	router, _, cleanup := setupErrorHandlingTestRouter(t)
	defer cleanup()

	// Test validation error with details
	req := httptest.NewRequest(http.MethodGet, "/test/validation-error", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusBadRequest, w.Code)

	var resp map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &resp)
	require.NoError(t, err)

	errorData, ok := resp["error"].(map[string]interface{})
	require.True(t, ok)

	details, ok := errorData["details"].([]interface{})
	require.True(t, ok, "Details should be present")
	assert.NotEmpty(t, details, "Details should not be empty")

	// Verify detail structure
	detail, ok := details[0].(map[string]interface{})
	require.True(t, ok)
	assert.Contains(t, detail, "field")
	assert.Contains(t, detail, "message")
}

// TestErrorRecovery teste la récupération après erreur
func TestErrorRecovery(t *testing.T) {
	router, _, cleanup := setupErrorHandlingTestRouter(t)
	defer cleanup()

	// Test error case
	req := httptest.NewRequest(http.MethodGet, "/test/recovery?error=true", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusInternalServerError, w.Code)

	var errorResp map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &errorResp)
	require.NoError(t, err)
	assert.Contains(t, errorResp, "error")

	// Test recovery (success case)
	req2 := httptest.NewRequest(http.MethodGet, "/test/recovery", nil)
	w2 := httptest.NewRecorder()
	router.ServeHTTP(w2, req2)

	assert.Equal(t, http.StatusOK, w2.Code)

	var successResp map[string]interface{}
	err = json.Unmarshal(w2.Body.Bytes(), &successResp)
	require.NoError(t, err)
	assert.True(t, successResp["success"].(bool))
	assert.Contains(t, successResp, "data")
}

// TestErrorRecovery_ValidationDetails teste la récupération avec détails de validation
func TestErrorRecovery_ValidationDetails(t *testing.T) {
	router, _, cleanup := setupErrorHandlingTestRouter(t)
	defer cleanup()

	// Test with invalid input (should return validation error)
	invalidPayload := map[string]interface{}{}
	body, _ := json.Marshal(invalidPayload)

	req := httptest.NewRequest(http.MethodPost, "/test/validation-details", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusBadRequest, w.Code)

	var errorResp map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &errorResp)
	require.NoError(t, err)

	errorData, ok := errorResp["error"].(map[string]interface{})
	require.True(t, ok)

	details, ok := errorData["details"].([]interface{})
	require.True(t, ok)
	assert.Len(t, details, 2, "Should have 2 validation errors")

	// Test with valid input (should succeed)
	validPayload := map[string]interface{}{
		"email":    "test@example.com",
		"password": "password123",
	}
	body2, _ := json.Marshal(validPayload)

	req2 := httptest.NewRequest(http.MethodPost, "/test/validation-details", bytes.NewBuffer(body2))
	req2.Header.Set("Content-Type", "application/json")
	w2 := httptest.NewRecorder()
	router.ServeHTTP(w2, req2)

	assert.Equal(t, http.StatusOK, w2.Code)

	var successResp map[string]interface{}
	err = json.Unmarshal(w2.Body.Bytes(), &successResp)
	require.NoError(t, err)
	assert.True(t, successResp["success"].(bool))
}

// TestErrorResponse_Timestamp teste que le timestamp est présent dans les erreurs
func TestErrorResponse_Timestamp(t *testing.T) {
	router, _, cleanup := setupErrorHandlingTestRouter(t)
	defer cleanup()

	req := httptest.NewRequest(http.MethodGet, "/test/validation-error", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	var resp map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &resp)
	require.NoError(t, err)

	errorData, ok := resp["error"].(map[string]interface{})
	require.True(t, ok)

	// Note: The error handler might not include timestamp in the current format
	// This test verifies the error structure is valid
	assert.Contains(t, errorData, "code")
	assert.Contains(t, errorData, "message")
}

// TestErrorResponse_Context teste que le contexte est présent dans les erreurs
func TestErrorResponse_Context(t *testing.T) {
	router, _, cleanup := setupErrorHandlingTestRouter(t)
	defer cleanup()

	req := httptest.NewRequest(http.MethodGet, "/test/validation-error", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	var resp map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &resp)
	require.NoError(t, err)

	errorData, ok := resp["error"].(map[string]interface{})
	require.True(t, ok)

	// Context might be present if set by middleware
	// This test verifies the error structure is valid
	assert.Contains(t, errorData, "code")
	assert.Contains(t, errorData, "message")
}

// TestErrorResponse_Consistency teste que toutes les erreurs suivent le même format
func TestErrorResponse_Consistency(t *testing.T) {
	router, _, cleanup := setupErrorHandlingTestRouter(t)
	defer cleanup()

	endpoints := []string{
		"/test/validation-error",
		"/test/not-found",
		"/test/unauthorized",
		"/test/forbidden",
		"/test/internal-error",
	}

	for _, endpoint := range endpoints {
		t.Run(fmt.Sprintf("Consistency check: %s", endpoint), func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, endpoint, nil)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			var resp map[string]interface{}
			err := json.Unmarshal(w.Body.Bytes(), &resp)
			require.NoError(t, err)

			// All errors should have the same structure
			assert.Contains(t, resp, "error")

			errorData, ok := resp["error"].(map[string]interface{})
			require.True(t, ok, "Error should be a map")

			// Required fields
			assert.Contains(t, errorData, "code")
			assert.Contains(t, errorData, "message")

			// Code should be a number
			code, ok := errorData["code"].(float64)
			assert.True(t, ok, "Code should be a number")
			assert.Greater(t, code, float64(0), "Code should be positive")
		})
	}
}

// TestErrorRecovery_RetryLogic teste la logique de retry après erreur
func TestErrorRecovery_RetryLogic(t *testing.T) {
	router, _, cleanup := setupErrorHandlingTestRouter(t)
	defer cleanup()

	// Simulate retry after error
	// First request: error
	req1 := httptest.NewRequest(http.MethodGet, "/test/recovery?error=true", nil)
	w1 := httptest.NewRecorder()
	router.ServeHTTP(w1, req1)
	assert.Equal(t, http.StatusInternalServerError, w1.Code)

	// Second request: success (recovery)
	req2 := httptest.NewRequest(http.MethodGet, "/test/recovery", nil)
	w2 := httptest.NewRecorder()
	router.ServeHTTP(w2, req2)
	assert.Equal(t, http.StatusOK, w2.Code)

	var successResp map[string]interface{}
	err := json.Unmarshal(w2.Body.Bytes(), &successResp)
	require.NoError(t, err)
	assert.True(t, successResp["success"].(bool))
}

// TestErrorHandling_AppErrorWrapper teste que les AppError sont correctement wrappées
func TestErrorHandling_AppErrorWrapper(t *testing.T) {
	router, _, cleanup := setupErrorHandlingTestRouter(t)
	defer cleanup()

	req := httptest.NewRequest(http.MethodGet, "/test/validation-error", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	var resp map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &resp)
	require.NoError(t, err)

	errorData, ok := resp["error"].(map[string]interface{})
	require.True(t, ok)

	// Verify AppError structure
	assert.Equal(t, float64(apperrors.ErrCodeValidation), errorData["code"])
	assert.NotEmpty(t, errorData["message"])
}

// TestErrorHandling_HTTPStatusMapping teste que les codes d'erreur sont correctement mappés vers les codes HTTP
func TestErrorHandling_HTTPStatusMapping(t *testing.T) {
	router, _, cleanup := setupErrorHandlingTestRouter(t)
	defer cleanup()

	tests := []struct {
		endpoint       string
		expectedStatus int
		expectedCode   apperrors.ErrorCode
	}{
		{"/test/validation-error", http.StatusBadRequest, apperrors.ErrCodeValidation},
		{"/test/not-found", http.StatusNotFound, apperrors.ErrCodeNotFound},
		{"/test/unauthorized", http.StatusUnauthorized, apperrors.ErrCodeUnauthorized},
		{"/test/forbidden", http.StatusForbidden, apperrors.ErrCodeForbidden},
		{"/test/internal-error", http.StatusInternalServerError, apperrors.ErrCodeInternal},
		{"/test/database-error", http.StatusInternalServerError, apperrors.ErrCodeDatabase},
		{"/test/conflict", http.StatusConflict, apperrors.ErrCodeConflict},
		{"/test/rate-limit", http.StatusTooManyRequests, apperrors.ErrCodeRateLimitExceeded},
		{"/test/quota-exceeded", http.StatusForbidden, apperrors.ErrCodeQuotaExceeded},
	}

	for _, tt := range tests {
		t.Run(tt.endpoint, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, tt.endpoint, nil)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			assert.Equal(t, tt.expectedStatus, w.Code,
				fmt.Sprintf("Expected HTTP status %d for error code %d", tt.expectedStatus, tt.expectedCode))

			var resp map[string]interface{}
			err := json.Unmarshal(w.Body.Bytes(), &resp)
			require.NoError(t, err)

			errorData, ok := resp["error"].(map[string]interface{})
			require.True(t, ok)
			assert.Equal(t, float64(tt.expectedCode), errorData["code"])
		})
	}
}
