package connectedmailhttp

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	standardmail "net/mail"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	connector_mail "github.com/pingcap-inc/nexuscrm-connectors/features/connector/mail"
	outboundintentv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/outboundintentv1"
)

func TestMailAdapterBuildsPlainTextMIMEAndReturnsReceipt(t *testing.T) {
	requestedMIME := make(chan []byte, 1)
	server := httptest.NewTLSServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
		if request.URL.Path != "/send" || request.Header.Get("Authorization") != "Bearer access-secret" ||
			request.Header.Get("Content-Type") != "application/json" {
			t.Errorf("mail request path=%q authorization=%q content-type=%q", request.URL.Path,
				request.Header.Get("Authorization"), request.Header.Get("Content-Type"))
			response.WriteHeader(http.StatusBadRequest)
			return
		}
		var payload map[string]string
		if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
			t.Error(err)
			response.WriteHeader(http.StatusBadRequest)
			return
		}
		raw, err := base64.RawURLEncoding.DecodeString(payload["encodedMessage"])
		if err != nil {
			t.Error(err)
			response.WriteHeader(http.StatusBadRequest)
			return
		}
		requestedMIME <- raw
		response.Header().Set("Content-Type", "application/json")
		_, _ = io.WriteString(response, `{"messageKey":"remote-message-1"}`)
	}))
	defer server.Close()

	adapter, err := NewMailAdapter(mailTestConfig(server.URL), server.Client().Transport, allowAllURLs{})
	if err != nil {
		t.Fatal(err)
	}
	result, err := adapter.Dispatch(context.Background(), "access-secret", connector_mail.Message{
		FromAddress: "sender@example.com", ToAddresses: []string{"first@example.com", "second@example.com"},
		Subject: "Hello 世界", TextBody: "Plain text body\nLine two",
	})
	if err != nil || result.RemoteMessageID != "remote-message-1" || result.ResponseStatus != http.StatusOK {
		t.Fatalf("Dispatch = %#v, %v", result, err)
	}
	raw := <-requestedMIME
	message, err := standardmail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		t.Fatal(err)
	}
	subject, err := new(mime.WordDecoder).DecodeHeader(message.Header.Get("Subject"))
	if err != nil || subject != "Hello 世界" {
		t.Fatalf("decoded subject = %q, %v", subject, err)
	}
	recipients, err := standardmail.ParseAddressList(message.Header.Get("To"))
	if err != nil || len(recipients) != 2 || recipients[0].Address != "first@example.com" || recipients[1].Address != "second@example.com" {
		t.Fatalf("decoded recipients = %#v, %v", recipients, err)
	}
	body, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, message.Body))
	if err != nil || string(body) != "Plain text body\r\nLine two" ||
		message.Header.Get("From") != "sender@example.com" ||
		message.Header.Get("Content-Type") != "text/plain; charset=UTF-8" ||
		message.Header.Get("Content-Transfer-Encoding") != "base64" {
		t.Fatalf("decoded MIME headers=%#v body=%q error=%v", message.Header, body, err)
	}
}

