//go:build unit

package email

import (
	"bufio"
	"context"
	"io"
	"net"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/pocket-id/pocket-id/backend/internal/appconfig"
	"github.com/pocket-id/pocket-id/backend/internal/apperror"
	"github.com/pocket-id/pocket-id/backend/internal/model"
	testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
)

func TestNewLoadsEveryEmailTemplate(t *testing.T) {
	module, err := New(nil)
	require.NoError(t, err)
	require.Len(t, module.textTemplates, len(templatePaths))
	require.Len(t, module.htmlTemplates, len(templatePaths))

	for _, templatePath := range templatePaths {
		assert.NotNil(t, module.textTemplates[templatePath])
		assert.NotNil(t, module.htmlTemplates[templatePath])
	}
}

func TestModuleSendsEveryEmailType(t *testing.T) {
	db := testutils.NewDatabaseForTest(t)
	userEmail := "recipient@example.test"
	user := model.User{
		Base:      model.Base{ID: "email-recipient"},
		Username:  "email-recipient",
		Email:     &userEmail,
		FirstName: "Test",
		LastName:  "User",
	}
	require.NoError(t, db.Create(&user).Error)

	module, err := New(db)
	require.NoError(t, err)

	eventTime := time.Date(2030, time.January, 2, 15, 4, 5, 0, time.UTC)
	tests := []struct {
		name         string
		subject      string
		bodyContains []string
		send         func(ctx context.Context, config *appconfig.AppConfigModel) error
	}{
		{
			name:         "test email",
			subject:      "Test email",
			bodyContains: []string{"TEST EMAIL", "Your email setup is working correctly!"},
			send: func(ctx context.Context, config *appconfig.AppConfigModel) error {
				return module.SendTestEmail(ctx, config, user.ID)
			},
		},
		{
			name:         "email verification",
			subject:      "Verify your Pocket ID Test email address",
			bodyContains: []string{"EMAIL VERIFICATION", "Hello Test User", "https://id.example.test/verify-token"},
			send: func(ctx context.Context, config *appconfig.AppConfigModel) error {
				return module.SendEmailVerification(ctx, config, user.FullName(), userEmail, "https://id.example.test/verify-token")
			},
		},
		{
			name:         "one-time access",
			subject:      "Login Code",
			bodyContains: []string{"YOUR LOGIN CODE", "123456", "https://id.example.test/lc/123456", "15 minutes"},
			send: func(ctx context.Context, config *appconfig.AppConfigModel) error {
				return module.SendOneTimeAccessEmail(ctx, config, user.FullName(), userEmail, "123456", "https://id.example.test/lc", "https://id.example.test/lc/123456", "15 minutes")
			},
		},
		{
			name:         "new login",
			subject:      "New device login with Pocket ID Test",
			bodyContains: []string{"NEW SIGN-IN DETECTED", "Zurich, Switzerland", "192.0.2.10", "Firefox on Linux", "January 2, 2030 at 3:04 PM UTC"},
			send: func(ctx context.Context, config *appconfig.AppConfigModel) error {
				return module.SendNewLogin(ctx, config, user.FullName(), userEmail, "192.0.2.10", "Switzerland", "Zurich", "Firefox on Linux", eventTime)
			},
		},
		{
			name:         "API key expiration",
			subject:      `API Key "Automation" Expiring Soon`,
			bodyContains: []string{"API KEY EXPIRING SOON", "Hello Test", "Automation", "2030-01-02 15:04:05 UTC"},
			send: func(ctx context.Context, config *appconfig.AppConfigModel) error {
				return module.SendAPIKeyExpiringSoon(ctx, config, user.FullName(), userEmail, user.FirstName, "Automation", eventTime)
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			// Use the real SMTP transport so the test covers module mapping, rendering, MIME generation, and delivery together
			server := newSMTPTestServer(t)
			config := newSMTPTestConfig(t, server.address())

			require.NoError(t, test.send(t.Context(), config))

			session, sessionErr := server.wait()
			require.NoError(t, sessionErr)
			assert.Equal(t, "<sender@example.test>", session.mailFrom)
			assert.Equal(t, "<recipient@example.test>", session.rcptTo)
			assert.Contains(t, session.message, "From: Pocket ID Test <sender@example.test>\r\n")
			assert.Contains(t, session.message, "To: Test User <recipient@example.test>\r\n")
			assert.Contains(t, session.message, "Subject: "+test.subject+"\r\n")
			assert.Contains(t, session.message, "Content-Type: multipart/alternative; boundary=")
			assert.Contains(t, session.message, "Content-Type: text/plain; charset=UTF-8")
			assert.Contains(t, session.message, "Content-Type: text/html; charset=UTF-8")
			for _, expected := range test.bodyContains {
				assert.Contains(t, session.message, expected)
			}
		})
	}
}

