//go:build integration

// todo: set up a test for dane, mta-sts, etc.

package main

import (
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"os/exec"
	"strings"
	"testing"
	"time"

	"golang.org/x/exp/slog"

	"github.com/mjl-/mox/dns"
	"github.com/mjl-/mox/imapclient"
	"github.com/mjl-/mox/mlog"
	"github.com/mjl-/mox/mox-"
	"github.com/mjl-/mox/sasl"
	"github.com/mjl-/mox/smtpclient"
)

func tcheck(t *testing.T, err error, errmsg string) {
	if err != nil {
		t.Helper()
		t.Fatalf("%s: %s", errmsg, err)
	}
}

func TestDeliver(t *testing.T) {
	log := mlog.New("integration", nil)
	mlog.Logfmt = true

	hostname, err := os.Hostname()
	tcheck(t, err, "hostname")
	ourHostname, err := dns.ParseDomain(hostname)
	tcheck(t, err, "parse hostname")

	// Single update from IMAP IDLE.
	type idleResponse struct {
		untagged imapclient.Untagged
		err      error
	}

	// Deliver submits a message over submissions, and checks with imap idle if the
	// message is received by the destination mail server.
	deliver := func(checkTime bool, dialtls bool, imaphost, imapuser, imappassword string, send func()) {
		t.Helper()

		// Connect to IMAP, execute IDLE command, which will return on deliver message.
		// TLS certificates work because the container has the CA certificates configured.
		var imapconn net.Conn
		var err error
		if dialtls {
			imapconn, err = tls.Dial("tcp", imaphost, nil)
		} else {
			imapconn, err = net.Dial("tcp", imaphost)
		}
		tcheck(t, err, "dial imap")
		defer imapconn.Close()

		imapc, err := imapclient.New(imapconn, false)
		tcheck(t, err, "new imapclient")

		_, _, err = imapc.Login(imapuser, imappassword)
		tcheck(t, err, "imap login")

		_, _, err = imapc.Select("Inbox")
		tcheck(t, err, "imap select inbox")

		err = imapc.Commandf("", "idle")
		tcheck(t, err, "write imap idle command")

		_, _, _, err = imapc.ReadContinuation()
		tcheck(t, err, "read imap continuation")

		idle := make(chan idleResponse)
		go func() {
			for {
				untagged, err := imapc.ReadUntagged()
				idle <- idleResponse{untagged, err}
				if err != nil {
					return
				}
			}
		}()
		defer func() {
			err := imapc.Writelinef("done")
			tcheck(t, err, "aborting idle")
		}()

		t0 := time.Now()
		send()

		// Wait for notification of delivery.
		select {
		case resp := <-idle:
			tcheck(t, resp.err, "idle notification")
			_, ok := resp.untagged.(imapclient.UntaggedExists)
			if !ok {
				t.Fatalf("got idle %#v, expected untagged exists", resp.untagged)
			}
			if d := time.Since(t0); checkTime && d < 1*time.Second {
				t.Fatalf("delivery took %v, but should have taken at least 1 second, the first-time sender delay", d)
			}
		case <-time.After(30 * time.Second):
			t.Fatalf("timeout after 5s waiting for IMAP IDLE notification of new message, should take about 1 second")
		}
	}

	submit := func(dialtls bool, mailfrom, password, desthost, rcptto string) {
		var conn net.Conn
		var err error
		if dialtls {
			conn, err = tls.Dial("tcp", desthost, nil)
		} else {
			conn, err = net.Dial("tcp", desthost)
		}
		tcheck(t, err, "dial submission")
		defer conn.Close()

		msg := fmt.Sprintf(`From: <%s>
To: <%s>
Subject: test message

This is the message.
`, mailfrom, rcptto)
		msg = strings.ReplaceAll(msg, "\n", "\r\n")
		auth := func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
			return sasl.NewClientPlain(mailfrom, password), nil
		}
		c, err := smtpclient.New(mox.Context, log.Logger, conn, smtpclient.TLSSkip, false, ourHostname, dns.Domain{ASCII: desthost}, smtpclient.Opts{Auth: auth})
		tcheck(t, err, "smtp hello")
		err = c.Deliver(mox.Context, mailfrom, rcptto, int64(len(msg)), strings.NewReader(msg), false, false, false)
		tcheck(t, err, "deliver with smtp")
		err = c.Close()
		tcheck(t, err, "close smtpclient")
	}

	// Make sure moxacmepebble has a TLS certificate.
	conn, err := tls.Dial("tcp", "moxacmepebble.mox1.example:465", nil)
	tcheck(t, err, "dial submission")
	defer conn.Close()

	log.Print("submitting email to moxacmepebble, waiting for imap notification at moxmail2")
	t0 := time.Now()
	deliver(true, true, "moxmail2.mox2.example:993", "moxtest2@mox2.example", "accountpass4321", func() {
		submit(true, "moxtest1@mox1.example", "accountpass1234", "moxacmepebble.mox1.example:465", "moxtest2@mox2.example")
	})
	log.Print("success", slog.Duration("duration", time.Since(t0)))

	log.Print("submitting email to moxmail2, waiting for imap notification at moxacmepebble")
	t0 = time.Now()
	deliver(true, true, "moxacmepebble.mox1.example:993", "moxtest1@mox1.example", "accountpass1234", func() {
		submit(true, "moxtest2@mox2.example", "accountpass4321", "moxmail2.mox2.example:465", "moxtest1@mox1.example")
	})
	log.Print("success", slog.Duration("duration", time.Since(t0)))

	log.Print("submitting email to postfix, waiting for imap notification at moxacmepebble")
	t0 = time.Now()
	deliver(false, true, "moxacmepebble.mox1.example:993", "moxtest1@mox1.example", "accountpass1234", func() {
		submit(true, "moxtest1@mox1.example", "accountpass1234", "moxacmepebble.mox1.example:465", "root@postfix.example")
	})
	log.Print("success", slog.Duration("duration", time.Since(t0)))

	log.Print("submitting email to localserve")
	t0 = time.Now()
	deliver(false, false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
		submit(false, "mox@localhost", "moxmoxmox", "localserve.mox1.example:1587", "moxtest1@mox1.example")
	})
	log.Print("success", slog.Duration("duration", time.Since(t0)))

	log.Print("submitting email to localserve")
	t0 = time.Now()
	deliver(false, false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
		cmd := exec.Command("go", "run", ".", "sendmail", "mox@localhost")
		const msg = `Subject: test

a message.
`
		cmd.Stdin = strings.NewReader(msg)
		var out strings.Builder
		cmd.Stdout = &out
		err := cmd.Run()
		log.Print("sendmail", slog.String("output", out.String()))
		tcheck(t, err, "sendmail")
	})
	log.Print("success", slog.Any("duration", time.Since(t0)))
}