func TestMailAdapterV2BuildsRecipientHeadersAndMultipartAlternative(t *testing.T) {
	raw := buildMailV2MIME(connector_mail.MessageV2{
		Message: connector_mail.Message{
			FromAddress: "sender@example.com", ToAddresses: []string{"to@example.com"},
			Subject: "Rich message", TextBody: "Plain body",
		},
		CcAddresses: []string{"cc@example.com"}, BccAddresses: []string{"bcc@example.com"},
		HTMLBody: "<p>Rich body</p>",
	})
	message, err := standardmail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		t.Fatal(err)
	}
	for name, want := range map[string]string{
		"To": "to@example.com", "Cc": "cc@example.com", "Bcc": "bcc@example.com",
	} {
		addresses, parseErr := standardmail.ParseAddressList(message.Header.Get(name))
		if parseErr != nil || len(addresses) != 1 || addresses[0].Address != want {
			t.Fatalf("%s header = %#v, %v", name, addresses, parseErr)
		}
	}
	mediaType, params, err := mime.ParseMediaType(message.Header.Get("Content-Type"))
	if err != nil || mediaType != "multipart/alternative" || params["boundary"] == "" {
		t.Fatalf("content type = %q %#v, %v", mediaType, params, err)
	}
	reader := multipart.NewReader(message.Body, params["boundary"])
	wantParts := []struct{ mediaType, body string }{
		{"text/plain", "Plain body"}, {"text/html", "<p>Rich body</p>"},
	}
	for index, want := range wantParts {
		part, partErr := reader.NextPart()
		if partErr != nil {
			t.Fatalf("part %d: %v", index, partErr)
		}
		partType, _, parseErr := mime.ParseMediaType(part.Header.Get("Content-Type"))
		body, readErr := io.ReadAll(base64.NewDecoder(base64.StdEncoding, part))
		if parseErr != nil || readErr != nil || partType != want.mediaType || strings.TrimSpace(string(body)) != want.body {
			t.Fatalf("part %d: type=%q body=%q parse=%v read=%v", index, partType, body, parseErr, readErr)
		}
	}
	if _, err := reader.NextPart(); !errors.Is(err, io.EOF) {
		t.Fatalf("extra MIME part: %v", err)
	}
}

func TestMailAdapterV3BuildsMixedMIMEWithIntactNonASCIIAttachment(t *testing.T) {
	content := []byte("intact-pdf-bytes")
	raw, ok := buildMailV3MIME(connector_mail.MessageV3{
		MessageV2: connector_mail.MessageV2{
			Message:  connector_mail.Message{FromAddress: "sender@example.com", ToAddresses: []string{"to@example.com"}, Subject: "資料", TextBody: "Plain"},
			HTMLBody: "<p>Plain</p>",
		},
		Attachments: []connector_mail.Attachment{{FileName: "営業資料.pdf", ContentType: "Application/PDF; charset=UTF-8", Data: content}},
	})
	if !ok {
		t.Fatal("bounded MIME builder rejected valid message")
	}
	message, err := standardmail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		t.Fatal(err)
	}
	mediaType, params, err := mime.ParseMediaType(message.Header.Get("Content-Type"))
	if err != nil || mediaType != "multipart/mixed" {
		t.Fatalf("content type = %q %#v, %v", mediaType, params, err)
	}
	reader := multipart.NewReader(message.Body, params["boundary"])
	bodyPart, err := reader.NextPart()
	if err != nil {
		t.Fatal(err)
	}
	bodyType, _, err := mime.ParseMediaType(bodyPart.Header.Get("Content-Type"))
	if err != nil || bodyType != "multipart/alternative" {
		t.Fatalf("body type = %q, %v", bodyType, err)
	}
	attachment, err := reader.NextPart()
	if err != nil {
		t.Fatal(err)
	}
	attachmentType, attachmentParams, typeErr := mime.ParseMediaType(attachment.Header.Get("Content-Type"))
	_, disposition, err := mime.ParseMediaType(attachment.Header.Get("Content-Disposition"))
	decoded, readErr := io.ReadAll(base64.NewDecoder(base64.StdEncoding, attachment))
	if typeErr != nil || attachmentType != "application/pdf" || attachmentParams["charset"] != "UTF-8" ||
		err != nil || readErr != nil || disposition["filename"] != "営業資料.pdf" || !bytes.Equal(decoded, content) {
		t.Fatalf("attachment type=%q params=%#v filename=%q body=%q typeParse=%v dispositionParse=%v read=%v",
			attachmentType, attachmentParams, disposition["filename"], decoded, typeErr, err, readErr)
	}
	if _, err := reader.NextPart(); !errors.Is(err, io.EOF) {
		t.Fatalf("extra MIME part: %v", err)
	}
}

