package notifications

import (
	"testing"
	"time"

	"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
)

func TestAnnotateResolvedMetadata(t *testing.T) {
	tests := []struct {
		name       string
		alert      *alerts.Alert
		resolvedAt time.Time
		checkFn    func(*testing.T, *alerts.Alert)
	}{
		{
			name:       "nil alert",
			alert:      nil,
			resolvedAt: time.Now(),
			checkFn: func(t *testing.T, a *alerts.Alert) {
				// Should not panic, nothing to check
			},
		},
		{
			name:       "alert with nil metadata",
			alert:      &alerts.Alert{ID: "test-1"},
			resolvedAt: time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC),
			checkFn: func(t *testing.T, a *alerts.Alert) {
				if a.Metadata == nil {
					t.Error("Metadata should be initialized")
					return
				}
				raw, ok := a.Metadata[metadataResolvedAt]
				if !ok {
					t.Error("resolvedAt key should be set")
					return
				}
				ts, ok := raw.(string)
				if !ok {
					t.Errorf("resolvedAt should be string, got %T", raw)
					return
				}
				expected := "2025-01-15T10:30:00Z"
				if ts != expected {
					t.Errorf("resolvedAt = %q, want %q", ts, expected)
				}
			},
		},
		{
			name: "alert with existing metadata",
			alert: &alerts.Alert{
				ID: "test-2",
				Metadata: map[string]interface{}{
					"existingKey": "existingValue",
				},
			},
			resolvedAt: time.Date(2025, 6, 20, 15, 45, 30, 0, time.UTC),
			checkFn: func(t *testing.T, a *alerts.Alert) {
				// Should preserve existing metadata
				if v, ok := a.Metadata["existingKey"]; !ok || v != "existingValue" {
					t.Error("existing metadata should be preserved")
				}
				// Should add resolvedAt
				raw, ok := a.Metadata[metadataResolvedAt]
				if !ok {
					t.Error("resolvedAt key should be set")
					return
				}
				ts, ok := raw.(string)
				if !ok {
					t.Errorf("resolvedAt should be string, got %T", raw)
					return
				}
				expected := "2025-06-20T15:45:30Z"
				if ts != expected {
					t.Errorf("resolvedAt = %q, want %q", ts, expected)
				}
			},
		},
		{
			name: "overwrites existing resolvedAt",
			alert: &alerts.Alert{
				ID: "test-3",
				Metadata: map[string]interface{}{
					metadataResolvedAt: "old-value",
				},
			},
			resolvedAt: time.Date(2025, 12, 1, 0, 0, 0, 0, time.UTC),
			checkFn: func(t *testing.T, a *alerts.Alert) {
				raw := a.Metadata[metadataResolvedAt]
				ts, ok := raw.(string)
				if !ok {
					t.Errorf("resolvedAt should be string, got %T", raw)
					return
				}
				expected := "2025-12-01T00:00:00Z"
				if ts != expected {
					t.Errorf("resolvedAt = %q, want %q (should overwrite old value)", ts, expected)
				}
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			annotateResolvedMetadata(tc.alert, tc.resolvedAt)
			tc.checkFn(t, tc.alert)
		})
	}
}

