package handlers

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

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

	"github.com/Wikid82/charon/backend/internal/models"
	"github.com/Wikid82/charon/backend/internal/services"
)

// setupUpdateTestRouter creates a test router with the proxy host handler registered.
// Uses a dedicated in-memory SQLite database with all required models migrated.
func setupUpdateTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
	t.Helper()
	gin.SetMode(gin.TestMode)

	dsn := "file:" + t.Name() + "?mode=memory&cache=shared"
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&models.ProxyHost{},
		&models.Location{},
		&models.SecurityHeaderProfile{},
		&models.Notification{},
		&models.NotificationProvider{},
	))

	ns := services.NewNotificationService(db)
	h := NewProxyHostHandler(db, nil, ns, nil)

	r := gin.New()
	api := r.Group("/api/v1")
	h.RegisterRoutes(api)

	return r, db
}

// createTestProxyHost creates a proxy host in the database for testing.
func createTestProxyHost(t *testing.T, db *gorm.DB, name string) models.ProxyHost {
	t.Helper()
	host := models.ProxyHost{
		UUID:          uuid.NewString(),
		Name:          name,
		DomainNames:   name + ".test.com",
		ForwardScheme: "http",
		ForwardHost:   "localhost",
		ForwardPort:   8080,
		Enabled:       true,
	}
	require.NoError(t, db.Create(&host).Error)
	return host
}

// createTestSecurityHeaderProfile creates a security header profile for testing.
func createTestSecurityHeaderProfile(t *testing.T, db *gorm.DB, name string) models.SecurityHeaderProfile {
	t.Helper()
	profile := models.SecurityHeaderProfile{
		UUID:          uuid.NewString(),
		Name:          name,
		IsPreset:      false,
		SecurityScore: 85,
	}
	require.NoError(t, db.Create(&profile).Error)
	return profile
}

// TestProxyHostUpdate_EnableStandardHeaders_Null tests updating enable_standard_headers to null.
func TestProxyHostUpdate_EnableStandardHeaders_Null(t *testing.T) {
	t.Parallel()
	router, db := setupUpdateTestRouter(t)

	// Create host with enable_standard_headers set to true
	enabled := true
	host := models.ProxyHost{
		UUID:                  uuid.NewString(),
		Name:                  "Test Host",
		DomainNames:           "test.example.com",
		ForwardScheme:         "http",
		ForwardHost:           "localhost",
		ForwardPort:           8080,
		Enabled:               true,
		EnableStandardHeaders: &enabled,
	}
	require.NoError(t, db.Create(&host).Error)

	// Verify initial state
	require.NotNil(t, host.EnableStandardHeaders)
	require.True(t, *host.EnableStandardHeaders)

	// Update with enable_standard_headers: null
	updateBody := map[string]any{
		"name":                    "Test Host Updated",
		"domain_names":            "test.example.com",
		"forward_scheme":          "http",
		"forward_host":            "localhost",
		"forward_port":            8080,
		"enable_standard_headers": nil,
	}
	body, _ := json.Marshal(updateBody)

	req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusOK, resp.Code)

	// Verify enable_standard_headers is now nil
	var updated models.ProxyHost
	require.NoError(t, db.First(&updated, "uuid = ?", host.UUID).Error)
	assert.Nil(t, updated.EnableStandardHeaders)
}

// TestProxyHostUpdate_EnableStandardHeaders_True tests updating enable_standard_headers to true.
func TestProxyHostUpdate_EnableStandardHeaders_True(t *testing.T) {
	t.Parallel()
	router, db := setupUpdateTestRouter(t)

	// Create host with enable_standard_headers set to nil
	host := models.ProxyHost{
		UUID:                  uuid.NewString(),
		Name:                  "Test Host",
		DomainNames:           "test.example.com",
		ForwardScheme:         "http",
		ForwardHost:           "localhost",
		ForwardPort:           8080,
		Enabled:               true,
		EnableStandardHeaders: nil,
	}
	require.NoError(t, db.Create(&host).Error)

	// Update with enable_standard_headers: true
	updateBody := map[string]any{
		"name":                    "Test Host Updated",
		"domain_names":            "test.example.com",
		"forward_scheme":          "http",
		"forward_host":            "localhost",
		"forward_port":            8080,
		"enable_standard_headers": true,
	}
	body, _ := json.Marshal(updateBody)

	req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusOK, resp.Code)

	// Verify enable_standard_headers is now true
	var updated models.ProxyHost
	require.NoError(t, db.First(&updated, "uuid = ?", host.UUID).Error)
	require.NotNil(t, updated.EnableStandardHeaders)
	assert.True(t, *updated.EnableStandardHeaders)
}