func TestMailAdapterV3AcceptsContractValidAggregateAbovePerAttachmentLimit(t *testing.T) {
	firstSize := outboundintentv1.MaxMailAttachmentBytes / 2
	message := connector_mail.MessageV3{
		MessageV2: connector_mail.MessageV2{Message: connector_mail.Message{
			FromAddress: "sender@example.com", ToAddresses: []string{"to@example.com"},
			Subject: "Subject", TextBody: "Body",
		}},
		Attachments: []connector_mail.Attachment{
			{FileName: "first.bin", ContentType: "application/octet-stream", Data: make([]byte, firstSize)},
			{FileName: "second.bin", ContentType: "application/octet-stream", Data: make([]byte, outboundintentv1.MaxMailAttachmentBytes-firstSize+1)},
		},
	}
	defer func() {
		for index := range message.Attachments {
			clearBytes(message.Attachments[index].Data)
		}
	}()
	if !validMessageV3(message) {
		t.Fatal("contract-valid aggregate above the per-attachment limit was rejected")
	}
	raw, ok := buildMailV3MIME(message)
	if !ok || len(raw) > outboundintentv1.MaxMailRawMessageBytes {
		t.Fatalf("bounded MIME build = %d bytes, accepted=%t", len(raw), ok)
	}
	clearBytes(raw)
}

func TestMailAdapterV3RejectsOversizeBeforeMaterializingMIME(t *testing.T) {
	message := connector_mail.MessageV3{
		MessageV2: connector_mail.MessageV2{Message: connector_mail.Message{
			FromAddress: "sender@example.com", ToAddresses: []string{"to@example.com"},
			Subject: "Subject", TextBody: "Body",
		}},
		Attachments: []connector_mail.Attachment{
			{FileName: "first.bin", ContentType: "application/octet-stream", Data: make([]byte, 14*1024*1024)},
			{FileName: "second.bin", ContentType: "application/octet-stream", Data: make([]byte, 14*1024*1024)},
		},
	}
	defer func() {
		for index := range message.Attachments {
			clearBytes(message.Attachments[index].Data)
		}
	}()
	if !validMessageV3(message) {
		t.Fatal("per-attachment-valid message was rejected before the MIME size owner")
	}
	if raw, ok := buildMailV3MIME(message); ok || raw != nil {
		clearBytes(raw)
		t.Fatalf("oversize MIME was materialized: accepted=%t bytes=%d", ok, len(raw))
	}
}

func TestMailAdapterV2RejectsDuplicatesAcrossRecipientClasses(t *testing.T) {
	message := connector_mail.MessageV2{
		Message: connector_mail.Message{
			FromAddress: "sender@example.com", ToAddresses: []string{"same@example.com"},
			Subject: "Subject", TextBody: "Body",
		},
		CcAddresses: []string{"SAME@example.com"},
	}
	if validMessageV2(message) {
		t.Fatal("duplicate address across To and Cc was accepted")
	}
}

