package middleware

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

	"veza-backend-api/internal/errors"
	"veza-backend-api/internal/metrics"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gorm.io/gorm"
)

func TestStructuredErrorLogging_AppError_AllFields(t *testing.T) {
	gin.SetMode(gin.TestMode)

	// Créer un logger avec un buffer pour capturer les logs
	buffer := &strings.Builder{}
	writer := zapcore.AddSync(buffer)
	encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
	core := zapcore.NewCore(encoder, writer, zap.DebugLevel)
	logger := zap.New(core)

	errorMetrics := metrics.NewErrorMetrics()
	router := gin.New()
	router.Use(RequestID())
	router.Use(Tracing())
	router.Use(ErrorHandler(logger, errorMetrics, true))
	router.GET("/test", func(c *gin.Context) {
		c.Set("user_id", int64(123))
		appErr := errors.New(errors.ErrCodeValidation, "Test validation error")
		appErr.Details = []errors.ErrorDetail{
			{Field: "email", Message: "Invalid email format"},
		}
		c.Error(appErr)
	})

	w := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/test", nil)
	router.ServeHTTP(w, req)

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

	// Vérifier la réponse JSON
	var response map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &response)
	require.NoError(t, err)

	// Vérifier les logs structurés
	logOutput := buffer.String()
	require.NotEmpty(t, logOutput)

	// Parser les logs JSON
	var logEntry map[string]interface{}
	lines := strings.Split(strings.TrimSpace(logOutput), "\n")
	found := false
	for _, line := range lines {
		if strings.Contains(line, "Application error") {
			err := json.Unmarshal([]byte(line), &logEntry)
			require.NoError(t, err)
			found = true
			break
		}
	}
	require.True(t, found, "Log entry not found")

	// Vérifier tous les champs requis dans les logs structurés
	assert.Equal(t, "Application error", logEntry["msg"])
	assert.Equal(t, "error", logEntry["level"])
	assert.Contains(t, logEntry, "code")
	assert.Contains(t, logEntry, "message")
	assert.Contains(t, logEntry, "http_status")
	assert.Contains(t, logEntry, "request_id")
	assert.Contains(t, logEntry, "trace_id")
	assert.Contains(t, logEntry, "span_id")
	assert.Contains(t, logEntry, "user_id")
	assert.Contains(t, logEntry, "details")
}

func TestStructuredErrorLogging_InternalError_StackTrace(t *testing.T) {
	gin.SetMode(gin.TestMode)

	// Créer un logger avec un buffer pour capturer les logs
	buffer := &strings.Builder{}
	writer := zapcore.AddSync(buffer)
	encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
	core := zapcore.NewCore(encoder, writer, zap.DebugLevel)
	logger := zap.New(core)

	errorMetrics := metrics.NewErrorMetrics()
	router := gin.New()
	router.Use(RequestID())
	router.Use(Tracing())
	router.Use(ErrorHandler(logger, errorMetrics, true))
	router.GET("/test", func(c *gin.Context) {
		c.Set("user_id", int64(456))
		// Utiliser une erreur générique (non-AppError) pour déclencher le chemin "erreur générique"
		c.Error(fmt.Errorf("generic error: something went wrong"))
	})

	w := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/test", nil)
	router.ServeHTTP(w, req)

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

	// Vérifier les logs structurés
	logOutput := buffer.String()
	require.NotEmpty(t, logOutput)

	// Parser les logs JSON
	var logEntry map[string]interface{}
	lines := strings.Split(strings.TrimSpace(logOutput), "\n")
	found := false
	for _, line := range lines {
		if strings.Contains(line, "Internal server error") && strings.Contains(line, "stack_trace") {
			err := json.Unmarshal([]byte(line), &logEntry)
			require.NoError(t, err)
			found = true
			break
		}
	}
	require.True(t, found, "Log entry with stack_trace not found. Log output: %s", logOutput)

	// Vérifier tous les champs requis dans les logs structurés
	assert.Equal(t, "Internal server error", logEntry["msg"])
	assert.Equal(t, "error", logEntry["level"])
	assert.Contains(t, logEntry, "code")
	assert.Contains(t, logEntry, "message")
	assert.Contains(t, logEntry, "http_status")
	assert.Contains(t, logEntry, "request_id")
	assert.Contains(t, logEntry, "trace_id")
	assert.Contains(t, logEntry, "span_id")
	assert.Contains(t, logEntry, "user_id")
	assert.Contains(t, logEntry, "stack_trace")

	// Vérifier que stack_trace contient des données
	stackTrace, ok := logEntry["stack_trace"].(string)
	require.True(t, ok, "stack_trace should be a string")
	assert.NotEmpty(t, stackTrace)
	assert.Contains(t, stackTrace, "runtime")
}