func TestSendTestEmailRequiresUserEmail(t *testing.T) {
	db := testutils.NewDatabaseForTest(t)
	user := model.User{
		Base:     model.Base{ID: "user-without-email"},
		Username: "user-without-email",
	}
	require.NoError(t, db.Create(&user).Error)

	module, err := New(db)
	require.NoError(t, err)

	err = module.SendTestEmail(t.Context(), &appconfig.AppConfigModel{}, user.ID)
	require.True(t, apperror.IsCode(err, apperror.CodeUserEmailNotSet))
}

func TestSendTestEmailRejectsMissingUser(t *testing.T) {
	module, err := New(testutils.NewDatabaseForTest(t))
	require.NoError(t, err)

	err = module.SendTestEmail(t.Context(), &appconfig.AppConfigModel{}, "missing-user")

	require.True(t, apperror.IsCode(err, apperror.CodeUserNotFound))
}

func TestSMTPConnStringPreservesConfiguration(t *testing.T) {
	config := &appconfig.AppConfigModel{
		AppName:            "Pocket ID Test",
		SmtpHost:           "smtp.example.test",
		SmtpPort:           "2525",
		SmtpFrom:           "sender@example.test",
		SmtpUser:           "mailer",
		SmtpPassword:       "secret",
		SmtpTls:            "starttls",
		SmtpSkipCertVerify: "true",
	}

	connectionString, err := smtpConnString(config)
	require.NoError(t, err)

	smtpURL, err := url.Parse(connectionString)
	require.NoError(t, err)
	assert.Equal(t, "smtp", smtpURL.Scheme)
	assert.Equal(t, "smtp.example.test:2525", smtpURL.Host)
	assert.Equal(t, "mailer", smtpURL.User.Username())
	password, hasPassword := smtpURL.User.Password()
	assert.True(t, hasPassword)
	assert.Equal(t, "secret", password)
	assert.Equal(t, "sender@example.test", smtpURL.Query().Get("fromAddress"))
	assert.Equal(t, "Pocket ID Test", smtpURL.Query().Get("fromName"))
	assert.Equal(t, "starttls", smtpURL.Query().Get("tls"))
	assert.Equal(t, "true", smtpURL.Query().Get("insecureSkipVerify"))
}

func TestSMTPConnStringRequiresHostAndDefaultsTLS(t *testing.T) {
	_, err := smtpConnString(&appconfig.AppConfigModel{})
	require.ErrorContains(t, err, "SMTP host is not configured")

	connectionString, err := smtpConnString(&appconfig.AppConfigModel{SmtpHost: "smtp.example.test"})
	require.NoError(t, err)

	smtpURL, err := url.Parse(connectionString)
	require.NoError(t, err)
	assert.Equal(t, "none", smtpURL.Query().Get("tls"))
	assert.Empty(t, smtpURL.Query().Get("insecureSkipVerify"))
}

type smtpTestSession struct {
	mailFrom string
	rcptTo   string
	message  string
}

type smtpTestServer struct {
	listener  net.Listener
	sessionCh chan smtpTestSession
	errorCh   chan error
}

