package handlers_test

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"

	"github.com/Wikid82/charon/backend/internal/api/handlers"
	"github.com/Wikid82/charon/backend/internal/models"
)

type mockCaddyConfigManager struct {
	applyFunc func(context.Context) error
	calls     int
}

type mockCacheInvalidator struct {
	calls int
}

func (m *mockCacheInvalidator) InvalidateCache() {
	m.calls++
}

func (m *mockCaddyConfigManager) ApplyConfig(ctx context.Context) error {
	m.calls++
	if m.applyFunc != nil {
		return m.applyFunc(ctx)
	}
	return nil
}

func startTestSMTPServer(t *testing.T) (host string, port int) {
	t.Helper()

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("failed to listen for smtp test server: %v", err)
	}

	var wg sync.WaitGroup
	acceptDone := make(chan struct{})
	go func() {
		defer close(acceptDone)
		for {
			conn, acceptErr := ln.Accept()
			if acceptErr != nil {
				return
			}
			wg.Add(1)
			go func(c net.Conn) {
				defer wg.Done()
				defer func() { _ = c.Close() }()
				handleSMTPConnection(c)
			}(conn)
		}
	}()

	t.Cleanup(func() {
		_ = ln.Close()
		<-acceptDone
		wg.Wait()
	})

	host, portStr, err := net.SplitHostPort(ln.Addr().String())
	if err != nil {
		t.Fatalf("failed to split smtp listener addr: %v", err)
	}
	if _, err := fmt.Sscanf(portStr, "%d", &port); err != nil {
		t.Fatalf("failed to parse smtp listener port: %v", err)
	}

	return host, port
}

func handleSMTPConnection(conn net.Conn) {
	r := bufio.NewReader(conn)
	w := bufio.NewWriter(conn)

	writeLine := func(line string) {
		_, _ = w.WriteString(line + "\r\n")
		_ = w.Flush()
	}

	writeLine("220 localhost ESMTP test")

	for {
		line, err := r.ReadString('\n')
		if err != nil {
			return
		}
		cmd := strings.TrimSpace(line)
		upper := strings.ToUpper(cmd)

		switch {
		case strings.HasPrefix(upper, "EHLO") || strings.HasPrefix(upper, "HELO"):
			writeLine("250-localhost")
			writeLine("250 OK")
		case strings.HasPrefix(upper, "MAIL FROM:"):
			writeLine("250 OK")
		case strings.HasPrefix(upper, "RCPT TO:"):
			writeLine("250 OK")
		case strings.HasPrefix(upper, "DATA"):
			writeLine("354 End data with <CR><LF>.<CR><LF>")
			for {
				dataLine, err := r.ReadString('\n')
				if err != nil {
					return
				}
				if strings.TrimRight(dataLine, "\r\n") == "." {
					break
				}
			}
			writeLine("250 OK")
		case strings.HasPrefix(upper, "RSET"):
			writeLine("250 OK")
		case strings.HasPrefix(upper, "NOOP"):
			writeLine("250 OK")
		case strings.HasPrefix(upper, "QUIT"):
			writeLine("221 Bye")
			return
		default:
			writeLine("250 OK")
		}
	}
}

func setupSettingsTestDB(t *testing.T) *gorm.DB {
	dsn := "file:" + t.Name() + "?mode=memory&cache=shared"
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect to test database")
	}
	_ = db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})
	return db
}

func newAdminRouter() *gin.Engine {
	router := gin.New()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Set("userID", uint(1))
		c.Next()
	})
	return router
}

func TestSettingsHandler_GetSettings(t *testing.T) {
	db := setupSettingsTestDB(t)

	// Seed data
	db.Create(&models.Setting{Key: "test_key", Value: "test_value", Category: "general", Type: "string"})

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.GET("/settings", handler.GetSettings)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/settings", http.NoBody)
	router.ServeHTTP(w, req)

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

	var response map[string]string
	err := json.Unmarshal(w.Body.Bytes(), &response)
	assert.NoError(t, err)
	assert.Equal(t, "test_value", response["test_key"])
}

func TestSettingsHandler_GetSettings_MasksSensitiveValues(t *testing.T) {
	db := setupSettingsTestDB(t)

	db.Create(&models.Setting{Key: "smtp_password", Value: "super-secret-password", Category: "smtp", Type: "string"})

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.GET("/settings", handler.GetSettings)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/settings", http.NoBody)
	router.ServeHTTP(w, req)

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

	var response map[string]any
	err := json.Unmarshal(w.Body.Bytes(), &response)
	assert.NoError(t, err)
	assert.Equal(t, "********", response["smtp_password"])
	assert.Equal(t, true, response["smtp_password.has_secret"])
	_, hasRaw := response["super-secret-password"]
	assert.False(t, hasRaw)
}

