package handlers_test

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

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

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

func setupNotificationProviderTest(t *testing.T) (*gin.Engine, *gorm.DB) {
	t.Helper()
	db := handlers.OpenTestDB(t)
	require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.Notification{}))

	service := services.NewNotificationService(db)
	handler := handlers.NewNotificationProviderHandler(service)

	r := gin.Default()
	r.Use(func(c *gin.Context) {
		c.Set("role", "admin")
		c.Set("userID", uint(1))
		c.Next()
	})
	api := r.Group("/api/v1")
	providers := api.Group("/notifications/providers")
	providers.GET("", handler.List)
	providers.POST("/preview", handler.Preview)
	providers.POST("", handler.Create)
	providers.PUT("/:id", handler.Update)
	providers.DELETE("/:id", handler.Delete)
	providers.POST("/test", handler.Test)
	api.GET("/notifications/templates", handler.Templates)

	return r, db
}

func TestNotificationProviderHandler_CRUD(t *testing.T) {
	r, db := setupNotificationProviderTest(t)

	// 1. Create
	provider := models.NotificationProvider{
		Name: "Test Discord",
		Type: "discord",
		URL:  "https://discord.com/api/webhooks/...",
	}
	body, _ := json.Marshal(provider)
	req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusCreated, w.Code)
	var created models.NotificationProvider
	err := json.Unmarshal(w.Body.Bytes(), &created)
	require.NoError(t, err)
	assert.Equal(t, provider.Name, created.Name)
	assert.NotEmpty(t, created.ID)

	// 2. List
	req, _ = http.NewRequest("GET", "/api/v1/notifications/providers", http.NoBody)
	w = httptest.NewRecorder()
	r.ServeHTTP(w, req)
	assert.Equal(t, http.StatusOK, w.Code)
	var list []models.NotificationProvider
	err = json.Unmarshal(w.Body.Bytes(), &list)
	require.NoError(t, err)
	assert.Len(t, list, 1)

	// 3. Update
	created.Name = "Updated Discord"
	body, _ = json.Marshal(created)
	req, _ = http.NewRequest("PUT", "/api/v1/notifications/providers/"+created.ID, bytes.NewBuffer(body))
	w = httptest.NewRecorder()
	r.ServeHTTP(w, req)
	assert.Equal(t, http.StatusOK, w.Code)
	var updated models.NotificationProvider
	err = json.Unmarshal(w.Body.Bytes(), &updated)
	require.NoError(t, err)
	assert.Equal(t, "Updated Discord", updated.Name)

	// Verify in DB
	var dbProvider models.NotificationProvider
	db.First(&dbProvider, "id = ?", created.ID)
	assert.Equal(t, "Updated Discord", dbProvider.Name)

	// 4. Delete
	req, _ = http.NewRequest("DELETE", "/api/v1/notifications/providers/"+created.ID, http.NoBody)
	w = httptest.NewRecorder()
	r.ServeHTTP(w, req)
	assert.Equal(t, http.StatusOK, w.Code)

	// Verify Delete
	var count int64
	db.Model(&models.NotificationProvider{}).Count(&count)
	assert.Equal(t, int64(0), count)
}

func TestNotificationProviderHandler_Templates(t *testing.T) {
	r, _ := setupNotificationProviderTest(t)

	req, _ := http.NewRequest("GET", "/api/v1/notifications/templates", http.NoBody)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	var templates []map[string]string
	err := json.Unmarshal(w.Body.Bytes(), &templates)
	require.NoError(t, err)
	assert.Len(t, templates, 3)
}

