package email_sender

import (
	"bufio"
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"math/big"
	"net"
	"strings"
	"testing"
	"time"
)

// startSMTPTestServer поднимает минимальный SMTP-сервер (implicit TLS) на
// случайном порту 127.0.0.1 и возвращает его адрес и канал с принятыми
// телами писем. При hang=true сервер отправляет приветствие и молчит —
// для проверки таймаутов. Сертификат генерируется один на тест и
// передаётся и серверу, и клиенту (testSender).
func startSMTPTestServer(t *testing.T, hang bool, cert tls.Certificate) (addr string, messages chan []byte) {
	t.Helper()

	ln, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{
		Certificates: []tls.Certificate{cert},
	})
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	t.Cleanup(func() { _ = ln.Close() })

	messages = make(chan []byte, 8)
	go func() {
		for {
			conn, err := ln.Accept()
			if err != nil {
				return
			}
			if hang {
				go func() {
					// Приветствие отправляем, дальше молчим.
					_, _ = conn.Write([]byte("220 hang.example.com ESMTP\r\n"))
				}()
				continue
			}
			go handleSMTPConn(conn, messages)
		}
	}()

	return ln.Addr().String(), messages
}

// handleSMTPConn обслуживает один SMTP-диалог, достаточный для net/smtp:
// EHLO, AUTH PLAIN, MAIL, RCPT, DATA, QUIT.
func handleSMTPConn(conn net.Conn, messages chan<- []byte) {
	defer conn.Close()

	r := bufio.NewReader(conn)
	write := func(s string) { _, _ = conn.Write([]byte(s)) }

	write("220 test.example.com ESMTP\r\n")
	for {
		line, err := r.ReadString('\n')
		if err != nil {
			return
		}
		cmd := strings.ToUpper(strings.TrimSpace(line))
		switch {
		case strings.HasPrefix(cmd, "EHLO"):
			write("250-test.example.com\r\n250-AUTH PLAIN\r\n250 OK\r\n")
		case strings.HasPrefix(cmd, "AUTH"):
			write("235 2.7.0 Authentication successful\r\n")
		case strings.HasPrefix(cmd, "MAIL"):
			write("250 OK\r\n")
		case strings.HasPrefix(cmd, "RCPT"):
			write("250 OK\r\n")
		case strings.HasPrefix(cmd, "DATA"):
			write("354 End data with <CR><LF>.<CR><LF>\r\n")
			var body []byte
			for {
				b, err := r.ReadBytes('\n')
				if err != nil {
					return
				}
				body = append(body, b...)
				if string(b) == ".\r\n" {
					break
				}
			}
			select {
			case messages <- body:
			default:
			}
			write("250 OK\r\n")
		case strings.HasPrefix(cmd, "QUIT"):
			write("221 Bye\r\n")
			return
		}
	}
}

// testTLSCert генерирует самоподписанный сертификат для 127.0.0.1 и
// возвращает его вместе с распарсенным x509-представлением (Leaf), чтобы
// тест мог добавить его в корни доверия клиента.
func testTLSCert(t *testing.T) tls.Certificate {
	t.Helper()

	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("generate key: %v", err)
	}
	tmpl := &x509.Certificate{
		SerialNumber:          big.NewInt(1),
		Subject:               pkix.Name{CommonName: "127.0.0.1"},
		NotBefore:             time.Now().Add(-time.Hour),
		NotAfter:              time.Now().Add(time.Hour),
		IPAddresses:           []net.IP{net.ParseIP("127.0.0.1")},
		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
		BasicConstraintsValid: true,
		IsCA:                  true,
	}
	der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
	if err != nil {
		t.Fatalf("create certificate: %v", err)
	}
	leaf, err := x509.ParseCertificate(der)
	if err != nil {
		t.Fatalf("parse certificate: %v", err)
	}
	return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key, Leaf: leaf}
}

// testSender собирает sender, доверяющий самоподписанному сертификату
// тестового SMTP-сервера (в проде NewSender использует системные корни).
func testSender(t *testing.T, addr string, timeout time.Duration, cert tls.Certificate) *sender {
	t.Helper()
	host, port := splitHostPort(t, addr)

	pool := x509.NewCertPool()
	pool.AddCert(cert.Leaf)

	return &sender{
		smtpHost:     host,
		smtpPort:     port,
		smtpUser:     "sender@example.com",
		smtpPassword: "secret",
		from:         "sender@example.com",
		fromName:     "Вечерний детектив",
		replyTo:      "support@example.com",
		timeout:      timeout,
		tlsConfig:    &tls.Config{ServerName: host, RootCAs: pool},
	}
}

func splitHostPort(t *testing.T, addr string) (host, port string) {
	t.Helper()
	host, port, err := net.SplitHostPort(addr)
	if err != nil {
		t.Fatalf("split host port: %v", err)
	}
	return host, port
}

func TestSendSuccess(t *testing.T) {
	cert := testTLSCert(t)
	addr, messages := startSMTPTestServer(t, false, cert)
	s := testSender(t, addr, 5*time.Second, cert)

	err := s.Send(context.Background(), Message{
		To:      "user@example.com",
		Subject: "Привет, детектив!",
		Body:    "Текст письма",
		HTML:    "<html><body>Текст письма</body></html>",
	})
	if err != nil {
		t.Fatalf("Send: %v", err)
	}

	select {
	case data := <-messages:
		raw := string(data)
		for _, want := range []string{
			// From с отображаемым именем (RFC 2047).
			"From: =?utf-8?q?",
			"<sender@example.com>",
			"To: user@example.com",
			"Subject: =?utf-8?q?",
			"Date: ",
			"Message-ID: <",
			"@example.com>",
			"Reply-To: support@example.com",
			"MIME-Version: 1.0",
			"Content-Type: multipart/alternative; boundary=",
			"Content-Type: text/plain; charset=utf-8",
			"Текст письма",
			"Content-Type: text/html; charset=utf-8",
			"<html><body>Текст письма</body></html>",
		} {
			if !strings.Contains(raw, want) {
				t.Errorf("письмо не содержит %q:\n%s", want, raw)
			}
		}
	case <-time.After(2 * time.Second):
		t.Fatal("письмо не получено сервером")
	}
}