func newSMTPTestServer(t *testing.T) *smtpTestServer {
	t.Helper()

	// Bind an ephemeral loopback port so each delivery test gets an isolated SMTP endpoint
	listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", "127.0.0.1:0")
	require.NoError(t, err)

	server := &smtpTestServer{
		listener:  listener,
		sessionCh: make(chan smtpTestSession, 1),
		errorCh:   make(chan error, 1),
	}
	go server.serve()

	t.Cleanup(func() {
		_ = listener.Close()
	})

	return server
}

func newSMTPTestConfig(t *testing.T, address string) *appconfig.AppConfigModel {
	t.Helper()

	host, port, err := net.SplitHostPort(address)
	require.NoError(t, err)

	return &appconfig.AppConfigModel{
		AppName:  "Pocket ID Test",
		SmtpHost: appconfig.AppConfigValue(host),
		SmtpPort: appconfig.AppConfigValue(port),
		SmtpFrom: "sender@example.test",
		SmtpTls:  "none",
	}
}

func (s *smtpTestServer) address() string {
	return s.listener.Addr().String()
}

func (s *smtpTestServer) wait() (smtpTestSession, error) {
	select {
	case session := <-s.sessionCh:
		return session, nil
	case err := <-s.errorCh:
		return smtpTestSession{}, err
	case <-time.After(5 * time.Second):
		return smtpTestSession{}, context.DeadlineExceeded
	}
}

func (s *smtpTestServer) serve() {
	conn, err := s.listener.Accept()
	if err != nil {
		s.errorCh <- err
		return
	}

	session, err := handleSMTPConnection(conn)
	if err != nil {
		s.errorCh <- err
		return
	}

	s.sessionCh <- session
}

func handleSMTPConnection(conn net.Conn) (smtpTestSession, error) {
	defer func() {
		_ = conn.Close()
	}()

	reader := bufio.NewReader(conn)
	writer := bufio.NewWriter(conn)
	session := smtpTestSession{}

	err := writeSMTPResponse(writer, "220 localhost ESMTP test")
	if err != nil {
		return smtpTestSession{}, err
	}

	for {
		line, readErr := reader.ReadString('\n')
		if readErr != nil {
			return smtpTestSession{}, readErr
		}
		line = strings.TrimRight(line, "\r\n")

		switch {
		case strings.HasPrefix(line, "EHLO "):
			err = writeSMTPResponse(writer, "250-localhost ESMTP test", "250 OK")
		case strings.HasPrefix(line, "HELO "):
			err = writeSMTPResponse(writer, "250 localhost")
		case strings.HasPrefix(line, "MAIL FROM:"):
			session.mailFrom = strings.TrimPrefix(line, "MAIL FROM:")
			err = writeSMTPResponse(writer, "250 2.1.0 Ok")
		case strings.HasPrefix(line, "RCPT TO:"):
			session.rcptTo = strings.TrimPrefix(line, "RCPT TO:")
			err = writeSMTPResponse(writer, "250 2.1.5 Ok")
		case line == "DATA":
			err = writeSMTPResponse(writer, "354 End data with <CR><LF>.<CR><LF>")
			if err != nil {
				return smtpTestSession{}, err
			}
			session.message, err = readSMTPData(reader)
			if err == nil {
				err = writeSMTPResponse(writer, "250 2.0.0 Ok: queued")
			}
		case line == "QUIT":
			err = writeSMTPResponse(writer, "221 2.0.0 Bye")
			return session, err
		default:
			err = writeSMTPResponse(writer, "250 2.0.0 Ok")
		}

		if err != nil {
			return smtpTestSession{}, err
		}
	}
}

func writeSMTPResponse(writer *bufio.Writer, lines ...string) error {
	for _, line := range lines {
		_, err := writer.WriteString(line + "\r\n")
		if err != nil {
			return err
		}
	}

	return writer.Flush()
}

func readSMTPData(reader *bufio.Reader) (string, error) {
	var message strings.Builder
	for {
		line, err := reader.ReadString('\n')
		if err != nil {
			return "", err
		}
		if line == ".\r\n" {
			return message.String(), nil
		}
		if strings.HasPrefix(line, "..") {
			line = line[1:]
		}
		_, err = io.WriteString(&message, line)
		if err != nil {
			return "", err
		}
	}
}