func TestNotificationProviderHandler_Test(t *testing.T) {
	r, db := setupNotificationProviderTest(t)

	stored := models.NotificationProvider{
		ID:      "trusted-provider-id",
		Name:    "Stored Provider",
		Type:    "discord",
		URL:     "invalid-url",
		Enabled: true,
	}
	require.NoError(t, db.Create(&stored).Error)

	payload := map[string]any{
		"id":   stored.ID,
		"type": "discord",
		"url":  "https://discord.com/api/webhooks/123/override",
	}
	body, _ := json.Marshal(payload)
	req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

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

func TestNotificationProviderHandler_Test_RequiresTrustedProviderID(t *testing.T) {
	r, _ := setupNotificationProviderTest(t)

	payload := map[string]any{
		"type": "discord",
		"url":  "https://discord.com/api/webhooks/123/abc",
	}
	body, _ := json.Marshal(payload)
	req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

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

func TestNotificationProviderHandler_Test_ReturnsNotFoundForUnknownProvider(t *testing.T) {
	r, _ := setupNotificationProviderTest(t)

	payload := map[string]any{
		"id": "missing-provider-id",
	}
	body, _ := json.Marshal(payload)
	req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusNotFound, w.Code)
	assert.Contains(t, w.Body.String(), "PROVIDER_NOT_FOUND")
}

func TestNotificationProviderHandler_Errors(t *testing.T) {
	r, _ := setupNotificationProviderTest(t)

	// Create Invalid JSON
	req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer([]byte("invalid")))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	assert.Equal(t, http.StatusBadRequest, w.Code)

	// Update Invalid JSON
	req, _ = http.NewRequest("PUT", "/api/v1/notifications/providers/123", bytes.NewBuffer([]byte("invalid")))
	w = httptest.NewRecorder()
	r.ServeHTTP(w, req)
	assert.Equal(t, http.StatusBadRequest, w.Code)

	// Test Invalid JSON
	req, _ = http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer([]byte("invalid")))
	w = httptest.NewRecorder()
	r.ServeHTTP(w, req)
	assert.Equal(t, http.StatusBadRequest, w.Code)
}

func TestNotificationProviderHandler_InvalidCustomTemplate_Rejects(t *testing.T) {
	r, _ := setupNotificationProviderTest(t)

	// Create with invalid custom template should return 400
	provider := models.NotificationProvider{
		Name:     "Bad",
		Type:     "discord",
		URL:      "https://discord.com/api/webhooks/123/abc",
		Template: "custom",
		Config:   `{"broken": "{{.Title"}`,
	}
	body, _ := json.Marshal(provider)
	req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	assert.Equal(t, http.StatusBadRequest, w.Code)

	// Create valid and then attempt update to invalid custom template
	provider = models.NotificationProvider{
		Name:     "Good",
		Type:     "discord",
		URL:      "https://discord.com/api/webhooks/456/def",
		Template: "minimal",
	}
	body, _ = json.Marshal(provider)
	req, _ = http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
	w = httptest.NewRecorder()
	r.ServeHTTP(w, req)
	assert.Equal(t, http.StatusCreated, w.Code)
	var created models.NotificationProvider
	_ = json.Unmarshal(w.Body.Bytes(), &created)

	created.Template = "custom"
	created.Config = `{"broken": "{{.Title"}`
	body, _ = json.Marshal(created)
	req, _ = http.NewRequest("PUT", "/api/v1/notifications/providers/"+created.ID, bytes.NewBuffer(body))
	w = httptest.NewRecorder()
	r.ServeHTTP(w, req)
	assert.Equal(t, http.StatusBadRequest, w.Code)
}

func TestNotificationProviderHandler_Preview(t *testing.T) {
	r, _ := setupNotificationProviderTest(t)

	// Minimal template preview
	provider := models.NotificationProvider{
		Type:     "discord",
		URL:      "https://discord.com/api/webhooks/123/abc",
		Template: "minimal",
	}
	body, _ := json.Marshal(provider)
	req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/preview", bytes.NewBuffer(body))
	w := httptest.NewRecorder()
	r.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)
	assert.Contains(t, resp, "rendered")
	assert.Contains(t, resp, "parsed")

	// Invalid template should not succeed
	provider.Config = `{"broken": "{{.Title"}`
	provider.Template = "custom"
	body, _ = json.Marshal(provider)
	req, _ = http.NewRequest("POST", "/api/v1/notifications/providers/preview", bytes.NewBuffer(body))
	w = httptest.NewRecorder()
	r.ServeHTTP(w, req)
	assert.Equal(t, http.StatusBadRequest, w.Code)
}

