package testenv

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/testcontainers/testcontainers-go"
	tcnetwork "github.com/testcontainers/testcontainers-go/network"
	"github.com/testcontainers/testcontainers-go/wait"
)

// Mailpit holds an axllent/mailpit testcontainer that captures
// outbound SMTP from backend. The HTTP API is exposed for mail
// inspection from tests.
type Mailpit struct {
	container testcontainers.Container
	SMTPHost  string
	SMTPPort  int
	APIBase   string
}

// StartMailpit starts an axllent/mailpit container attached to network.
func StartMailpit(t *testing.T, network string) *Mailpit {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
	defer cancel()

	req := testcontainers.ContainerRequest{
		Image:        "axllent/mailpit:latest",
		ExposedPorts: []string{"1025/tcp", "8025/tcp"},
		WaitingFor:   wait.ForHTTP("/api/v1/info").WithPort("8025/tcp"),
	}
	gcr := &testcontainers.GenericContainerRequest{ContainerRequest: req}
	if network != "" {
		netOpt := tcnetwork.WithNetwork([]string{"mailpit"}, &testcontainers.DockerNetwork{Name: network})
		_ = netOpt.Customize(gcr)
	}

	gcr.Started = true
	container, err := testcontainers.GenericContainer(ctx, *gcr)
	if err != nil {
		t.Skipf("mailpit container unavailable: %v", err)
	}
	t.Cleanup(func() {
		if err := testcontainers.TerminateContainer(container); err != nil {
			t.Logf("terminate mailpit: %v", err)
		}
	})

	host, err := container.Host(ctx)
	if err != nil {
		t.Fatalf("mailpit host: %v", err)
	}
	smtpPort, err := container.MappedPort(ctx, "1025/tcp")
	if err != nil {
		t.Fatalf("mailpit smtp port: %v", err)
	}
	apiPort, err := container.MappedPort(ctx, "8025/tcp")
	if err != nil {
		t.Fatalf("mailpit api port: %v", err)
	}
	return &Mailpit{
		container: container,
		SMTPHost:  host,
		SMTPPort:  int(smtpPort.Num()),
		APIBase:   fmt.Sprintf("http://%s:%d", host, apiPort.Num()),
	}
}

// Message is a single mailpit message summary.
type Message struct {
	ID      string         `json:"ID"`
	From    MessageAddress `json:"From"`
	To      []MessageAddress `json:"To"`
	Subject string         `json:"Subject"`
	Snippet string         `json:"Snippet"`
}

// MessageAddress is one address in From/To.
type MessageAddress struct {
	Address string `json:"Address"`
	Name    string `json:"Name"`
}

type messagesResponse struct {
	Messages []Message `json:"messages"`
	Total    int       `json:"total"`
}

// MessageBody fetches the rendered body (text) of message id.
func (m *Mailpit) MessageBody(ctx context.Context, id string) (string, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, m.APIBase+"/api/v1/message/"+url.PathEscape(id), nil)
	if err != nil {
		return "", err
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("mailpit message %s: status %d", id, resp.StatusCode)
	}
	var body struct {
		Text string `json:"Text"`
		HTML string `json:"HTML"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		return "", err
	}
	if body.Text != "" {
		return body.Text, nil
	}
	return body.HTML, nil
}

// Search returns messages matching the mailpit search expression. See
// https://mailpit.axllent.org/docs/usage/search-filters/.
func (m *Mailpit) Search(ctx context.Context, query string) ([]Message, error) {
	u := m.APIBase + "/api/v1/search?query=" + url.QueryEscape(query)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return nil, err
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("mailpit search: status %d: %s", resp.StatusCode, string(body))
	}
	var out messagesResponse
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, err
	}
	return out.Messages, nil
}

// WaitForMessage polls Search until a message matching query is seen
// or the deadline elapses.
func (m *Mailpit) WaitForMessage(ctx context.Context, query string, timeout time.Duration) (Message, error) {
	deadline := time.Now().Add(timeout)
	for {
		msgs, err := m.Search(ctx, query)
		if err == nil && len(msgs) > 0 {
			return msgs[0], nil
		}
		if time.Now().After(deadline) {
			if err == nil {
				err = fmt.Errorf("no messages match %q", query)
			}
			return Message{}, err
		}
		select {
		case <-ctx.Done():
			return Message{}, ctx.Err()
		case <-time.After(200 * time.Millisecond):
		}
	}
}

// DeleteAll clears the mailpit inbox. Useful between phases of a test.
func (m *Mailpit) DeleteAll(ctx context.Context) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, m.APIBase+"/api/v1/messages", nil)
	if err != nil {
		return err
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode/100 != 2 {
		return fmt.Errorf("mailpit delete: status %d", resp.StatusCode)
	}
	return nil
}

// ContainsLine reports whether body contains a line that begins with
// prefix; helpful for extracting login codes from the text body.
func ContainsLine(body, prefix string) bool {
	for _, line := range strings.Split(body, "\n") {
		if strings.HasPrefix(strings.TrimSpace(line), prefix) {
			return true
		}
	}
	return false
}