func TestResolveAppriseNotificationType(t *testing.T) {
	tests := []struct {
		name     string
		alerts   []*alerts.Alert
		expected string
	}{
		{
			name:     "nil slice",
			alerts:   nil,
			expected: "info",
		},
		{
			name:     "empty slice",
			alerts:   []*alerts.Alert{},
			expected: "info",
		},
		{
			name:     "slice with nil alert",
			alerts:   []*alerts.Alert{nil},
			expected: "info",
		},
		{
			name:     "slice with multiple nil alerts",
			alerts:   []*alerts.Alert{nil, nil, nil},
			expected: "info",
		},
		{
			name: "single info-level alert (no level set)",
			alerts: []*alerts.Alert{
				{ID: "test-1", Level: ""},
			},
			expected: "info",
		},
		{
			name: "single warning alert",
			alerts: []*alerts.Alert{
				{ID: "test-1", Level: alerts.AlertLevelWarning},
			},
			expected: "warning",
		},
		{
			name: "single critical alert",
			alerts: []*alerts.Alert{
				{ID: "test-1", Level: alerts.AlertLevelCritical},
			},
			expected: "failure",
		},
		{
			name: "multiple warnings",
			alerts: []*alerts.Alert{
				{ID: "test-1", Level: alerts.AlertLevelWarning},
				{ID: "test-2", Level: alerts.AlertLevelWarning},
			},
			expected: "warning",
		},
		{
			name: "warning and critical - returns failure (critical takes priority)",
			alerts: []*alerts.Alert{
				{ID: "test-1", Level: alerts.AlertLevelWarning},
				{ID: "test-2", Level: alerts.AlertLevelCritical},
			},
			expected: "failure",
		},
		{
			name: "critical first - returns failure immediately",
			alerts: []*alerts.Alert{
				{ID: "test-1", Level: alerts.AlertLevelCritical},
				{ID: "test-2", Level: alerts.AlertLevelWarning},
			},
			expected: "failure",
		},
		{
			name: "mixed with nil - critical takes priority",
			alerts: []*alerts.Alert{
				nil,
				{ID: "test-1", Level: alerts.AlertLevelWarning},
				nil,
				{ID: "test-2", Level: alerts.AlertLevelCritical},
			},
			expected: "failure",
		},
		{
			name: "info and warning - warning takes priority",
			alerts: []*alerts.Alert{
				{ID: "test-1", Level: ""},
				{ID: "test-2", Level: alerts.AlertLevelWarning},
				{ID: "test-3", Level: ""},
			},
			expected: "warning",
		},
		{
			name: "unknown level treated as info",
			alerts: []*alerts.Alert{
				{ID: "test-1", Level: "unknown"},
			},
			expected: "info",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := resolveAppriseNotificationType(tc.alerts)
			if result != tc.expected {
				t.Errorf("resolveAppriseNotificationType() = %q, want %q", result, tc.expected)
			}
		})
	}
}

func TestNormalizeQueueType(t *testing.T) {
	tests := []struct {
		name          string
		notifType     string
		expectedType  string
		expectedEvent notificationEvent
	}{
		{
			name:          "email type",
			notifType:     "email",
			expectedType:  "email",
			expectedEvent: eventAlert,
		},
		{
			name:          "webhook type",
			notifType:     "webhook",
			expectedType:  "webhook",
			expectedEvent: eventAlert,
		},
		{
			name:          "apprise type",
			notifType:     "apprise",
			expectedType:  "apprise",
			expectedEvent: eventAlert,
		},
		{
			name:          "email_resolved type",
			notifType:     "email_resolved",
			expectedType:  "email",
			expectedEvent: eventResolved,
		},
		{
			name:          "webhook_resolved type",
			notifType:     "webhook_resolved",
			expectedType:  "webhook",
			expectedEvent: eventResolved,
		},
		{
			name:          "apprise_resolved type",
			notifType:     "apprise_resolved",
			expectedType:  "apprise",
			expectedEvent: eventResolved,
		},
		{
			name:          "empty type",
			notifType:     "",
			expectedType:  "",
			expectedEvent: eventAlert,
		},
		{
			name:          "unknown type",
			notifType:     "unknown",
			expectedType:  "unknown",
			expectedEvent: eventAlert,
		},
		{
			name:          "unknown_resolved type",
			notifType:     "unknown_resolved",
			expectedType:  "unknown",
			expectedEvent: eventResolved,
		},
		{
			name:          "type with _resolved in middle - not stripped",
			notifType:     "_resolved_email",
			expectedType:  "_resolved_email",
			expectedEvent: eventAlert,
		},
		{
			name:          "just _resolved suffix",
			notifType:     "_resolved",
			expectedType:  "",
			expectedEvent: eventResolved,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			gotType, gotEvent := normalizeQueueType(tc.notifType)
			if gotType != tc.expectedType {
				t.Errorf("normalizeQueueType() type = %q, want %q", gotType, tc.expectedType)
			}
			if gotEvent != tc.expectedEvent {
				t.Errorf("normalizeQueueType() event = %q, want %q", gotEvent, tc.expectedEvent)
			}
		})
	}
}