func TestStructuredErrorLogging_AppError_MinimalContext(t *testing.T) {
	gin.SetMode(gin.TestMode)

	// Créer un logger avec un buffer pour capturer les logs
	buffer := &strings.Builder{}
	writer := zapcore.AddSync(buffer)
	encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
	core := zapcore.NewCore(encoder, writer, zap.DebugLevel)
	logger := zap.New(core)

	errorMetrics := metrics.NewErrorMetrics()
	router := gin.New()
	router.Use(RequestID())
	router.Use(Tracing())
	router.Use(ErrorHandler(logger, errorMetrics, true))
	router.GET("/test", func(c *gin.Context) {
		// Pas de user_id - test avec contexte minimal
		appErr := errors.New(errors.ErrCodeNotFound, "Resource not found")
		c.Error(appErr)
	})

	w := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/test", nil)
	router.ServeHTTP(w, req)

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

	// Vérifier les logs structurés
	logOutput := buffer.String()
	require.NotEmpty(t, logOutput)

	// Parser les logs JSON
	var logEntry map[string]interface{}
	lines := strings.Split(strings.TrimSpace(logOutput), "\n")
	found := false
	for _, line := range lines {
		if strings.Contains(line, "Application error") {
			err := json.Unmarshal([]byte(line), &logEntry)
			require.NoError(t, err)
			found = true
			break
		}
	}
	require.True(t, found, "Log entry not found")

	// Vérifier les champs de base (sans user_id)
	assert.Equal(t, "Application error", logEntry["msg"])
	assert.Contains(t, logEntry, "request_id")
	assert.Contains(t, logEntry, "trace_id")
	assert.Contains(t, logEntry, "span_id")
	// user_id ne devrait pas être présent
	assert.NotContains(t, logEntry, "user_id")
}

func TestStructuredErrorLogging_GORMError_WithContext(t *testing.T) {
	gin.SetMode(gin.TestMode)

	// Créer un logger avec un buffer pour capturer les logs
	buffer := &strings.Builder{}
	writer := zapcore.AddSync(buffer)
	encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
	core := zapcore.NewCore(encoder, writer, zap.DebugLevel)
	logger := zap.New(core)

	errorMetrics := metrics.NewErrorMetrics()
	router := gin.New()
	router.Use(RequestID())
	router.Use(Tracing())
	router.Use(ErrorHandler(logger, errorMetrics, true))
	router.GET("/test", func(c *gin.Context) {
		c.Set("user_id", "user-789")
		c.Error(gorm.ErrRecordNotFound)
	})

	w := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/test", nil)
	router.ServeHTTP(w, req)

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

	// Vérifier les logs structurés
	logOutput := buffer.String()
	require.NotEmpty(t, logOutput)

	// Parser les logs JSON
	var logEntry map[string]interface{}
	lines := strings.Split(strings.TrimSpace(logOutput), "\n")
	found := false
	for _, line := range lines {
		if strings.Contains(line, "Record not found") {
			err := json.Unmarshal([]byte(line), &logEntry)
			require.NoError(t, err)
			found = true
			break
		}
	}
	require.True(t, found, "Log entry not found")

	// Vérifier les champs dans les logs structurés
	assert.Equal(t, "Record not found", logEntry["msg"])
	assert.Equal(t, "warn", logEntry["level"])
	assert.Contains(t, logEntry, "code")
	assert.Contains(t, logEntry, "message")
	assert.Contains(t, logEntry, "http_status")
	assert.Contains(t, logEntry, "request_id")
	assert.Contains(t, logEntry, "trace_id")
	assert.Contains(t, logEntry, "span_id")
}