func TestSendRejectsCRLFInjection(t *testing.T) {
	cert := testTLSCert(t)
	addr, messages := startSMTPTestServer(t, false, cert)
	s := testSender(t, addr, 5*time.Second, cert)

	// CRLF в адресе — попытка инъекции заголовков (Bcc и т.п.): письмо
	// должно быть отклонено до обращения к SMTP-серверу.
	err := s.Send(context.Background(), Message{
		To:      "user@example.com\r\nBcc: victim@example.com",
		Subject: "Test",
		Body:    "body",
	})
	if err == nil {
		t.Fatal("ожидалась ошибка для адреса с CRLF")
	}
	select {
	case <-messages:
		t.Fatal("письмо не должно было уйти на сервер")
	default:
	}
}

func TestSendContextCancelled(t *testing.T) {
	cert := testTLSCert(t)
	addr, _ := startSMTPTestServer(t, false, cert)
	s := testSender(t, addr, 10*time.Second, cert)

	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	start := time.Now()
	err := s.Send(ctx, Message{To: "user@example.com", Subject: "Test", Body: "body"})
	if err == nil {
		t.Fatal("ожидалась ошибка при отменённом контексте")
	}
	if elapsed := time.Since(start); elapsed > 2*time.Second {
		t.Fatalf("отмена контекста должна прерывать отправку быстро, заняло %v", elapsed)
	}
}

func TestSendTimeout(t *testing.T) {
	cert := testTLSCert(t)
	addr, _ := startSMTPTestServer(t, true, cert) // сервер молчит после приветствия
	s := testSender(t, addr, 500*time.Millisecond, cert)

	start := time.Now()
	err := s.Send(context.Background(), Message{To: "user@example.com", Subject: "Test", Body: "body"})
	if err == nil {
		t.Fatal("ожидалась ошибка таймаута")
	}
	if elapsed := time.Since(start); elapsed > 3*time.Second {
		t.Fatalf("таймаут должен сработать быстро, заняло %v", elapsed)
	}
}

func TestBuildMessage(t *testing.T) {
	// С HTML-версией — multipart/alternative.
	raw := string(buildMessage(
		"sender@example.com", "Вечерний детектив",
		"user@example.com", "Привет", "support@example.com",
		"текст", "<html><body>текст</body></html>",
	))
	for _, want := range []string{
		"From: =?utf-8?q?",
		"<sender@example.com>",
		"To: user@example.com",
		"Subject: =?utf-8?q?",
		"Date: ",
		"Message-ID: <",
		"Reply-To: support@example.com",
		"MIME-Version: 1.0",
		"Content-Type: multipart/alternative; boundary=",
		"Content-Type: text/plain; charset=utf-8",
		"текст",
		"Content-Type: text/html; charset=utf-8",
		"<html><body>текст</body></html>",
	} {
		if !strings.Contains(raw, want) {
			t.Errorf("письмо не содержит %q:\n%s", want, raw)
		}
	}

	// Без HTML-версии — только text/plain (обратная совместимость).
	raw = string(buildMessage(
		"sender@example.com", "Вечерний детектив",
		"user@example.com", "Привет", "",
		"текст", "",
	))
	for _, want := range []string{
		"Content-Type: text/plain; charset=utf-8",
		"текст",
	} {
		if !strings.Contains(raw, want) {
			t.Errorf("plain-письмо не содержит %q:\n%s", want, raw)
		}
	}
	if strings.Contains(raw, "multipart/alternative") {
		t.Errorf("plain-письмо не должно быть multipart:\n%s", raw)
	}
}

func TestBuildMessageNoFromName(t *testing.T) {
	raw := string(buildMessage(
		"sender@example.com", "",
		"user@example.com", "Привет", "",
		"текст", "",
	))
	if !strings.Contains(raw, "From: sender@example.com\r\n") {
		t.Errorf("адрес без имени отправителя должен идти без угловых скобок:\n%s", raw)
	}
}

func TestBuildMessageMessageIDDomain(t *testing.T) {
	raw := string(buildMessage(
		"evening_detective@crabs-games.art", "Вечерний детектив",
		"user@example.com", "Привет", "",
		"текст", "",
	))
	for _, want := range []string{
		"Message-ID: <",
		"@crabs-games.art>",
	} {
		if !strings.Contains(raw, want) {
			t.Errorf("письмо не содержит %q:\n%s", want, raw)
		}
	}
}

func TestSanitizeHeader(t *testing.T) {
	got := sanitizeHeader("a@b.c\r\nBcc: x@y.z\x00")
	want := "a@b.cBcc: x@y.z"
	if got != want {
		t.Errorf("sanitizeHeader = %q, want %q", got, want)
	}
}

func TestValidateMessage(t *testing.T) {
	if err := validateMessage("user@example.com", "тема"); err != nil {
		t.Errorf("валидное письмо отклонено: %v", err)
	}
	if err := validateMessage("not-an-email", "тема"); err == nil {
		t.Error("невалидный адрес принят")
	}
	if err := validateMessage("user@example.com", ""); err == nil {
		t.Error("пустая тема принята")
	}
}