func TestResolvedTimeFromAlerts(t *testing.T) {
	fixedTime := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)
	fixedTimeStr := fixedTime.Format(time.RFC3339)

	tests := []struct {
		name     string
		alerts   []*alerts.Alert
		checkFn  func(*testing.T, time.Time)
	}{
		{
			name:   "nil slice - returns current time",
			alerts: nil,
			checkFn: func(t *testing.T, result time.Time) {
				// Should return a time close to now
				if time.Since(result) > time.Second {
					t.Error("expected time close to now for nil slice")
				}
			},
		},
		{
			name:   "empty slice - returns current time",
			alerts: []*alerts.Alert{},
			checkFn: func(t *testing.T, result time.Time) {
				if time.Since(result) > time.Second {
					t.Error("expected time close to now for empty slice")
				}
			},
		},
		{
			name:   "slice with nil alert - returns current time",
			alerts: []*alerts.Alert{nil},
			checkFn: func(t *testing.T, result time.Time) {
				if time.Since(result) > time.Second {
					t.Error("expected time close to now for nil alert")
				}
			},
		},
		{
			name: "alert with nil metadata - returns current time",
			alerts: []*alerts.Alert{
				{ID: "test-1", Metadata: nil},
			},
			checkFn: func(t *testing.T, result time.Time) {
				if time.Since(result) > time.Second {
					t.Error("expected time close to now for nil metadata")
				}
			},
		},
		{
			name: "alert without resolvedAt key - returns current time",
			alerts: []*alerts.Alert{
				{ID: "test-1", Metadata: map[string]interface{}{"other": "value"}},
			},
			checkFn: func(t *testing.T, result time.Time) {
				if time.Since(result) > time.Second {
					t.Error("expected time close to now for missing resolvedAt")
				}
			},
		},
		{
			name: "alert with string resolvedAt (RFC3339)",
			alerts: []*alerts.Alert{
				{
					ID: "test-1",
					Metadata: map[string]interface{}{
						metadataResolvedAt: fixedTimeStr,
					},
				},
			},
			checkFn: func(t *testing.T, result time.Time) {
				if !result.Equal(fixedTime) {
					t.Errorf("got %v, want %v", result, fixedTime)
				}
			},
		},
		{
			name: "alert with float64 resolvedAt (Unix timestamp)",
			alerts: []*alerts.Alert{
				{
					ID: "test-1",
					Metadata: map[string]interface{}{
						metadataResolvedAt: float64(fixedTime.Unix()),
					},
				},
			},
			checkFn: func(t *testing.T, result time.Time) {
				// Unix timestamp loses nanoseconds
				expected := time.Unix(fixedTime.Unix(), 0)
				if !result.Equal(expected) {
					t.Errorf("got %v, want %v", result, expected)
				}
			},
		},
		{
			name: "alert with zero float64 - returns current time",
			alerts: []*alerts.Alert{
				{
					ID: "test-1",
					Metadata: map[string]interface{}{
						metadataResolvedAt: float64(0),
					},
				},
			},
			checkFn: func(t *testing.T, result time.Time) {
				if time.Since(result) > time.Second {
					t.Error("expected time close to now for zero timestamp")
				}
			},
		},
		{
			name: "alert with negative float64 - returns current time",
			alerts: []*alerts.Alert{
				{
					ID: "test-1",
					Metadata: map[string]interface{}{
						metadataResolvedAt: float64(-1000),
					},
				},
			},
			checkFn: func(t *testing.T, result time.Time) {
				if time.Since(result) > time.Second {
					t.Error("expected time close to now for negative timestamp")
				}
			},
		},
		{
			name: "alert with invalid string format - returns current time",
			alerts: []*alerts.Alert{
				{
					ID: "test-1",
					Metadata: map[string]interface{}{
						metadataResolvedAt: "not-a-timestamp",
					},
				},
			},
			checkFn: func(t *testing.T, result time.Time) {
				if time.Since(result) > time.Second {
					t.Error("expected time close to now for invalid string")
				}
			},
		},
		{
			name: "alert with unsupported type - returns current time",
			alerts: []*alerts.Alert{
				{
					ID: "test-1",
					Metadata: map[string]interface{}{
						metadataResolvedAt: 12345, // int, not float64
					},
				},
			},
			checkFn: func(t *testing.T, result time.Time) {
				if time.Since(result) > time.Second {
					t.Error("expected time close to now for unsupported type")
				}
			},
		},
		{
			name: "multiple alerts - returns first valid resolvedAt",
			alerts: []*alerts.Alert{
				nil,
				{ID: "test-1", Metadata: nil},
				{ID: "test-2", Metadata: map[string]interface{}{}},
				{
					ID: "test-3",
					Metadata: map[string]interface{}{
						metadataResolvedAt: fixedTimeStr,
					},
				},
				{
					ID: "test-4",
					Metadata: map[string]interface{}{
						metadataResolvedAt: "2024-01-01T00:00:00Z", // should not be reached
					},
				},
			},
			checkFn: func(t *testing.T, result time.Time) {
				if !result.Equal(fixedTime) {
					t.Errorf("got %v, want %v (first valid)", result, fixedTime)
				}
			},
		},
		{
			name: "first alert has valid resolvedAt",
			alerts: []*alerts.Alert{
				{
					ID: "test-1",
					Metadata: map[string]interface{}{
						metadataResolvedAt: fixedTimeStr,
					},
				},
			},
			checkFn: func(t *testing.T, result time.Time) {
				if !result.Equal(fixedTime) {
					t.Errorf("got %v, want %v", result, fixedTime)
				}
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := resolvedTimeFromAlerts(tc.alerts)
			tc.checkFn(t, result)
		})
	}
}