// TestProxyHostUpdate_EnableStandardHeaders_False tests updating enable_standard_headers to false.
func TestProxyHostUpdate_EnableStandardHeaders_False(t *testing.T) {
	t.Parallel()
	router, db := setupUpdateTestRouter(t)

	// Create host with enable_standard_headers set to true
	enabled := true
	host := models.ProxyHost{
		UUID:                  uuid.NewString(),
		Name:                  "Test Host",
		DomainNames:           "test.example.com",
		ForwardScheme:         "http",
		ForwardHost:           "localhost",
		ForwardPort:           8080,
		Enabled:               true,
		EnableStandardHeaders: &enabled,
	}
	require.NoError(t, db.Create(&host).Error)

	// Update with enable_standard_headers: false
	updateBody := map[string]any{
		"name":                    "Test Host Updated",
		"domain_names":            "test.example.com",
		"forward_scheme":          "http",
		"forward_host":            "localhost",
		"forward_port":            8080,
		"enable_standard_headers": false,
	}
	body, _ := json.Marshal(updateBody)

	req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusOK, resp.Code)

	// Verify enable_standard_headers is now false
	var updated models.ProxyHost
	require.NoError(t, db.First(&updated, "uuid = ?", host.UUID).Error)
	require.NotNil(t, updated.EnableStandardHeaders)
	assert.False(t, *updated.EnableStandardHeaders)
}

// TestProxyHostUpdate_ForwardAuthEnabled tests updating forward_auth_enabled from false to true.
func TestProxyHostUpdate_ForwardAuthEnabled(t *testing.T) {
	t.Parallel()
	router, db := setupUpdateTestRouter(t)

	// Create host with forward_auth_enabled = false
	host := models.ProxyHost{
		UUID:               uuid.NewString(),
		Name:               "Test Host",
		DomainNames:        "test.example.com",
		ForwardScheme:      "http",
		ForwardHost:        "localhost",
		ForwardPort:        8080,
		Enabled:            true,
		ForwardAuthEnabled: false,
	}
	require.NoError(t, db.Create(&host).Error)
	require.False(t, host.ForwardAuthEnabled)

	// Update with forward_auth_enabled: true
	updateBody := map[string]any{
		"name":                 "Test Host Updated",
		"domain_names":         "test.example.com",
		"forward_scheme":       "http",
		"forward_host":         "localhost",
		"forward_port":         8080,
		"forward_auth_enabled": true,
	}
	body, _ := json.Marshal(updateBody)

	req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusOK, resp.Code)

	// Verify forward_auth_enabled is now true
	var updated models.ProxyHost
	require.NoError(t, db.First(&updated, "uuid = ?", host.UUID).Error)
	assert.True(t, updated.ForwardAuthEnabled)
}

// TestProxyHostUpdate_WAFDisabled tests updating waf_disabled from false to true.
func TestProxyHostUpdate_WAFDisabled(t *testing.T) {
	t.Parallel()
	router, db := setupUpdateTestRouter(t)

	// Create host with waf_disabled = false
	host := models.ProxyHost{
		UUID:          uuid.NewString(),
		Name:          "Test Host",
		DomainNames:   "test.example.com",
		ForwardScheme: "http",
		ForwardHost:   "localhost",
		ForwardPort:   8080,
		Enabled:       true,
		WAFDisabled:   false,
	}
	require.NoError(t, db.Create(&host).Error)
	require.False(t, host.WAFDisabled)

	// Update with waf_disabled: true
	updateBody := map[string]any{
		"name":           "Test Host Updated",
		"domain_names":   "test.example.com",
		"forward_scheme": "http",
		"forward_host":   "localhost",
		"forward_port":   8080,
		"waf_disabled":   true,
	}
	body, _ := json.Marshal(updateBody)

	req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusOK, resp.Code)

	// Verify waf_disabled is now true
	var updated models.ProxyHost
	require.NoError(t, db.First(&updated, "uuid = ?", host.UUID).Error)
	assert.True(t, updated.WAFDisabled)
}

// TestProxyHostUpdate_SecurityHeaderProfileID_NegativeFloat tests that a negative float64
// for security_header_profile_id returns a 400 Bad Request.
func TestProxyHostUpdate_SecurityHeaderProfileID_NegativeFloat(t *testing.T) {
	t.Parallel()
	router, db := setupUpdateTestRouter(t)

	host := createTestProxyHost(t, db, "negative-float-test")

	// Update with security_header_profile_id as negative float64
	// JSON numbers default to float64 in Go
	updateBody := map[string]any{
		"name":                       "Test Host Updated",
		"domain_names":               "negative-float-test.test.com",
		"forward_scheme":             "http",
		"forward_host":               "localhost",
		"forward_port":               8080,
		"security_header_profile_id": -1.0,
	}
	body, _ := json.Marshal(updateBody)

	req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusBadRequest, resp.Code)

	var result map[string]any
	require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &result))
	assert.Contains(t, result["error"], "invalid security_header_profile_id")
}

