package handlers

// P1 #4 回归测试：Web 写信的邮件头不可被 CRLF 注入。
// 旧实现把 to/cc/subject/附件名原样拼进 MIME 头，攻击者可通过
// subject 注入 Reply-To/Bcc 等任意头用于钓鱼。

import (
	"strings"
	"testing"
)

func TestSanitizeHeaderField(t *testing.T) {
	cases := []struct {
		in, want string
	}{
		{"normal value", "normal value"},
		{"with\r\ninjected: header", "withinjected: header"},
		{"lf\nonly", "lfonly"},
		{"cr\ronly", "cronly"},
		{"nul\x00byte", "nulbyte"},
		{"mixed\r\n\x00all", "mixedall"},
		{"中文主题", "中文主题"},
	}
	for _, tc := range cases {
		if got := sanitizeHeaderField(tc.in); got != tc.want {
			t.Errorf("sanitizeHeaderField(%q) = %q, want %q", tc.in, got, tc.want)
		}
	}
}

func TestBuildOutgoingMessageBlocksHeaderInjection(t *testing.T) {
	_, raw := buildOutgoingMessage(
		"alice@example.com",
		"bob@example.com\r\nBcc: victim@evil.com",
		"carol@example.com\r\nReply-To: attacker@evil.com",
		"Hi\r\nBcc: victim@evil.com\r\nReply-To: attacker@evil.com",
		"body",
		"",
		nil,
	)

	// 注入的头不允许以独立头形式出现
	for _, injected := range []string{
		"Bcc:", "Reply-To:",
	} {
		if strings.Contains(raw, "\r\n"+injected) || strings.HasPrefix(raw, injected) {
			t.Fatalf("injected header %q found in message:\n%s", injected, raw)
		}
	}

	// 注入的邮箱地址本身允许以折叠形式残留在原头值中，
	// 但绝不能成为独立的一行头。
	lines := strings.Split(raw, "\r\n")
	for _, line := range lines[1:] { // 跳过 From
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "Bcc:") || strings.HasPrefix(trimmed, "Reply-To:") {
			t.Fatalf("injected header line %q found in message:\n%s", line, raw)
		}
	}
}

func TestBuildOutgoingMessageAttFilenameInjection(t *testing.T) {
	atts := []pendingAttachment{
		{filename: "evil.png\r\nBcc: victim@evil.com", contentType: "image/png", data: []byte("x")},
		{filename: `quote".png`, contentType: "image/png", data: []byte("x")},
	}
	_, raw := buildOutgoingMessage("a@b.com", "c@d.com", "", "t", "body", "", atts)

	lines := strings.Split(raw, "\r\n")
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "Bcc:") {
			t.Fatalf("filename header injection found: %q\nmessage:\n%s", line, raw)
		}
	}

	// 含引号/换行的文件名必须被正确编码，不能破坏头结构
	if !strings.Contains(raw, "Content-Disposition: attachment;") {
		t.Fatalf("Content-Disposition missing in message:\n%s", raw)
	}
}

func TestBuildOutgoingMessageEncodesNonASCIISubject(t *testing.T) {
	_, raw := buildOutgoingMessage("a@b.com", "c@d.com", "", "中文主题测试", "body", "", nil)
	// 非 ASCII 主题应按 RFC 2047 编码为 =?utf-8?...?= 形式
	if !strings.Contains(raw, "Subject: =?utf-8?") && !strings.Contains(raw, "Subject: =?UTF-8?") {
		t.Fatalf("non-ASCII subject should be RFC 2047 encoded, got:\n%s", raw)
	}
	// 头部不应再包含裸中文（应被编码）
	for _, line := range strings.Split(raw, "\r\n") {
		if strings.HasPrefix(line, "Subject:") && strings.ContainsAny(line, "中文测试") {
			t.Fatalf("raw non-ASCII in Subject header: %q", line)
		}
	}
}

func TestFormatContentDisposition(t *testing.T) {
	if got := formatContentDisposition("report.pdf"); got != "attachment; filename=report.pdf" {
		t.Fatalf("simple filename: got %q", got)
	}
	// 特殊字符需要安全编码而不是原样嵌入
	got := formatContentDisposition("a\"b\\c\r\nd.png")
	if strings.ContainsAny(got, "\r\n") {
		t.Fatalf("CRLF leaked into Content-Disposition: %q", got)
	}
}

// P3 #12：Referer 开放重定向防护。
func TestSafeRedirectPath(t *testing.T) {
	cases := []struct {
		in   string
		want string
	}{
		{"", ""},
		{"/inbox", "/inbox"},
		{"/mail/delete/5", "/mail/delete/5"},
		{"/sent?page=2", "/sent?page=2"},
		{"https://evil.com/", ""},
		{"//evil.com/inbox", ""},
		{"http://mail.lmve.net/inbox", ""},
		{"javascript:alert(1)", ""},
		{"/\\evil.com", "/\\evil.com"}, // 浏览器对 /\\ 的处理不一致，但不涉及外部协议跳转
	}
	for _, tc := range cases {
		if got := safeRedirectPath(tc.in); got != tc.want {
			t.Errorf("safeRedirectPath(%q) = %q, want %q", tc.in, got, tc.want)
		}
	}
}