// Test the event constants
func TestNotificationEventConstants(t *testing.T) {
	if eventAlert != "alert" {
		t.Errorf("eventAlert = %q, want %q", eventAlert, "alert")
	}
	if eventResolved != "resolved" {
		t.Errorf("eventResolved = %q, want %q", eventResolved, "resolved")
	}
}

// Test the queue type suffix constant
func TestQueueTypeSuffixConstant(t *testing.T) {
	if queueTypeSuffixResolved != "_resolved" {
		t.Errorf("queueTypeSuffixResolved = %q, want %q", queueTypeSuffixResolved, "_resolved")
	}
}

// Test the metadata key constant
func TestMetadataKeyConstant(t *testing.T) {
	if metadataResolvedAt != "resolvedAt" {
		t.Errorf("metadataResolvedAt = %q, want %q", metadataResolvedAt, "resolvedAt")
	}
}

func TestCopyEmailConfig(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
		cfg  EmailConfig
	}{
		{
			name: "empty config",
			cfg:  EmailConfig{},
		},
		{
			name: "config with empty To slice",
			cfg: EmailConfig{
				Enabled:  true,
				SMTPHost: "smtp.example.com",
				SMTPPort: 587,
				From:     "sender@example.com",
				To:       []string{},
			},
		},
		{
			name: "config with single recipient",
			cfg: EmailConfig{
				Enabled:  true,
				SMTPHost: "smtp.example.com",
				SMTPPort: 465,
				Username: "user",
				Password: "pass",
				From:     "sender@example.com",
				To:       []string{"recipient@example.com"},
			},
		},
		{
			name: "config with multiple recipients",
			cfg: EmailConfig{
				Enabled:  true,
				SMTPHost: "mail.company.org",
				SMTPPort: 25,
				From:     "alerts@company.org",
				To:       []string{"admin@company.org", "ops@company.org", "devops@company.org"},
				TLS:      true,
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			copied := copyEmailConfig(tc.cfg)

			// Verify fields are equal
			if copied.Enabled != tc.cfg.Enabled {
				t.Errorf("Enabled = %v, want %v", copied.Enabled, tc.cfg.Enabled)
			}
			if copied.SMTPHost != tc.cfg.SMTPHost {
				t.Errorf("SMTPHost = %q, want %q", copied.SMTPHost, tc.cfg.SMTPHost)
			}
			if copied.SMTPPort != tc.cfg.SMTPPort {
				t.Errorf("SMTPPort = %d, want %d", copied.SMTPPort, tc.cfg.SMTPPort)
			}
			if copied.From != tc.cfg.From {
				t.Errorf("From = %q, want %q", copied.From, tc.cfg.From)
			}
			if len(copied.To) != len(tc.cfg.To) {
				t.Errorf("To length = %d, want %d", len(copied.To), len(tc.cfg.To))
			}

			// Verify slice independence (if original has elements)
			if len(tc.cfg.To) > 0 {
				originalTo := tc.cfg.To[0]
				copied.To[0] = "modified@example.com"
				if tc.cfg.To[0] != originalTo {
					t.Error("Modifying copied.To should not affect original")
				}
			}
		})
	}
}