// TestProxyHostUpdate_SecurityHeaderProfileID_NegativeInt tests that a negative int
// for security_header_profile_id returns a 400 Bad Request.
// Note: JSON decoding in Go typically produces float64, but we test the int branch
// by ensuring the conversion logic handles negative values correctly.
func TestProxyHostUpdate_SecurityHeaderProfileID_NegativeInt(t *testing.T) {
	t.Parallel()
	router, db := setupUpdateTestRouter(t)

	host := createTestProxyHost(t, db, "negative-int-test")

	// Update with security_header_profile_id as negative number
	// In JSON, -5 will be decoded as float64(-5), triggering the float64 path
	updateBody := map[string]any{
		"name":                       "Test Host Updated",
		"domain_names":               "negative-int-test.test.com",
		"forward_scheme":             "http",
		"forward_host":               "localhost",
		"forward_port":               8080,
		"security_header_profile_id": -5,
	}
	body, _ := json.Marshal(updateBody)

	req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusBadRequest, resp.Code)

	var result map[string]any
	require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &result))
	assert.Contains(t, result["error"], "invalid security_header_profile_id")
}

// TestProxyHostUpdate_SecurityHeaderProfileID_InvalidString tests that an invalid string
// for security_header_profile_id returns a 400 Bad Request.
func TestProxyHostUpdate_SecurityHeaderProfileID_InvalidString(t *testing.T) {
	t.Parallel()
	router, db := setupUpdateTestRouter(t)

	host := createTestProxyHost(t, db, "invalid-string-test")

	// Update with security_header_profile_id as invalid string
	updateBody := map[string]any{
		"name":                       "Test Host Updated",
		"domain_names":               "invalid-string-test.test.com",
		"forward_scheme":             "http",
		"forward_host":               "localhost",
		"forward_port":               8080,
		"security_header_profile_id": "not-a-number",
	}
	body, _ := json.Marshal(updateBody)

	req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusBadRequest, resp.Code)

	var result map[string]any
	require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &result))
	assert.Contains(t, result["error"], "invalid security_header_profile_id")
}

// TestProxyHostUpdate_SecurityHeaderProfileID_UnsupportedType tests that an unsupported type
// (boolean) for security_header_profile_id returns a 400 Bad Request.
func TestProxyHostUpdate_SecurityHeaderProfileID_UnsupportedType(t *testing.T) {
	t.Parallel()
	router, db := setupUpdateTestRouter(t)

	host := createTestProxyHost(t, db, "unsupported-type-test")

	testCases := []struct {
		name  string
		value any
	}{
		{
			name:  "boolean_true",
			value: true,
		},
		{
			name:  "boolean_false",
			value: false,
		},
		{
			name:  "array",
			value: []int{1, 2, 3},
		},
		{
			name:  "object",
			value: map[string]any{"id": 1},
		},
	}

	for _, tc := range testCases {
		tc := tc // capture range variable
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			updateBody := map[string]any{
				"name":                       "Test Host Updated",
				"domain_names":               "unsupported-type-test.test.com",
				"forward_scheme":             "http",
				"forward_host":               "localhost",
				"forward_port":               8080,
				"security_header_profile_id": tc.value,
			}
			body, _ := json.Marshal(updateBody)

			req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
			req.Header.Set("Content-Type", "application/json")
			resp := httptest.NewRecorder()
			router.ServeHTTP(resp, req)

			require.Equal(t, http.StatusBadRequest, resp.Code)

			var result map[string]any
			require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &result))
			assert.Contains(t, result["error"], "invalid security_header_profile_id")
		})
	}
}

