package services

import (
	"strings"
	"testing"

	"github.com/google/uuid"

	"mgit.msbls.de/m/paliad/internal/models"
)

func TestSubstitutePlaceholders(t *testing.T) {
	rec := BroadcastRecipient{
		UserID:        uuid.New(),
		Email:         "anna@hlc.com",
		DisplayName:   "Anna Beispiel",
		FirstName:     "Anna",
		RoleOnProject: "lead",
	}
	cases := []struct {
		name string
		in   string
		want string
	}{
		{"name", "Hallo {{name}}", "Hallo Anna Beispiel"},
		{"first_name", "Hi {{first_name}}!", "Hi Anna!"},
		{"role_on_project", "Du bist {{role_on_project}}.", "Du bist lead."},
		{"whitespace tolerated", "{{ first_name }}", "Anna"},
		{"unknown token passes through", "Literal {{example}} stays", "Literal {{example}} stays"},
		{"all three together",
			"{{name}} ({{first_name}}, {{role_on_project}})",
			"Anna Beispiel (Anna, lead)"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := substitutePlaceholders(tc.in, rec)
			if got != tc.want {
				t.Errorf("got %q, want %q", got, tc.want)
			}
		})
	}
}

// renderMarkdownSafe must escape raw HTML and only re-emit a small whitelist
// of tags. Any leakage of a <script> tag would be an XSS vector since the
// rendered output goes straight into an HTML email body.
func TestRenderMarkdownSafe(t *testing.T) {
	cases := []struct {
		name        string
		in          string
		wantContains []string
		wantMissing  []string
	}{
		{
			name:        "bold",
			in:          "**hallo**",
			wantContains: []string{"<strong>hallo</strong>"},
		},
		{
			name:        "italic underscore",
			in:          "_hallo_",
			wantContains: []string{"<em>hallo</em>"},
		},
		{
			name:        "link",
			in:          "[paliad](https://paliad.de)",
			wantContains: []string{`<a href="https://paliad.de">paliad</a>`},
		},
		{
			name:        "bullet list",
			in:          "- erstens\n- zweitens",
			wantContains: []string{"<ul>", "<li>erstens</li>", "<li>zweitens</li>", "</ul>"},
		},
		{
			name:        "paragraph break",
			in:          "Erste Zeile\n\nZweite Zeile",
			wantContains: []string{"<p>Erste Zeile</p>", "<p>Zweite Zeile</p>"},
		},
		{
			name:        "single newline → br",
			in:          "Zeile A\nZeile B",
			wantContains: []string{"<p>Zeile A<br>", "Zeile B</p>"},
		},
		{
			name:        "script tag escaped",
			in:          "Hallo <script>alert(1)</script>",
			wantContains: []string{"&lt;script&gt;", "&lt;/script&gt;"},
			wantMissing:  []string{"<script>", "alert(1)</script>"},
		},
		{
			name:        "link injection attempt — javascript: URL is rejected",
			in:          "[click](javascript:alert(1))",
			wantMissing:  []string{`href="javascript:`},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := renderMarkdownSafe(tc.in)
			for _, want := range tc.wantContains {
				if !strings.Contains(got, want) {
					t.Errorf("missing %q in %q", want, got)
				}
			}
			for _, miss := range tc.wantMissing {
				if strings.Contains(got, miss) {
					t.Errorf("unexpected %q in %q", miss, got)
				}
			}
		})
	}
}

func TestFirstNameExtraction(t *testing.T) {
	// senderSignature uses DisplayName directly; firstName extraction is
	// frontend-side. Smoke-test only that DisplayName placeholder lands.
	sender := models.User{
		ID:          uuid.New(),
		Email:       "max@hlc.com",
		DisplayName: "Max Mustermann",
	}
	sig := senderSignature("de", sender)
	if !strings.Contains(sig, "Max Mustermann") {
		t.Errorf("DisplayName not in signature: %q", sig)
	}
	if !strings.Contains(sig, "Gesendet von") {
		t.Errorf("DE prefix missing: %q", sig)
	}
	if !strings.Contains(sig, `mailto:max@hlc.com`) {
		t.Errorf("mailto link missing: %q", sig)
	}
	sigEN := senderSignature("en", sender)
	if !strings.Contains(sigEN, "Sent by") {
		t.Errorf("EN prefix missing: %q", sigEN)
	}
}

// TestBroadcastValidation exercises the cheap guards that fire before any
// SQL or SMTP I/O. Constructed with a nil DB so the tests don't need a
// connection string. The Send path bails out at validation before touching
// db.ExecContext.
func TestBroadcastValidation(t *testing.T) {
	mailSvc, err := NewMailService()
	if err != nil {
		t.Fatalf("NewMailService: %v", err)
	}
	svc := NewBroadcastService(nil, mailSvc, nil, nil, NewEmailTemplateService(nil))

	cases := []struct {
		name string
		in   BroadcastInput
		want error
	}{
		{
			name: "empty subject",
			in:   BroadcastInput{Subject: "", Body: "x", Recipients: oneRec()},
			want: ErrBroadcastEmptySubject,
		},
		{
			name: "empty body",
			in:   BroadcastInput{Subject: "Hi", Body: "  ", Recipients: oneRec()},
			want: ErrBroadcastEmptyBody,
		},
		{
			name: "no recipients",
			in:   BroadcastInput{Subject: "Hi", Body: "x", Recipients: nil},
			want: ErrBroadcastNoRecipients,
		},
		{
			name: "too many recipients",
			in:   BroadcastInput{Subject: "Hi", Body: "x", Recipients: nRecipients(BroadcastRecipientCap + 1)},
			want: ErrBroadcastTooManyRecipients,
		},
		{
			name: "invalid email",
			in: BroadcastInput{
				Subject: "Hi",
				Body:    "x",
				Recipients: []BroadcastRecipient{{
					UserID: uuid.New(),
					Email:  "not-an-email",
				}},
			},
			want: ErrBroadcastInvalidEmail,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			_, err := svc.Send(t.Context(), uuid.New(), tc.in)
			if err == nil {
				t.Fatal("expected error, got nil")
			}
			// Use errors.Is so wrapped errors still match.
			if !errorIs(err, tc.want) {
				t.Errorf("got %v, want %v", err, tc.want)
			}
		})
	}
}

// errorIs is a tiny shim so the test file doesn't need to import "errors".
// (Imports are kept terse on purpose — see existing test files.)
func errorIs(have, want error) bool {
	if have == want {
		return true
	}
	if have == nil || want == nil {
		return false
	}
	// Fall back to message-level matching for fmt.Errorf %w wraps.
	return strings.Contains(have.Error(), want.Error())
}

func oneRec() []BroadcastRecipient {
	return []BroadcastRecipient{{
		UserID:      uuid.New(),
		Email:       "anna@hlc.com",
		DisplayName: "Anna",
		FirstName:   "Anna",
	}}
}

func nRecipients(n int) []BroadcastRecipient {
	out := make([]BroadcastRecipient, 0, n)
	for i := 0; i < n; i++ {
		out = append(out, BroadcastRecipient{
			UserID:      uuid.New(),
			Email:       "user@hlc.com",
			DisplayName: "User",
			FirstName:   "User",
		})
	}
	return out
}