func TestCopyWebhookConfigs(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		webhooks []WebhookConfig
		wantNil  bool
	}{
		{
			name:     "nil input",
			webhooks: nil,
			wantNil:  true,
		},
		{
			name:     "empty slice",
			webhooks: []WebhookConfig{},
			wantNil:  true,
		},
		{
			name: "single webhook without maps",
			webhooks: []WebhookConfig{
				{
					Enabled: true,
					URL:     "https://hooks.example.com/webhook",
					Method:  "POST",
				},
			},
		},
		{
			name: "webhook with headers",
			webhooks: []WebhookConfig{
				{
					Enabled: true,
					URL:     "https://api.example.com/alerts",
					Method:  "POST",
					Headers: map[string]string{
						"Authorization": "Bearer token123",
						"Content-Type":  "application/json",
					},
				},
			},
		},
		{
			name: "webhook with custom fields",
			webhooks: []WebhookConfig{
				{
					Enabled: true,
					URL:     "https://pushover.net/api",
					CustomFields: map[string]string{
						"priority": "1",
						"sound":    "alarm",
					},
				},
			},
		},
		{
			name: "multiple webhooks with all fields",
			webhooks: []WebhookConfig{
				{
					Enabled: true,
					URL:     "https://discord.com/api/webhooks/123",
					Headers: map[string]string{"X-Custom": "value"},
					CustomFields: map[string]string{"key": "val"},
				},
				{
					Enabled: false,
					URL:     "https://slack.com/api/post",
					Method:  "POST",
					Headers: map[string]string{"Authorization": "xoxb-token"},
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			copied := copyWebhookConfigs(tc.webhooks)

			if tc.wantNil {
				if copied != nil {
					t.Errorf("Expected nil, got %v", copied)
				}
				return
			}

			if len(copied) != len(tc.webhooks) {
				t.Errorf("Length = %d, want %d", len(copied), len(tc.webhooks))
				return
			}

			// Verify each webhook
			for i := range tc.webhooks {
				if copied[i].URL != tc.webhooks[i].URL {
					t.Errorf("[%d] URL = %q, want %q", i, copied[i].URL, tc.webhooks[i].URL)
				}
				if copied[i].Enabled != tc.webhooks[i].Enabled {
					t.Errorf("[%d] Enabled = %v, want %v", i, copied[i].Enabled, tc.webhooks[i].Enabled)
				}

				// Verify headers independence
				if len(tc.webhooks[i].Headers) > 0 {
					for k := range tc.webhooks[i].Headers {
						originalVal := tc.webhooks[i].Headers[k]
						copied[i].Headers[k] = "modified"
						if tc.webhooks[i].Headers[k] != originalVal {
							t.Errorf("[%d] Modifying Headers should not affect original", i)
						}
						break // Test one key is enough
					}
				}

				// Verify custom fields independence
				if len(tc.webhooks[i].CustomFields) > 0 {
					for k := range tc.webhooks[i].CustomFields {
						originalVal := tc.webhooks[i].CustomFields[k]
						copied[i].CustomFields[k] = "modified"
						if tc.webhooks[i].CustomFields[k] != originalVal {
							t.Errorf("[%d] Modifying CustomFields should not affect original", i)
						}
						break
					}
				}
			}
		})
	}
}