func TestMailAdapterClassifiesProviderOutcomesWithoutRetrying(t *testing.T) {
	tests := []struct {
		name   string
		status int
		body   string
		kind   connector_mail.ProviderErrorKind
	}{
		{name: "bad request", status: http.StatusBadRequest, kind: connector_mail.ProviderRejected},
		{name: "unauthorized", status: http.StatusUnauthorized, kind: connector_mail.ProviderAuthorization},
		{name: "redirect", status: http.StatusTemporaryRedirect, kind: connector_mail.ProviderUncertain},
		{name: "timeout", status: http.StatusRequestTimeout, kind: connector_mail.ProviderUncertain},
		{name: "too early", status: http.StatusTooEarly, kind: connector_mail.ProviderUncertain},
		{name: "rate limited", status: http.StatusTooManyRequests, kind: connector_mail.ProviderUncertain},
		{name: "server error", status: http.StatusInternalServerError, kind: connector_mail.ProviderUncertain},
		{name: "malformed success", status: http.StatusOK, body: `{}`, kind: connector_mail.ProviderUncertain},
		{name: "malformed UTF-8 receipt", status: http.StatusOK, body: "{\"messageKey\":\"remote-\xff-id\"}", kind: connector_mail.ProviderUncertain},
		{name: "control receipt", status: http.StatusOK, body: `{"messageKey":"remote\u0085id"}`, kind: connector_mail.ProviderUncertain},
		{name: "padded receipt", status: http.StatusOK, body: `{"messageKey":" remote-id "}`, kind: connector_mail.ProviderUncertain},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			var calls atomic.Int64
			adapter, err := NewMailAdapter(mailTestConfig("https://provider.example"), roundTripFunc(func(request *http.Request) (*http.Response, error) {
				calls.Add(1)
				_ = request.Body.Close()
				return jsonResponse(test.status, test.body), nil
			}), allowAllURLs{})
			if err != nil {
				t.Fatal(err)
			}
			result, err := adapter.Dispatch(context.Background(), "access-secret", validMailMessage())
			assertMailProviderError(t, err, test.kind)
			if calls.Load() != 1 || result.ResponseStatus != test.status {
				t.Fatalf("calls=%d result=%#v", calls.Load(), result)
			}
		})
	}
}

func TestMailAdapterDoesNotFollowCredentialedRedirects(t *testing.T) {
	var targetCalls atomic.Int64
	target := httptest.NewTLSServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		targetCalls.Add(1)
	}))
	defer target.Close()
	var sourceCalls atomic.Int64
	source := httptest.NewTLSServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
		sourceCalls.Add(1)
		response.Header().Set("Location", target.URL+"/capture")
		response.WriteHeader(http.StatusTemporaryRedirect)
	}))
	defer source.Close()

	adapter, err := NewMailAdapter(mailTestConfig(source.URL), source.Client().Transport, allowAllURLs{})
	if err != nil {
		t.Fatal(err)
	}
	_, err = adapter.Dispatch(context.Background(), "access-secret", validMailMessage())
	assertMailProviderError(t, err, connector_mail.ProviderUncertain)
	if sourceCalls.Load() != 1 || targetCalls.Load() != 0 {
		t.Fatalf("redirect calls source=%d target=%d", sourceCalls.Load(), targetCalls.Load())
	}
}

func TestMailAdapterFailsBeforeProviderIOWhenNotDispatchable(t *testing.T) {
	allowlist := &mutableAllowlist{}
	var calls atomic.Int64
	adapter, err := NewMailAdapter(mailTestConfig("https://provider.example"), roundTripFunc(func(request *http.Request) (*http.Response, error) {
		calls.Add(1)
		_ = request.Body.Close()
		return nil, errors.New("must not run")
	}), allowlist)
	if err != nil {
		t.Fatal(err)
	}
	allowlist.set(errors.New("denied with access-secret"))
	_, err = adapter.Dispatch(context.Background(), "access-secret", validMailMessage())
	assertMailProviderError(t, err, connector_mail.ProviderRejected)
	if calls.Load() != 0 || strings.Contains(err.Error(), "secret") || strings.Contains(err.Error(), "denied") {
		t.Fatalf("allowlist result calls=%d error=%q", calls.Load(), err)
	}

	allowlist.set(nil)
	canceled, cancel := context.WithCancel(context.Background())
	cancel()
	_, err = adapter.Dispatch(canceled, "access-secret", validMailMessage())
	assertMailProviderError(t, err, connector_mail.ProviderNotDispatched)
	if calls.Load() != 0 {
		t.Fatalf("canceled dispatch made %d provider call(s)", calls.Load())
	}
}