func TestStructuredErrorLogging_JSONFormat(t *testing.T) {
	gin.SetMode(gin.TestMode)

	// Créer un logger avec un buffer pour capturer les logs
	buffer := &strings.Builder{}
	writer := zapcore.AddSync(buffer)
	encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
	core := zapcore.NewCore(encoder, writer, zap.DebugLevel)
	logger := zap.New(core)

	errorMetrics := metrics.NewErrorMetrics()
	router := gin.New()
	router.Use(RequestID())
	router.Use(ErrorHandler(logger, errorMetrics, true))
	router.GET("/test", func(c *gin.Context) {
		appErr := errors.New(errors.ErrCodeValidation, "Validation failed")
		c.Error(appErr)
	})

	w := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/test", nil)
	router.ServeHTTP(w, req)

	// Vérifier que les logs sont au format JSON
	logOutput := buffer.String()
	require.NotEmpty(t, logOutput)

	lines := strings.Split(strings.TrimSpace(logOutput), "\n")
	for _, line := range lines {
		if strings.Contains(line, "Application error") {
			var logEntry map[string]interface{}
			err := json.Unmarshal([]byte(line), &logEntry)
			assert.NoError(t, err, "Log should be valid JSON")
			assert.NotEmpty(t, logEntry, "Log entry should not be empty")
		}
	}
}

func TestStructuredErrorLogging_NoSensitiveData(t *testing.T) {
	gin.SetMode(gin.TestMode)

	// Créer un logger avec un buffer pour capturer les logs
	buffer := &strings.Builder{}
	writer := zapcore.AddSync(buffer)
	encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
	core := zapcore.NewCore(encoder, writer, zap.DebugLevel)
	logger := zap.New(core)

	errorMetrics := metrics.NewErrorMetrics()
	router := gin.New()
	router.Use(RequestID())
	router.Use(ErrorHandler(logger, errorMetrics, true))
	router.GET("/test", func(c *gin.Context) {
		// Simuler une erreur qui pourrait contenir des données sensibles
		appErr := errors.New(errors.ErrCodeUnauthorized, "Authentication failed")
		c.Error(appErr)
	})

	w := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/test", nil)
	router.ServeHTTP(w, req)

	// Vérifier que les logs ne contiennent pas de données sensibles
	logOutput := buffer.String()

	// Vérifier qu'il n'y a pas de mots-clés sensibles dans les logs
	sensitiveKeywords := []string{"password", "token", "secret", "key", "credential"}
	for _, keyword := range sensitiveKeywords {
		assert.NotContains(t, strings.ToLower(logOutput), keyword, "Logs should not contain sensitive data: %s", keyword)
	}
}

// Test helper: vérifier que le format JSON est valide
func TestStructuredErrorLogging_ValidJSON(t *testing.T) {
	gin.SetMode(gin.TestMode)

	buffer := &strings.Builder{}
	writer := zapcore.AddSync(buffer)
	encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
	core := zapcore.NewCore(encoder, writer, zap.DebugLevel)
	logger := zap.New(core)

	errorMetrics := metrics.NewErrorMetrics()
	router := gin.New()
	router.Use(RequestID())
	router.Use(Tracing())
	router.Use(ErrorHandler(logger, errorMetrics, true))
	router.GET("/test", func(c *gin.Context) {
		c.Set("user_id", int64(999))
		appErr := errors.New(errors.ErrCodeInternal, "Internal error")
		appErr.Err = errors.New(errors.ErrCodeValidation, "wrapped error")
		c.Error(appErr)
	})

	w := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/test", nil)
	router.ServeHTTP(w, req)

	logOutput := buffer.String()
	lines := strings.Split(strings.TrimSpace(logOutput), "\n")

	for _, line := range lines {
		if strings.Contains(line, "Application error") || strings.Contains(line, "Internal server error") {
			var logEntry map[string]interface{}
			err := json.Unmarshal([]byte(line), &logEntry)
			assert.NoError(t, err, "Each log line should be valid JSON")

			// Vérifier la structure des champs
			if code, ok := logEntry["code"].(float64); ok {
				assert.Greater(t, code, float64(0), "Error code should be positive")
			}

			if httpStatus, ok := logEntry["http_status"].(float64); ok {
				assert.GreaterOrEqual(t, httpStatus, float64(400), "HTTP status should be 4xx or 5xx")
				assert.LessOrEqual(t, httpStatus, float64(599), "HTTP status should be 4xx or 5xx")
			}
		}
	}
}