func TestCopyAppriseConfig(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
		cfg  AppriseConfig
	}{
		{
			name: "empty config",
			cfg:  AppriseConfig{},
		},
		{
			name: "config with empty targets",
			cfg: AppriseConfig{
				Enabled: true,
				Targets: []string{},
			},
		},
		{
			name: "config with single target",
			cfg: AppriseConfig{
				Enabled: true,
				Targets: []string{"discord://webhook/id/token"},
			},
		},
		{
			name: "config with multiple targets",
			cfg: AppriseConfig{
				Enabled: true,
				Targets: []string{
					"slack://token/channel",
					"telegram://bot_token/chat_id",
					"email://user:pass@smtp.example.com",
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			copied := copyAppriseConfig(tc.cfg)

			if copied.Enabled != tc.cfg.Enabled {
				t.Errorf("Enabled = %v, want %v", copied.Enabled, tc.cfg.Enabled)
			}
			if len(copied.Targets) != len(tc.cfg.Targets) {
				t.Errorf("Targets length = %d, want %d", len(copied.Targets), len(tc.cfg.Targets))
			}

			// Verify slice independence
			if len(tc.cfg.Targets) > 0 {
				originalTarget := tc.cfg.Targets[0]
				copied.Targets[0] = "modified://target"
				if tc.cfg.Targets[0] != originalTarget {
					t.Error("Modifying copied.Targets should not affect original")
				}
			}
		})
	}
}

func TestBuildNotificationTestAlert(t *testing.T) {
	t.Parallel()

	alert := buildNotificationTestAlert()

	// Verify required fields are set
	if alert.ID != "test-alert" {
		t.Errorf("ID = %q, want %q", alert.ID, "test-alert")
	}
	if alert.Type != "cpu" {
		t.Errorf("Type = %q, want %q", alert.Type, "cpu")
	}
	if alert.Level != "warning" {
		t.Errorf("Level = %q, want %q", alert.Level, "warning")
	}
	if alert.ResourceID != "test-resource" {
		t.Errorf("ResourceID = %q, want %q", alert.ResourceID, "test-resource")
	}
	if alert.ResourceName != "Test Resource" {
		t.Errorf("ResourceName = %q, want %q", alert.ResourceName, "Test Resource")
	}
	if alert.Node == "" {
		t.Error("Node should not be empty")
	}
	if alert.Instance == "" {
		t.Error("Instance should not be empty")
	}
	if alert.Message == "" {
		t.Error("Message should not be empty")
	}
	if alert.Value == 0 {
		t.Error("Value should not be zero")
	}
	if alert.Threshold == 0 {
		t.Error("Threshold should not be zero")
	}
	if alert.StartTime.IsZero() {
		t.Error("StartTime should not be zero")
	}
	if alert.LastSeen.IsZero() {
		t.Error("LastSeen should not be zero")
	}
	if alert.Metadata == nil {
		t.Error("Metadata should not be nil")
	}

	// Verify StartTime is in the past (shows alert has been active)
	if !alert.StartTime.Before(time.Now()) {
		t.Error("StartTime should be in the past")
	}

	// Verify metadata contains resourceType
	if rt, ok := alert.Metadata["resourceType"]; !ok || rt != "vm" {
		t.Errorf("Metadata[resourceType] = %v, want %q", rt, "vm")
	}
}