func TestMailAdapterTreatsTransportFailureAsSanitizedUnknownOutcome(t *testing.T) {
	adapter, err := NewMailAdapter(mailTestConfig("https://provider.example"), roundTripFunc(func(request *http.Request) (*http.Response, error) {
		_ = request.Body.Close()
		return nil, errors.New("write failed with access-secret and message body")
	}), allowAllURLs{})
	if err != nil {
		t.Fatal(err)
	}
	_, err = adapter.Dispatch(context.Background(), "access-secret", validMailMessage())
	assertMailProviderError(t, err, connector_mail.ProviderUncertain)
	if strings.Contains(err.Error(), "secret") || strings.Contains(err.Error(), "message") || strings.Contains(err.Error(), "write") {
		t.Fatalf("provider error leaked detail: %q", err)
	}
}

func TestMailAdapterKeepsPayloadUntilTransportClosesRequestBody(t *testing.T) {
	releaseRead := make(chan struct{})
	readPayload := make(chan []byte, 1)
	adapter, err := NewMailAdapter(mailTestConfig("https://provider.example"), roundTripFunc(func(request *http.Request) (*http.Response, error) {
		go func() {
			<-releaseRead
			payload, _ := io.ReadAll(request.Body)
			readPayload <- payload
			_ = request.Body.Close()
		}()
		return jsonResponse(http.StatusOK, `{"messageKey":"remote-message-1"}`), nil
	}), allowAllURLs{})
	if err != nil {
		t.Fatal(err)
	}
	if _, err := adapter.Dispatch(context.Background(), "access-secret", validMailMessage()); err != nil {
		t.Fatal(err)
	}
	close(releaseRead)
	select {
	case payload := <-readPayload:
		var request map[string]string
		if err := json.Unmarshal(payload, &request); err != nil || request["encodedMessage"] == "" {
			t.Fatalf("asynchronous request payload = %q, %v", payload, err)
		}
	case <-time.After(time.Second):
		t.Fatal("transport did not finish reading the request body")
	}
}

func TestMailAdapterRejectsInvalidConfigurationAndMessages(t *testing.T) {
	for name, mutate := range map[string]func(*MailConfig){
		"plain HTTP":     func(config *MailConfig) { config.Endpoint = "http://127.0.0.1/send" },
		"endpoint query": func(config *MailConfig) { config.Endpoint += "?token=forbidden" },
		"request field":  func(config *MailConfig) { config.RequestField = "raw.message" },
		"response field": func(config *MailConfig) { config.ResponseIDField = "" },
		"excessive bound": func(config *MailConfig) {
			config.TerminationBound = time.Minute + time.Nanosecond
		},
	} {
		t.Run(name, func(t *testing.T) {
			config := mailTestConfig("https://provider.example")
			mutate(&config)
			if _, err := NewMailAdapter(config, http.DefaultTransport, allowAllURLs{}); !errors.Is(err, connector_mail.ErrInvalidInput) {
				t.Fatalf("NewMailAdapter error = %v", err)
			}
		})
	}
	if _, err := NewMailAdapter(mailTestConfig("https://provider.example"), http.DefaultTransport,
		&mutableAllowlist{err: errors.New("host denied")}); !errors.Is(err, connector_mail.ErrInvalidInput) {
		t.Fatalf("allowlist configuration error = %v", err)
	}

	var calls atomic.Int64
	adapter, err := NewMailAdapter(mailTestConfig("https://provider.example"), roundTripFunc(func(request *http.Request) (*http.Response, error) {
		calls.Add(1)
		_ = request.Body.Close()
		return jsonResponse(http.StatusOK, `{"messageKey":"unexpected"}`), nil
	}), allowAllURLs{})
	if err != nil {
		t.Fatal(err)
	}
	for name, message := range map[string]connector_mail.Message{
		"header injection": {
			FromAddress: "sender@example.com", ToAddresses: []string{"recipient@example.com"}, Subject: "unsafe\r\nBcc: attacker@example.com", TextBody: "Body",
		},
		"duplicate recipient": {
			FromAddress: "sender@example.com", ToAddresses: []string{"recipient@example.com", "RECIPIENT@example.com"}, Subject: "Subject", TextBody: "Body",
		},
		"display-name sender": {
			FromAddress: "Sender <sender@example.com>", ToAddresses: []string{"recipient@example.com"}, Subject: "Subject", TextBody: "Body",
		},
		"control sender": {
			FromAddress: "sender\u0085@example.com", ToAddresses: []string{"recipient@example.com"}, Subject: "Subject", TextBody: "Body",
		},
		"empty body": {
			FromAddress: "sender@example.com", ToAddresses: []string{"recipient@example.com"}, Subject: "Subject",
		},
	} {
		t.Run(name, func(t *testing.T) {
			_, err := adapter.Dispatch(context.Background(), "access-secret", message)
			assertMailProviderError(t, err, connector_mail.ProviderRejected)
		})
	}
	_, err = adapter.Dispatch(context.Background(), "unsafe\r\nheader", validMailMessage())
	assertMailProviderError(t, err, connector_mail.ProviderRejected)
	if calls.Load() != 0 {
		t.Fatalf("invalid messages made %d provider call(s)", calls.Load())
	}
}