func TestSettingsHandler_GetSettings_DatabaseError(t *testing.T) {
	db := setupSettingsTestDB(t)

	// Close the database to force an error
	sqlDB, _ := db.DB()
	_ = sqlDB.Close()

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.GET("/settings", handler.GetSettings)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/settings", http.NoBody)
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusInternalServerError, w.Code)
	var response map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &response)
	assert.NoError(t, err)
	assert.Contains(t, response["error"], "Failed to fetch settings")
}

func TestSettingsHandler_UpdateSettings(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	// Test Create
	payload := map[string]string{
		"key":      "new_key",
		"value":    "new_value",
		"category": "system",
		"type":     "string",
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

	var setting models.Setting
	db.Where("key = ?", "new_key").First(&setting)
	assert.Equal(t, "new_value", setting.Value)

	// Test Update
	payload["value"] = "updated_value"
	body, _ = json.Marshal(payload)

	w = httptest.NewRecorder()
	req, _ = http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

	db.Where("key = ?", "new_key").First(&setting)
	assert.Equal(t, "updated_value", setting.Value)
}

func TestSettingsHandler_UpdateSetting_SyncsAdminWhitelist(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	payload := map[string]string{
		"key":   "security.admin_whitelist",
		"value": "192.0.2.1/32",
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

	var cfg models.SecurityConfig
	err := db.Where("name = ?", "default").First(&cfg).Error
	assert.NoError(t, err)
	assert.Equal(t, "192.0.2.1/32", cfg.AdminWhitelist)
}

func TestSettingsHandler_UpdateSetting_EnablesCerberusWhenACLEnabled(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	payload := map[string]string{
		"key":   "security.acl.enabled",
		"value": "true",
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

	var setting models.Setting
	err := db.Where("key = ?", "feature.cerberus.enabled").First(&setting).Error
	assert.NoError(t, err)
	assert.Equal(t, "true", setting.Value)

	var legacySetting models.Setting
	err = db.Where("key = ?", "security.cerberus.enabled").First(&legacySetting).Error
	assert.NoError(t, err)
	assert.Equal(t, "true", legacySetting.Value)

	var aclSetting models.Setting
	err = db.Where("key = ?", "security.acl.enabled").First(&aclSetting).Error
	assert.NoError(t, err)
	assert.Equal(t, "true", aclSetting.Value)

	var cfg models.SecurityConfig
	err = db.Where("name = ?", "default").First(&cfg).Error
	assert.NoError(t, err)
	assert.True(t, cfg.Enabled)
}

func TestSettingsHandler_UpdateSetting_SecurityKeyAppliesConfigSynchronously(t *testing.T) {
	db := setupSettingsTestDB(t)

	mgr := &mockCaddyConfigManager{}
	handler := handlers.NewSettingsHandlerWithDeps(db, mgr, nil, nil, "")
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	payload := map[string]string{
		"key":   "security.waf.enabled",
		"value": "true",
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_UpdateSetting_SecurityKeyApplyFailureReturnsError(t *testing.T) {
	db := setupSettingsTestDB(t)

	mgr := &mockCaddyConfigManager{applyFunc: func(context.Context) error {
		return fmt.Errorf("apply failed")
	}}
	handler := handlers.NewSettingsHandlerWithDeps(db, mgr, nil, nil, "")
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	payload := map[string]string{
		"key":   "security.waf.enabled",
		"value": "true",
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusInternalServerError, w.Code)
	assert.Equal(t, 1, mgr.calls)
}

func TestSettingsHandler_UpdateSetting_NonAdminForbidden(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := gin.New()
	router.Use(func(c *gin.Context) {
		c.Set("role", "user")
		c.Next()
	})
	router.POST("/settings", handler.UpdateSetting)

	payload := map[string]string{"key": "security.waf.enabled", "value": "true"}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_UpdateSetting_InvalidAdminWhitelist(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	payload := map[string]string{
		"key":   "security.admin_whitelist",
		"value": "invalid-cidr-without-prefix",
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_UpdateSetting_EmptyValueAccepted(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	payload := map[string]string{
		"key":   "some.setting",
		"value": "",
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

	var setting models.Setting
	require.NoError(t, db.Where("key = ?", "some.setting").First(&setting).Error)
	assert.Equal(t, "some.setting", setting.Key)
	assert.Equal(t, "", setting.Value)
}

func TestSettingsHandler_UpdateSetting_MissingKeyRejected(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	payload := map[string]string{
		"value": "some-value",
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_UpdateSetting_InvalidKeepaliveIdle(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	payload := map[string]string{
		"key":   "caddy.keepalive_idle",
		"value": "bad-duration",
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_UpdateSetting_ValidKeepaliveCount(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	payload := map[string]string{
		"key":      "caddy.keepalive_count",
		"value":    "9",
		"category": "caddy",
		"type":     "number",
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

	var setting models.Setting
	err := db.Where("key = ?", "caddy.keepalive_count").First(&setting).Error
	assert.NoError(t, err)
	assert.Equal(t, "9", setting.Value)
}

func TestSettingsHandler_UpdateSetting_SecurityKeyInvalidatesCache(t *testing.T) {
	db := setupSettingsTestDB(t)

	mgr := &mockCaddyConfigManager{}
	inv := &mockCacheInvalidator{}
	handler := handlers.NewSettingsHandlerWithDeps(db, mgr, inv, nil, "")
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	payload := map[string]string{
		"key":   "security.rate_limit.enabled",
		"value": "true",
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	assert.Equal(t, 1, inv.calls)
	assert.Equal(t, 1, mgr.calls)
}

func TestSettingsHandler_PatchConfig_InvalidAdminWhitelist(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.PATCH("/config", handler.PatchConfig)

	payload := map[string]any{
		"security": map[string]any{
			"admin_whitelist": "bad-cidr",
		},
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest(http.MethodPatch, "/config", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_PatchConfig_InvalidKeepaliveCount(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.PATCH("/config", handler.PatchConfig)

	payload := map[string]any{
		"caddy": map[string]any{
			"keepalive_count": 0,
		},
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest(http.MethodPatch, "/config", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_PatchConfig_ValidKeepaliveSettings(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.PATCH("/config", handler.PatchConfig)

	payload := map[string]any{
		"caddy": map[string]any{
			"keepalive_idle":  "30s",
			"keepalive_count": 12,
		},
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest(http.MethodPatch, "/config", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

	var idle models.Setting
	err := db.Where("key = ?", "caddy.keepalive_idle").First(&idle).Error
	assert.NoError(t, err)
	assert.Equal(t, "30s", idle.Value)

	var count models.Setting
	err = db.Where("key = ?", "caddy.keepalive_count").First(&count).Error
	assert.NoError(t, err)
	assert.Equal(t, "12", count.Value)
}

func TestSettingsHandler_PatchConfig_ReloadFailureReturns500(t *testing.T) {
	db := setupSettingsTestDB(t)

	mgr := &mockCaddyConfigManager{applyFunc: func(context.Context) error {
		return fmt.Errorf("reload failed")
	}}
	inv := &mockCacheInvalidator{}
	handler := handlers.NewSettingsHandlerWithDeps(db, mgr, inv, nil, "")
	router := newAdminRouter()
	router.PATCH("/config", handler.PatchConfig)

	payload := map[string]any{
		"security": map[string]any{
			"waf": map[string]any{"enabled": true},
		},
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest(http.MethodPatch, "/config", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusInternalServerError, w.Code)
	assert.Equal(t, 1, inv.calls)
	assert.Equal(t, 1, mgr.calls)
	assert.Contains(t, w.Body.String(), "Failed to reload configuration")
}

func TestSettingsHandler_PatchConfig_SyncsAdminWhitelist(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.PATCH("/config", handler.PatchConfig)

	payload := map[string]any{
		"security": map[string]any{
			"admin_whitelist": "203.0.113.0/24",
		},
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("PATCH", "/config", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

	var cfg models.SecurityConfig
	err := db.Where("name = ?", "default").First(&cfg).Error
	assert.NoError(t, err)
	assert.Equal(t, "203.0.113.0/24", cfg.AdminWhitelist)
}

func TestSettingsHandler_PatchConfig_EnablesCerberusWhenACLEnabled(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.PATCH("/config", handler.PatchConfig)

	payload := map[string]any{
		"security": map[string]any{
			"acl": map[string]any{
				"enabled": true,
			},
		},
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("PATCH", "/config", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

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

	var setting models.Setting
	err := db.Where("key = ?", "feature.cerberus.enabled").First(&setting).Error
	assert.NoError(t, err)
	assert.Equal(t, "true", setting.Value)

	var cfg models.SecurityConfig
	err = db.Where("name = ?", "default").First(&cfg).Error
	assert.NoError(t, err)
	assert.True(t, cfg.Enabled)
}

func TestSettingsHandler_UpdateSetting_DatabaseError(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	// Close the database to force an error
	sqlDB, _ := db.DB()
	_ = sqlDB.Close()

	payload := map[string]string{
		"key":   "test_key",
		"value": "test_value",
	}
	body, _ := json.Marshal(payload)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusInternalServerError, w.Code)
	var response map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &response)
	assert.NoError(t, err)
	assert.Contains(t, response["error"], "Failed to save setting")
}

func TestSettingsHandler_Errors(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	// Invalid JSON
	req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer([]byte("invalid")))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusBadRequest, w.Code)

	// Value omitted — allowed since binding:"required" was removed; empty string is a valid value
	payload := map[string]string{
		"key": "some_key",
		// value intentionally absent; defaults to empty string
	}
	body, _ := json.Marshal(payload)
	req, _ = http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusOK, w.Code)

	// Missing key — key is still binding:"required" so this must return 400
	payloadNoKey := map[string]string{
		"value": "some_value",
	}
	bodyNoKey, _ := json.Marshal(payloadNoKey)
	req, _ = http.NewRequest("POST", "/settings", bytes.NewBuffer(bodyNoKey))
	req.Header.Set("Content-Type", "application/json")
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusBadRequest, w.Code)
}

// ============= SMTP Settings Tests =============

func setupSettingsHandlerWithMail(t *testing.T) (*handlers.SettingsHandler, *gorm.DB) {
	dsn := "file:" + t.Name() + "?mode=memory&cache=shared"
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect to test database")
	}
	_ = db.AutoMigrate(&models.Setting{})
	return handlers.NewSettingsHandler(db), db
}

func TestSettingsHandler_GetSMTPConfig(t *testing.T) {
	handler, db := setupSettingsHandlerWithMail(t)

	// Seed SMTP config
	db.Create(&models.Setting{Key: "smtp_host", Value: "smtp.example.com", Category: "smtp", Type: "string"})
	db.Create(&models.Setting{Key: "smtp_port", Value: "587", Category: "smtp", Type: "number"})
	db.Create(&models.Setting{Key: "smtp_username", Value: "user@example.com", Category: "smtp", Type: "string"})
	db.Create(&models.Setting{Key: "smtp_password", Value: "secret123", Category: "smtp", Type: "string"})
	db.Create(&models.Setting{Key: "smtp_from_address", Value: "noreply@example.com", Category: "smtp", Type: "string"})
	db.Create(&models.Setting{Key: "smtp_encryption", Value: "starttls", Category: "smtp", Type: "string"})

	router := newAdminRouter()
	router.GET("/settings/smtp", handler.GetSMTPConfig)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/settings/smtp", http.NoBody)
	router.ServeHTTP(w, req)

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

	var resp map[string]any
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	assert.Equal(t, "smtp.example.com", resp["host"])
	assert.Equal(t, float64(587), resp["port"])
	assert.Equal(t, "********", resp["password"]) // Password should be masked
	assert.Equal(t, true, resp["configured"])
}

func TestSettingsHandler_GetSMTPConfig_Empty(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.GET("/settings/smtp", handler.GetSMTPConfig)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/settings/smtp", http.NoBody)
	router.ServeHTTP(w, req)

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

	var resp map[string]any
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	assert.Equal(t, false, resp["configured"])
}

func TestSettingsHandler_GetSMTPConfig_DatabaseError(t *testing.T) {
	handler, db := setupSettingsHandlerWithMail(t)
	sqlDB, _ := db.DB()
	_ = sqlDB.Close()

	router := newAdminRouter()
	router.GET("/settings/smtp", handler.GetSMTPConfig)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/settings/smtp", http.NoBody)
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_GetSMTPConfig_NonAdminForbidden(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := gin.New()
	router.Use(func(c *gin.Context) {
		c.Set("role", "user")
		c.Set("userID", uint(2))
		c.Next()
	})
	router.GET("/api/v1/settings/smtp", handler.GetSMTPConfig)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/api/v1/settings/smtp", http.NoBody)
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_UpdateSMTPConfig_NonAdmin(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "user")
		c.Next()
	})
	router.PUT("/settings/smtp", handler.UpdateSMTPConfig)

	body := map[string]any{
		"host":         "smtp.example.com",
		"port":         587,
		"from_address": "test@example.com",
		"encryption":   "starttls",
	}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("PUT", "/settings/smtp", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_UpdateSMTPConfig_InvalidJSON(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.PUT("/settings/smtp", handler.UpdateSMTPConfig)

	req, _ := http.NewRequest("PUT", "/settings/smtp", bytes.NewBufferString("invalid"))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_UpdateSMTPConfig_Success(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.PUT("/settings/smtp", handler.UpdateSMTPConfig)

	body := map[string]any{
		"host":         "smtp.example.com",
		"port":         587,
		"username":     "user@example.com",
		"password":     "password123",
		"from_address": "noreply@example.com",
		"encryption":   "starttls",
	}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("PUT", "/settings/smtp", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_UpdateSMTPConfig_KeepExistingPassword(t *testing.T) {
	handler, db := setupSettingsHandlerWithMail(t)

	// Seed existing password
	db.Create(&models.Setting{Key: "smtp_password", Value: "existingpassword", Category: "smtp", Type: "string"})
	db.Create(&models.Setting{Key: "smtp_host", Value: "old.example.com", Category: "smtp", Type: "string"})
	db.Create(&models.Setting{Key: "smtp_port", Value: "25", Category: "smtp", Type: "number"})
	db.Create(&models.Setting{Key: "smtp_from_address", Value: "old@example.com", Category: "smtp", Type: "string"})
	db.Create(&models.Setting{Key: "smtp_encryption", Value: "none", Category: "smtp", Type: "string"})

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.PUT("/settings/smtp", handler.UpdateSMTPConfig)

	// Send masked password (simulating frontend sending back masked value)
	body := map[string]any{
		"host":         "smtp.example.com",
		"port":         587,
		"password":     "********", // Masked
		"from_address": "noreply@example.com",
		"encryption":   "starttls",
	}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("PUT", "/settings/smtp", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

	// Verify password was preserved
	var setting models.Setting
	db.Where("key = ?", "smtp_password").First(&setting)
	assert.Equal(t, "existingpassword", setting.Value)
}

func TestSettingsHandler_TestSMTPConfig_NonAdmin(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "user")
		c.Next()
	})
	router.POST("/settings/smtp/test", handler.TestSMTPConfig)

	req, _ := http.NewRequest("POST", "/settings/smtp/test", http.NoBody)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_TestSMTPConfig_NotConfigured(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/smtp/test", handler.TestSMTPConfig)

	req, _ := http.NewRequest("POST", "/settings/smtp/test", http.NoBody)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusBadRequest, w.Code)
	var resp map[string]any
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	assert.Equal(t, false, resp["success"])
}

func TestSettingsHandler_TestSMTPConfig_Success(t *testing.T) {
	handler, db := setupSettingsHandlerWithMail(t)

	host, port := startTestSMTPServer(t)

	// Seed SMTP config for local test server.
	db.Create(&models.Setting{Key: "smtp_host", Value: host, Category: "smtp", Type: "string"})
	db.Create(&models.Setting{Key: "smtp_port", Value: fmt.Sprintf("%d", port), Category: "smtp", Type: "number"})
	db.Create(&models.Setting{Key: "smtp_encryption", Value: "none", Category: "smtp", Type: "string"})

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/smtp/test", handler.TestSMTPConfig)

	req, _ := http.NewRequest("POST", "/settings/smtp/test", http.NoBody)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

	var resp map[string]any
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	assert.Equal(t, true, resp["success"])
}

func TestSettingsHandler_SendTestEmail_NonAdmin(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "user")
		c.Next()
	})
	router.POST("/settings/smtp/send-test", handler.SendTestEmail)

	body := map[string]string{"to": "test@example.com"}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/smtp/send-test", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_SendTestEmail_InvalidJSON(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/smtp/send-test", handler.SendTestEmail)

	req, _ := http.NewRequest("POST", "/settings/smtp/send-test", bytes.NewBufferString("invalid"))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_SendTestEmail_NotConfigured(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/smtp/send-test", handler.SendTestEmail)

	body := map[string]string{"to": "test@example.com"}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/smtp/send-test", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusBadRequest, w.Code)
	var resp map[string]any
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	assert.Equal(t, false, resp["success"])
}

func TestSettingsHandler_SendTestEmail_Success(t *testing.T) {
	handler, db := setupSettingsHandlerWithMail(t)

	host, port := startTestSMTPServer(t)

	// Seed SMTP config for local test server.
	db.Create(&models.Setting{Key: "smtp_host", Value: host, Category: "smtp", Type: "string"})
	db.Create(&models.Setting{Key: "smtp_port", Value: fmt.Sprintf("%d", port), Category: "smtp", Type: "number"})
	db.Create(&models.Setting{Key: "smtp_from_address", Value: "noreply@example.com", Category: "smtp", Type: "string"})
	db.Create(&models.Setting{Key: "smtp_encryption", Value: "none", Category: "smtp", Type: "string"})

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/smtp/send-test", handler.SendTestEmail)

	body := map[string]string{"to": "test@example.com"}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/smtp/send-test", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	var resp map[string]any
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	assert.Equal(t, true, resp["success"])
}

func TestMaskPassword(t *testing.T) {
	// Empty password
	assert.Equal(t, "", handlers.MaskPasswordForTest(""))

	// Non-empty password
	assert.Equal(t, "********", handlers.MaskPasswordForTest("secret"))
}

// ============= URL Testing Tests =============

func TestSettingsHandler_ValidatePublicURL_NonAdmin(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "user")
		c.Next()
	})
	router.POST("/settings/validate-url", handler.ValidatePublicURL)

	body := map[string]string{"url": "https://example.com"}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/validate-url", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_ValidatePublicURL_InvalidFormat(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/validate-url", handler.ValidatePublicURL)

	testCases := []struct {
		name string
		url  string
	}{
		{"Missing scheme", "example.com"},
		{"Invalid scheme", "ftp://example.com"},
		{"URL with path", "https://example.com/path"},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			body := map[string]string{"url": tc.url}
			jsonBody, _ := json.Marshal(body)
			req, _ := http.NewRequest("POST", "/settings/validate-url", bytes.NewBuffer(jsonBody))
			req.Header.Set("Content-Type", "application/json")
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			assert.Equal(t, http.StatusBadRequest, w.Code)
			var resp map[string]any
			_ = json.Unmarshal(w.Body.Bytes(), &resp)
			assert.Equal(t, false, resp["valid"])
		})
	}
}

func TestSettingsHandler_ValidatePublicURL_Success(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/validate-url", handler.ValidatePublicURL)

	testCases := []struct {
		name     string
		url      string
		expected string
	}{
		{"HTTPS URL", "https://example.com", "https://example.com"},
		{"HTTP URL", "http://example.com", "http://example.com"},
		{"URL with port", "https://example.com:8080", "https://example.com:8080"},
		{"URL with trailing slash", "https://example.com/", "https://example.com"},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			body := map[string]string{"url": tc.url}
			jsonBody, _ := json.Marshal(body)
			req, _ := http.NewRequest("POST", "/settings/validate-url", bytes.NewBuffer(jsonBody))
			req.Header.Set("Content-Type", "application/json")
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			assert.Equal(t, http.StatusOK, w.Code)
			var resp map[string]any
			_ = json.Unmarshal(w.Body.Bytes(), &resp)
			assert.Equal(t, true, resp["valid"])
			assert.Equal(t, tc.expected, resp["normalized"])
		})
	}
}

func TestSettingsHandler_TestPublicURL_NonAdmin(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "user")
		c.Next()
	})
	router.POST("/settings/test-url", handler.TestPublicURL)

	body := map[string]string{"url": "https://example.com"}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_TestPublicURL_NoRole(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := gin.New()
	// No role set in context
	router.POST("/settings/test-url", handler.TestPublicURL)

	body := map[string]string{"url": "https://example.com"}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_TestPublicURL_InvalidJSON(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/test-url", handler.TestPublicURL)

	req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBufferString("invalid json"))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_TestPublicURL_InvalidURL(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/test-url", handler.TestPublicURL)

	body := map[string]string{"url": "not-a-valid-url"}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusBadRequest, w.Code)
	var resp map[string]any
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	// BadRequest responses only have 'error' field, not 'reachable'
	assert.Contains(t, resp["error"].(string), "parse")
}

func TestSettingsHandler_TestPublicURL_PrivateIPBlocked(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/test-url", handler.TestPublicURL)

	// Test various private IPs that should be blocked
	testCases := []struct {
		name string
		url  string
	}{
		{"localhost", "http://localhost"},
		{"127.0.0.1", "http://127.0.0.1"},
		{"Private 10.x", "http://10.0.0.1"},
		{"Private 192.168.x", "http://192.168.1.1"},
		{"AWS metadata", "http://169.254.169.254"},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			body := map[string]string{"url": tc.url}
			jsonBody, _ := json.Marshal(body)
			req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBuffer(jsonBody))
			req.Header.Set("Content-Type", "application/json")
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			assert.Equal(t, http.StatusOK, w.Code) // Returns 200 but with reachable=false
			var resp map[string]any
			_ = json.Unmarshal(w.Body.Bytes(), &resp)
			assert.Equal(t, false, resp["reachable"])
			// Verify error message contains relevant security text
			errorMsg := resp["error"].(string)
			assert.True(t,
				contains(errorMsg, "private ip") || contains(errorMsg, "metadata") || contains(errorMsg, "blocked"),
				"Expected security error message, got: %s", errorMsg)
		})
	}
}

// Helper function for case-insensitive contains
func contains(s, substr string) bool {
	return bytes.Contains([]byte(s), []byte(substr))
}

func TestSettingsHandler_TestPublicURL_Success(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	// NOTE: Using a real public URL instead of httptest.NewServer() because
	// SSRF protection (correctly) blocks localhost/127.0.0.1.
	// Using example.com which is guaranteed to be reachable and is designed for testing
	// Alternative: Refactor handler to accept injectable URL validator (future improvement).
	publicTestURL := "https://example.com"

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/test-url", handler.TestPublicURL)

	body := map[string]string{"url": publicTestURL}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	var resp map[string]any
	_ = json.Unmarshal(w.Body.Bytes(), &resp)

	// The test verifies the handler works with a real public URL
	assert.Equal(t, true, resp["reachable"], "example.com should be reachable")
	assert.NotNil(t, resp["latency"])
	// Note: message field is no longer included in response
}

func TestSettingsHandler_TestPublicURL_DNSFailure(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/test-url", handler.TestPublicURL)

	body := map[string]string{"url": "http://nonexistent-domain-12345.invalid"}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code) // Returns 200 but with reachable=false
	var resp map[string]any
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	assert.Equal(t, false, resp["reachable"])
	// DNS errors contain "dns" or "resolution" keywords (case-insensitive)
	errorMsg := resp["error"].(string)
	assert.True(t,
		contains(errorMsg, "dns") || contains(errorMsg, "resolution"),
		"Expected DNS error message, got: %s", errorMsg)
}

func TestSettingsHandler_TestPublicURL_ConnectivityError(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/test-url", handler.TestPublicURL)

	// 192.0.2.0/24 is reserved for documentation/testing and is not considered private by
	// network.IsPrivateIP(). Using a closed port should trigger a deterministic connect error
	// after passing SSRF validation.
	body := map[string]string{"url": "http://192.0.2.1:1"}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	var resp map[string]any
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	assert.Equal(t, false, resp["reachable"])
	_, ok := resp["error"].(string)
	assert.True(t, ok)
}

// ============= SSRF Protection Tests =============

func TestSettingsHandler_TestPublicURL_SSRFProtection(t *testing.T) {
	tests := []struct {
		name              string
		url               string
		expectedStatus    int
		expectedReachable bool
		errorContains     string
	}{
		{
			name:              "blocks RFC 1918 - 10.x",
			url:               "http://10.0.0.1",
			expectedStatus:    http.StatusOK,
			expectedReachable: false,
			errorContains:     "private",
		},
		{
			name:              "blocks RFC 1918 - 192.168.x",
			url:               "http://192.168.1.1",
			expectedStatus:    http.StatusOK,
			expectedReachable: false,
			errorContains:     "private",
		},
		{
			name:              "blocks RFC 1918 - 172.16.x",
			url:               "http://172.16.0.1",
			expectedStatus:    http.StatusOK,
			expectedReachable: false,
			errorContains:     "private",
		},
		{
			name:              "blocks localhost",
			url:               "http://localhost",
			expectedStatus:    http.StatusOK,
			expectedReachable: false,
			errorContains:     "private",
		},
		{
			name:              "blocks 127.0.0.1",
			url:               "http://127.0.0.1",
			expectedStatus:    http.StatusOK,
			expectedReachable: false,
			errorContains:     "private",
		},
		{
			name:              "blocks cloud metadata",
			url:               "http://169.254.169.254",
			expectedStatus:    http.StatusOK,
			expectedReachable: false,
			errorContains:     "cloud metadata",
		},
		{
			name:              "blocks link-local",
			url:               "http://169.254.1.1",
			expectedStatus:    http.StatusOK,
			expectedReachable: false,
			errorContains:     "private",
		},
	}

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

			router := newAdminRouter()
			router.Use(func(c *gin.Context) {
				c.Set("role", "admin")
				c.Next()
			})
			router.POST("/settings/test-url", handler.TestPublicURL)

			body := map[string]string{"url": tt.url}
			jsonBody, _ := json.Marshal(body)
			req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBuffer(jsonBody))
			req.Header.Set("Content-Type", "application/json")

			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			assert.Equal(t, tt.expectedStatus, w.Code)

			var resp map[string]any
			err := json.Unmarshal(w.Body.Bytes(), &resp)
			assert.NoError(t, err)
			assert.Equal(t, tt.expectedReachable, resp["reachable"])

			if tt.errorContains != "" {
				errorMsg, ok := resp["error"].(string)
				assert.True(t, ok, "error field should be a string")
				assert.Contains(t, errorMsg, tt.errorContains)
			}
		})
	}
}

func TestSettingsHandler_TestPublicURL_EmbeddedCredentials(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/test-url", handler.TestPublicURL)

	// Test URL with embedded credentials (parser differential attack)
	body := map[string]string{"url": "http://evil.com@127.0.0.1/"}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")

	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

	var resp map[string]any
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	assert.False(t, resp["reachable"].(bool))
	assert.Contains(t, resp["error"].(string), "credentials")
}

func TestSettingsHandler_TestPublicURL_EmptyURL(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/test-url", handler.TestPublicURL)

	tests := []struct {
		name    string
		payload string
	}{
		{"empty string", `{"url": ""}`},
		{"missing field", `{}`},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBufferString(tt.payload))
			req.Header.Set("Content-Type", "application/json")

			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

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

func TestSettingsHandler_TestPublicURL_InvalidScheme(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/test-url", handler.TestPublicURL)

	tests := []struct {
		name string
		url  string
	}{
		{"ftp scheme", "ftp://example.com"},
		{"file scheme", "file:///etc/passwd"},
		{"javascript scheme", "javascript:alert(1)"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			body := map[string]string{"url": tt.url}
			jsonBody, _ := json.Marshal(body)
			req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBuffer(jsonBody))
			req.Header.Set("Content-Type", "application/json")

			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			assert.Equal(t, http.StatusBadRequest, w.Code)
			var resp map[string]any
			err := json.Unmarshal(w.Body.Bytes(), &resp)
			require.NoError(t, err, "Failed to unmarshal response")
			// BadRequest responses only have 'error' field, not 'reachable'
			assert.Contains(t, resp["error"].(string), "parse")
		})
	}
}

func TestSettingsHandler_ValidatePublicURL_InvalidJSON(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/validate-url", handler.ValidatePublicURL)

	req, _ := http.NewRequest("POST", "/settings/validate-url", bytes.NewBufferString("not-json"))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestSettingsHandler_ValidatePublicURL_URLWithWarning(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/validate-url", handler.ValidatePublicURL)

	// URL with HTTP scheme may generate a warning
	body := map[string]string{"url": "http://example.com"}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/validate-url", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	var resp map[string]any
	err := json.Unmarshal(w.Body.Bytes(), &resp)
	require.NoError(t, err, "Failed to unmarshal response")
	assert.Equal(t, true, resp["valid"])
	// May have a warning about HTTP vs HTTPS
}

func TestSettingsHandler_UpdateSMTPConfig_DatabaseError(t *testing.T) {
	handler, db := setupSettingsHandlerWithMail(t)

	// Close the database to force an error
	sqlDB, _ := db.DB()
	_ = sqlDB.Close()

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.PUT("/settings/smtp", handler.UpdateSMTPConfig)

	// Include password (not masked) to skip GetSMTPConfig path which would also fail
	body := map[string]any{
		"host":         "smtp.example.com",
		"port":         587,
		"from_address": "test@example.com",
		"encryption":   "starttls",
		"password":     "test-password", // Provide password to skip GetSMTPConfig call
	}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("PUT", "/settings/smtp", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusInternalServerError, w.Code)
	assert.Contains(t, w.Body.String(), "Failed to save")
}

func TestSettingsHandler_TestPublicURL_IPv6LocalhostBlocked(t *testing.T) {
	handler, _ := setupSettingsHandlerWithMail(t)

	router := newAdminRouter()
	router.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Next()
	})
	router.POST("/settings/test-url", handler.TestPublicURL)

	// Test IPv6 loopback address
	body := map[string]string{"url": "http://[::1]"}
	jsonBody, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", "/settings/test-url", bytes.NewBuffer(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	var resp map[string]any
	err := json.Unmarshal(w.Body.Bytes(), &resp)
	require.NoError(t, err, "Failed to unmarshal response")
	assert.False(t, resp["reachable"].(bool))
	// IPv6 loopback should be blocked
}

// TestUpdateSetting_EmptyValueIsAccepted guards the PR-1 fix: Value must NOT carry
// binding:"required". Gin treats "" as missing for string fields and returns 400 if
// the tag is present. Re-adding the tag would silently regress the CrowdSec enable
// flow (which sends value="" to clear the setting).
func TestUpdateSetting_EmptyValueIsAccepted(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	body := `{"key":"security.crowdsec.enabled","value":""}`
	req, _ := http.NewRequest(http.MethodPost, "/settings", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code, "empty Value must not trigger a 400 validation error")

	var s models.Setting
	require.NoError(t, db.Where("key = ?", "security.crowdsec.enabled").First(&s).Error)
	assert.Equal(t, "", s.Value)
}

// TestUpdateSetting_MissingKeyRejected ensures binding:"required" was only removed
// from Value and not accidentally also from Key. A request with no "key" field must
// still return 400.
func TestUpdateSetting_MissingKeyRejected(t *testing.T) {
	db := setupSettingsTestDB(t)

	handler := handlers.NewSettingsHandler(db)
	router := newAdminRouter()
	router.POST("/settings", handler.UpdateSetting)

	body := `{"value":"true"}`
	req, _ := http.NewRequest(http.MethodPost, "/settings", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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