// TestProxyHostUpdate_SecurityHeaderProfileID_ValidAssignment tests that a valid
// security_header_profile_id can be assigned to a proxy host.
func TestProxyHostUpdate_SecurityHeaderProfileID_ValidAssignment(t *testing.T) {
	t.Parallel()
	router, db := setupUpdateTestRouter(t)

	// Create a security header profile
	profile := createTestSecurityHeaderProfile(t, db, "Valid Profile")

	// Create host without a profile
	host := createTestProxyHost(t, db, "valid-assignment-test")
	require.Nil(t, host.SecurityHeaderProfileID)

	// Test cases for valid assignment using different type representations
	testCases := []struct {
		name  string
		value any
	}{
		{
			name:  "as_float64",
			value: float64(profile.ID),
		},
		{
			name:  "as_string",
			value: fmt.Sprintf("%d", profile.ID),
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			// Reset host's profile to nil before each sub-test
			db.Model(&host).Update("security_header_profile_id", nil)

			updateBody := map[string]any{
				"name":                       "Test Host Updated",
				"domain_names":               "valid-assignment-test.test.com",
				"forward_scheme":             "http",
				"forward_host":               "localhost",
				"forward_port":               8080,
				"security_header_profile_id": tc.value,
			}
			body, _ := json.Marshal(updateBody)

			req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
			req.Header.Set("Content-Type", "application/json")
			resp := httptest.NewRecorder()
			router.ServeHTTP(resp, req)

			require.Equal(t, http.StatusOK, resp.Code)

			// Verify the profile was assigned
			var updated models.ProxyHost
			require.NoError(t, db.First(&updated, "uuid = ?", host.UUID).Error)
			require.NotNil(t, updated.SecurityHeaderProfileID)
			assert.Equal(t, profile.ID, *updated.SecurityHeaderProfileID)
		})
	}
}

// TestProxyHostUpdate_SecurityHeaderProfileID_SetToNull tests that setting
// security_header_profile_id to null removes the profile assignment.
func TestProxyHostUpdate_SecurityHeaderProfileID_SetToNull(t *testing.T) {
	t.Parallel()
	router, db := setupUpdateTestRouter(t)

	// Create a security header profile
	profile := createTestSecurityHeaderProfile(t, db, "Null Test Profile")

	// Create host with profile assigned
	host := models.ProxyHost{
		UUID:                    uuid.NewString(),
		Name:                    "Test Host",
		DomainNames:             "null-profile-test.test.com",
		ForwardScheme:           "http",
		ForwardHost:             "localhost",
		ForwardPort:             8080,
		Enabled:                 true,
		SecurityHeaderProfileID: &profile.ID,
	}
	require.NoError(t, db.Create(&host).Error)
	require.NotNil(t, host.SecurityHeaderProfileID)

	// Update with security_header_profile_id: null
	updateBody := map[string]any{
		"name":                       "Test Host Updated",
		"domain_names":               "null-profile-test.test.com",
		"forward_scheme":             "http",
		"forward_host":               "localhost",
		"forward_port":               8080,
		"security_header_profile_id": nil,
	}
	body, _ := json.Marshal(updateBody)

	req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	require.Equal(t, http.StatusOK, resp.Code)

	// Verify the profile was removed
	var updated models.ProxyHost
	require.NoError(t, db.First(&updated, "uuid = ?", host.UUID).Error)
	assert.Nil(t, updated.SecurityHeaderProfileID)
}

// TestBulkUpdateSecurityHeaders_DBError_NonNotFound tests that a database error
// (other than not found) during profile lookup returns a 500 Internal Server Error.
func TestBulkUpdateSecurityHeaders_DBError_NonNotFound(t *testing.T) {
	t.Parallel()
	gin.SetMode(gin.TestMode)

	dsn := "file:" + t.Name() + "?mode=memory&cache=shared"
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&models.ProxyHost{},
		&models.Location{},
		&models.SecurityHeaderProfile{},
		&models.Notification{},
		&models.NotificationProvider{},
	))

	// Create a valid security header profile
	profile := createTestSecurityHeaderProfile(t, db, "DB Error Test Profile")

	// Create a valid proxy host
	host := models.ProxyHost{
		UUID:          uuid.NewString(),
		Name:          "DB Error Test Host",
		DomainNames:   "dberror.test.com",
		ForwardScheme: "http",
		ForwardHost:   "localhost",
		ForwardPort:   8080,
		Enabled:       true,
	}
	require.NoError(t, db.Create(&host).Error)

	ns := services.NewNotificationService(db)
	h := NewProxyHostHandler(db, nil, ns, nil)

	r := gin.New()
	api := r.Group("/api/v1")
	h.RegisterRoutes(api)

	// Close the underlying SQL connection to simulate a DB error
	sqlDB, err := db.DB()
	require.NoError(t, err)
	require.NoError(t, sqlDB.Close())

	// Attempt bulk update - should fail with internal server error due to closed DB
	reqBody := map[string]any{
		"host_uuids":                 []string{host.UUID},
		"security_header_profile_id": profile.ID,
	}
	body, _ := json.Marshal(reqBody)

	req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/bulk-update-security-headers", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp := httptest.NewRecorder()
	r.ServeHTTP(resp, req)

	// The handler should return 500 when DB operations fail
	require.Equal(t, http.StatusInternalServerError, resp.Code)
}