func TestPlainTextMIMEKeepsEveryLineBounded(t *testing.T) {
	recipients := make([]string, outboundintentv1.MaxMailRecipients)
	for index := range recipients {
		recipients[index] = fmt.Sprintf("recipient-%02d@example.com", index)
	}
	subject := strings.Repeat("a", outboundintentv1.MaxMailSubjectBytes)
	body := strings.Repeat("x", outboundintentv1.MaxMailTextBodyBytes)
	raw := buildPlainTextMIME(connector_mail.Message{
		FromAddress: "sender@example.com", ToAddresses: recipients, Subject: subject, TextBody: body,
	})
	headerEnd := bytes.Index(raw, []byte("\r\n\r\n"))
	if headerEnd < 0 {
		t.Fatal("MIME header terminator is missing")
	}
	for _, line := range bytes.Split(raw[:headerEnd], []byte("\r\n")) {
		if len(line) > 998 {
			t.Fatalf("MIME header line length = %d", len(line))
		}
	}
	message, err := standardmail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		t.Fatal(err)
	}
	parsedRecipients, err := standardmail.ParseAddressList(message.Header.Get("To"))
	if err != nil || len(parsedRecipients) != len(recipients) {
		t.Fatalf("decoded bounded recipients=%d error=%v", len(parsedRecipients), err)
	}
	decoded, err := new(mime.WordDecoder).DecodeHeader(message.Header.Get("Subject"))
	if err != nil || decoded != subject {
		t.Fatalf("decoded bounded subject length=%d error=%v", len(decoded), err)
	}
	for _, line := range bytes.Split(raw[headerEnd+4:], []byte("\r\n")) {
		if len(line) > 76 {
			t.Fatalf("MIME body line length = %d", len(line))
		}
	}
	decodedBody, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, message.Body))
	if err != nil || len(decodedBody) != len(body) || !bytes.Equal(decodedBody, []byte(body)) {
		t.Fatalf("decoded bounded body length=%d error=%v", len(decodedBody), err)
	}
}

func mailTestConfig(base string) MailConfig {
	return MailConfig{
		Endpoint: base + "/send", RequestField: "encodedMessage", ResponseIDField: "messageKey",
		TerminationBound: time.Second,
	}
}

func validMailMessage() connector_mail.Message {
	return connector_mail.Message{
		FromAddress: "sender@example.com", ToAddresses: []string{"recipient@example.com"},
		Subject: "Subject", TextBody: "Plain text body",
	}
}

func assertMailProviderError(t *testing.T, err error, kind connector_mail.ProviderErrorKind) {
	t.Helper()
	var providerErr *connector_mail.ProviderError
	if !errors.As(err, &providerErr) || providerErr.Kind != kind {
		t.Fatalf("provider error = %#v; want kind=%q", err, kind)
	}
}