func TestNotificationProviderHandler_CreateRejectsDiscordIPHost(t *testing.T) {
	r, _ := setupNotificationProviderTest(t)

	provider := models.NotificationProvider{
		Name: "Discord IP",
		Type: "discord",
		URL:  "https://203.0.113.10/api/webhooks/123456/token_abc",
	}
	body, _ := json.Marshal(provider)
	req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

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

func TestNotificationProviderHandler_CreateAcceptsDiscordHostname(t *testing.T) {
	r, _ := setupNotificationProviderTest(t)

	provider := models.NotificationProvider{
		Name: "Discord Host",
		Type: "discord",
		URL:  "https://discord.com/api/webhooks/123456/token_abc",
	}
	body, _ := json.Marshal(provider)
	req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

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

func TestNotificationProviderHandler_CreateIgnoresServerManagedMigrationFields(t *testing.T) {
	r, db := setupNotificationProviderTest(t)

	payload := map[string]any{
		"name":                  "Create Ignore Migration",
		"type":                  "discord",
		"url":                   "https://discord.com/api/webhooks/123/abc",
		"template":              "minimal",
		"enabled":               true,
		"notify_proxy_hosts":    true,
		"notify_remote_servers": true,
		"notify_domains":        true,
		"notify_certs":          true,
		"notify_uptime":         true,
		"engine":                "notify_v1",
		"service_config":        `{"token":"attacker"}`,
		"migration_state":       "migrated",
		"migration_error":       "client-value",
		"legacy_url":            "https://malicious.example",
		"last_migrated_at":      "2020-01-01T00:00:00Z",
		"id":                    "client-controlled-id",
	}

	body, _ := json.Marshal(payload)
	req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

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

	var created models.NotificationProvider
	err := json.Unmarshal(w.Body.Bytes(), &created)
	require.NoError(t, err)
	require.NotEmpty(t, created.ID)
	assert.NotEqual(t, "client-controlled-id", created.ID)

	var dbProvider models.NotificationProvider
	err = db.First(&dbProvider, "id = ?", created.ID).Error
	require.NoError(t, err)
	assert.Empty(t, dbProvider.Engine)
	assert.Empty(t, dbProvider.ServiceConfig)
	assert.Empty(t, dbProvider.MigrationState)
	assert.Empty(t, dbProvider.MigrationError)
	assert.Empty(t, dbProvider.LegacyURL)
	assert.Nil(t, dbProvider.LastMigratedAt)
}

func TestNotificationProviderHandler_UpdatePreservesServerManagedMigrationFields(t *testing.T) {
	r, db := setupNotificationProviderTest(t)

	now := time.Now().UTC().Round(time.Second)
	original := models.NotificationProvider{
		Name:                "Original",
		Type:                "discord",
		URL:                 "https://discord.com/api/webhooks/123/abc",
		Template:            "minimal",
		Enabled:             true,
		NotifyProxyHosts:    true,
		NotifyRemoteServers: true,
		NotifyDomains:       true,
		NotifyCerts:         true,
		NotifyUptime:        true,
		Engine:              "notify_v1",
		ServiceConfig:       `{"token":"server"}`,
		MigrationState:      "migrated",
		MigrationError:      "",
		LegacyURL:           "discord://legacy",
		LastMigratedAt:      &now,
	}
	require.NoError(t, db.Create(&original).Error)

	payload := map[string]any{
		"name":                  "Updated Name",
		"type":                  "discord",
		"url":                   "https://discord.com/api/webhooks/456/def",
		"template":              "minimal",
		"enabled":               false,
		"notify_proxy_hosts":    false,
		"notify_remote_servers": false,
		"notify_domains":        false,
		"notify_certs":          false,
		"notify_uptime":         false,
		"engine":                "legacy",
		"service_config":        `{"token":"client-overwrite"}`,
		"migration_state":       "failed",
		"migration_error":       "client-error",
		"legacy_url":            "https://attacker.example",
		"last_migrated_at":      "1999-01-01T00:00:00Z",
	}

	body, _ := json.Marshal(payload)
	req, _ := http.NewRequest("PUT", "/api/v1/notifications/providers/"+original.ID, bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

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

	var dbProvider models.NotificationProvider
	require.NoError(t, db.First(&dbProvider, "id = ?", original.ID).Error)
	assert.Equal(t, "Updated Name", dbProvider.Name)
	assert.Equal(t, "notify_v1", dbProvider.Engine)
	assert.Equal(t, `{"token":"server"}`, dbProvider.ServiceConfig)
	assert.Equal(t, "migrated", dbProvider.MigrationState)
	assert.Equal(t, "", dbProvider.MigrationError)
	assert.Equal(t, "discord://legacy", dbProvider.LegacyURL)
	require.NotNil(t, dbProvider.LastMigratedAt)
	assert.Equal(t, now, dbProvider.LastMigratedAt.UTC().Round(time.Second))
}

func TestNotificationProviderHandler_List_ReturnsHasTokenTrue(t *testing.T) {
	r, db := setupNotificationProviderTest(t)

	p := models.NotificationProvider{
		ID:    "tok-true",
		Name:  "Gotify With Token",
		Type:  "gotify",
		URL:   "https://gotify.example.com",
		Token: "secret-app-token",
	}
	require.NoError(t, db.Create(&p).Error)

	req, _ := http.NewRequest("GET", "/api/v1/notifications/providers", http.NoBody)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

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

	var raw []map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
	require.Len(t, raw, 1)
	assert.Equal(t, true, raw[0]["has_token"])
}

func TestNotificationProviderHandler_List_ReturnsHasTokenFalse(t *testing.T) {
	r, db := setupNotificationProviderTest(t)

	p := models.NotificationProvider{
		ID:   "tok-false",
		Name: "Discord No Token",
		Type: "discord",
		URL:  "https://discord.com/api/webhooks/123/abc",
	}
	require.NoError(t, db.Create(&p).Error)

	req, _ := http.NewRequest("GET", "/api/v1/notifications/providers", http.NoBody)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

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

	var raw []map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
	require.Len(t, raw, 1)
	assert.Equal(t, false, raw[0]["has_token"])
}

func TestNotificationProviderHandler_List_NeverExposesRawToken(t *testing.T) {
	r, db := setupNotificationProviderTest(t)

	p := models.NotificationProvider{
		ID:    "tok-hidden",
		Name:  "Secret Gotify",
		Type:  "gotify",
		URL:   "https://gotify.example.com",
		Token: "super-secret-value",
	}
	require.NoError(t, db.Create(&p).Error)

	req, _ := http.NewRequest("GET", "/api/v1/notifications/providers", http.NoBody)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	assert.NotContains(t, w.Body.String(), "super-secret-value")

	var raw []map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
	require.Len(t, raw, 1)
	_, hasTokenField := raw[0]["token"]
	assert.False(t, hasTokenField, "raw token field must not appear in JSON response")
}

func TestNotificationProviderHandler_Create_ResponseHasHasToken(t *testing.T) {
	r, _ := setupNotificationProviderTest(t)

	payload := map[string]interface{}{
		"name":     "New Gotify",
		"type":     "gotify",
		"url":      "https://gotify.example.com",
		"token":    "app-token-123",
		"template": "minimal",
	}
	body, _ := json.Marshal(payload)
	req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

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

	var raw map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
	assert.Equal(t, true, raw["has_token"])
	assert.NotContains(t, w.Body.String(), "app-token-123")
}
