//go:build integration

// Run this using docker-compose.yml, see Makefile.

package main

import (
	"context"
	"fmt"
	"net"
	"os"
	"os/exec"
	"strings"
	"testing"
	"time"

	"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"
	"github.com/mjl-/mox/store"
)

var ctxbg = context.Background()

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

// Submit a message to mox, which sends it to postfix, which forwards back to mox.
// We check if we receive the message.
func TestDeliver(t *testing.T) {
	mlog.Logfmt = true
	log := mlog.New("test")

	// Remove state.
	os.RemoveAll("testdata/integration/data")
	os.MkdirAll("testdata/integration/data", 0750)

	// Cleanup afterwards, these are owned by root, annoying to have around due to
	// permission errors.
	defer os.RemoveAll("testdata/integration/data")

	// Load mox config.
	mox.ConfigStaticPath = "testdata/integration/config/mox.conf"
	if errs := mox.LoadConfig(ctxbg, true, false); len(errs) > 0 {
		t.Fatalf("loading mox config: %v", errs)
	}

	// Create new accounts
	createAccount := func(email, password string) {
		t.Helper()
		acc, _, err := store.OpenEmail(email)
		tcheck(t, err, "open account")
		err = acc.SetPassword(password)
		tcheck(t, err, "setting password")
		err = acc.Close()
		tcheck(t, err, "closing account")
	}

	createAccount("moxtest1@mox1.example", "pass1234")
	createAccount("moxtest2@mox2.example", "pass1234")
	createAccount("moxtest3@mox3.example", "pass1234")

	// Start mox.
	const mtastsdbRefresher = false
	const skipForkExec = true
	err := start(mtastsdbRefresher, skipForkExec)
	tcheck(t, err, "starting mox")

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

	testDeliver := func(checkTime bool, imapaddr, imapuser, imappass string, fn func()) {
		t.Helper()

		// Make IMAP connection, we'll wait for a delivery notification with IDLE.
		imapconn, err := net.Dial("tcp", imapaddr)
		tcheck(t, err, "dial imap server")
		defer imapconn.Close()
		client, err := imapclient.New(imapconn, false)
		tcheck(t, err, "new imapclient")
		_, _, err = client.Login(imapuser, imappass)
		tcheck(t, err, "imap client login")
		_, _, err = client.Select("inbox")
		tcheck(t, err, "imap select inbox")

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

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

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

		t0 := time.Now()
		fn()

		// 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(5 * time.Second):
			t.Fatalf("timeout after 5s waiting for IMAP IDLE notification of new message, should take about 1 second")
		}
	}

	submit := func(smtphost, smtpport, mailfrom, password, rcptto string) {
		conn, err := net.Dial("tcp", net.JoinHostPort(smtphost, smtpport))
		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 := []sasl.Client{sasl.NewClientPlain(mailfrom, password)}
		c, err := smtpclient.New(mox.Context, log, conn, smtpclient.TLSOpportunistic, mox.Conf.Static.HostnameDomain, dns.Domain{ASCII: smtphost}, auth)
		tcheck(t, err, "smtp hello")
		err = c.Deliver(mox.Context, mailfrom, rcptto, int64(len(msg)), strings.NewReader(msg), false, false)
		tcheck(t, err, "deliver with smtp")
		err = c.Close()
		tcheck(t, err, "close smtpclient")
	}

	testDeliver(true, "moxmail1.mox1.example:143", "moxtest1@mox1.example", "pass1234", func() {
		submit("moxmail1.mox1.example", "587", "moxtest1@mox1.example", "pass1234", "root@postfix.example")
	})
	testDeliver(true, "moxmail1.mox1.example:143", "moxtest3@mox3.example", "pass1234", func() {
		submit("moxmail2.mox2.example", "587", "moxtest2@mox2.example", "pass1234", "moxtest3@mox3.example")
	})

	testDeliver(false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
		submit("localserve.mox1.example", "1587", "mox@localhost", "moxmoxmox", "any@any.example")
	})

	testDeliver(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", mlog.Field("output", out.String()))
		tcheck(t, err, "sendmail")
	})
}