package server

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"strings"
	"testing"

	"github.com/axllent/mailpit/config"
	"github.com/axllent/mailpit/internal/logger"
	"github.com/axllent/mailpit/internal/storage"
	"github.com/axllent/mailpit/server/apiv1"
	"github.com/jhillyerd/enmime/v2"
)

var (
	putDataStruct struct {
		Read bool
		IDs  []string
	}
)

func TestAPIv1Messages(t *testing.T) {
	setup()
	defer storage.Close()

	r := apiRoutes()

	ts := httptest.NewServer(r)
	defer ts.Close()

	m, err := fetchMessages(ts.URL + "/api/v1/messages")
	if err != nil {
		t.Error(err.Error())
	}

	// check count of empty database
	assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 0)

	// insert 100
	t.Log("Insert 100 messages")
	insertEmailData(t)
	assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)

	m, err = fetchMessages(ts.URL + "/api/v1/messages")
	if err != nil {
		t.Error(err.Error())
	}

	// read first 10 messages
	t.Log("Read first 10 messages including raw & headers")
	for idx, msg := range m.Messages {
		if idx == 10 {
			break
		}

		if _, err := clientGet(ts.URL + "/api/v1/message/" + msg.ID); err != nil {
			t.Error(err.Error())
		}

		// get RAW
		if _, err := clientGet(ts.URL + "/api/v1/message/" + msg.ID + "/raw"); err != nil {
			t.Error(err.Error())
		}

		// get headers
		if _, err := clientGet(ts.URL + "/api/v1/message/" + msg.ID + "/headers"); err != nil {
			t.Error(err.Error())
		}
	}

	// 10 should be marked as read
	assertStatsEqual(t, ts.URL+"/api/v1/messages", 90, 100)

	// delete all
	t.Log("Delete all messages")
	_, err = clientDelete(ts.URL+"/api/v1/messages", "{}")
	if err != nil {
		t.Errorf("Expected nil, received %s", err.Error())
	}
	assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 0)
}

func TestAPIv1ToggleReadStatus(t *testing.T) {
	setup()
	defer storage.Close()

	r := apiRoutes()

	ts := httptest.NewServer(r)
	defer ts.Close()

	m, err := fetchMessages(ts.URL + "/api/v1/messages")
	if err != nil {
		t.Error(err.Error())
	}

	// check count of empty database
	assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 0)

	// insert 100
	t.Log("Insert 100 messages")
	insertEmailData(t)
	assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)

	m, err = fetchMessages(ts.URL + "/api/v1/messages")
	if err != nil {
		t.Error(err.Error())
	}

	// read first 10 IDs
	t.Log("Get first 10 IDs")
	putIDS := []string{}
	for idx, msg := range m.Messages {
		if idx == 10 {
			break
		}

		// store for later
		putIDS = append(putIDS, msg.ID)
	}
	assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)

	// mark first 10 as unread
	t.Log("Mark first 10 as read")
	putData := putDataStruct
	putData.Read = true
	putData.IDs = putIDS
	j, err := json.Marshal(putData)
	if err != nil {
		t.Error(err.Error())
	}
	_, err = clientPut(ts.URL+"/api/v1/messages", string(j))
	if err != nil {
		t.Error(err.Error())
	}
	assertStatsEqual(t, ts.URL+"/api/v1/messages", 90, 100)

	// mark first 10 as read
	t.Log("Mark first 10 as unread")
	putData.Read = false
	j, err = json.Marshal(putData)
	if err != nil {
		t.Error(err.Error())
	}
	_, err = clientPut(ts.URL+"/api/v1/messages", string(j))
	if err != nil {
		t.Error(err.Error())
	}
	assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)

	// mark all as read
	putData.Read = true
	putData.IDs = []string{}
	j, err = json.Marshal(putData)
	if err != nil {
		t.Error(err.Error())
	}

	t.Log("Mark all read")
	_, err = clientPut(ts.URL+"/api/v1/messages", string(j))
	if err != nil {
		t.Error(err.Error())
	}
	assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 100)
}

func TestAPIv1Search(t *testing.T) {
	setup()
	defer storage.Close()

	r := apiRoutes()

	ts := httptest.NewServer(r)
	defer ts.Close()

	// insert 100
	t.Log("Insert 100 messages & tag")
	insertEmailData(t)
	assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)

	// search
	assertSearchEqual(t, ts.URL+"/api/v1/search", "from-1@example.com", 1)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "from:from-1@example.com", 1)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "-from:from-1@example.com", 99)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "-FROM:FROM-1@EXAMPLE.COM", 99)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "to:from-1@example.com", 0)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "from:@example.com", 100)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"Subject line\"", 100)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"SUBJECT LINE 17 END\"", 1)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "!thisdoesnotexist", 100)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "-ThisDoesNotExist", 100)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "thisdoesnotexist", 0)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "tag:\"Test tag 065\"", 1)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "tag:\"TEST TAG 065\"", 1)
	assertSearchEqual(t, ts.URL+"/api/v1/search", "!tag:\"Test tag 023\"", 99)
}

func TestAPIv1Send(t *testing.T) {
	setup()
	defer storage.Close()

	r := apiRoutes()

	ts := httptest.NewServer(r)
	defer ts.Close()

	jsonData := `{
		"From": {
		  "Email": "john@example.com",
		  "Name": "John Doe"
		},
		"To": [
		  {
			"Email": "jane@example.com",
			"Name": "Jane Doe"
		  }
		],
		"Cc": [
		  {
			"Email": "manager1@example.com",
			"Name": "Manager 1"
		  },
		  {
			"Email": "manager2@example.com",
			"Name": "Manager 2"
		  }
		],
		"Bcc": ["jack@example.com"],
		"Headers": {
			"X-IP": "1.2.3.4"
		},
		"Subject": "Mailpit message via the HTTP API",
		"Text": "This is the text body",
		"HTML": "<p style=\"font-family: arial\">Mailpit is <b>awesome</b>!</p>",
		"Attachments": [
		  {
			"Content": "VGhpcyBpcyBhIHBsYWluIHRleHQgYXR0YWNobWVudA==",
			"Filename": "Attached File.txt"
		  },
		  {
			"Content": "iVBORw0KGgoAAAANSUhEUgAAAEEAAAA8CAMAAAAOlSdoAAAACXBIWXMAAAHrAAAB6wGM2bZBAAAAS1BMVEVHcEwRfnUkZ2gAt4UsSF8At4UtSV4At4YsSV4At4YsSV8At4YsSV4At4YsSV4sSV4At4YsSV4At4YtSV4At4YsSV4At4YtSV8At4YsUWYNAAAAGHRSTlMAAwoXGiktRE5dbnd7kpOlr7zJ0d3h8PD8PCSRAAACWUlEQVR42pXT4ZaqIBSG4W9rhqQYocG+/ys9Y0Z0Br+x3j8zaxUPewFh65K+7yrIMeIY4MT3wPfEJCidKXEMnLaVkxDiELiMz4WEOAZSFghxBIypCOlKiAMgXfIqTnBgSm8CIQ6BImxEUxEckClVQiHGj4Ba4AQHikAIClwTE9KtIghAhUJwoLkmLnCiAHJLRKgIMsEtVUKbBUIwoAg2C4QgQBE6l4VCnApBgSKYLLApCnCa0+96AEMW2BQcmC+Pr3nfp7o5Exy49gIADcIqUELGfeA+bp93LmAJp8QJoEcN3C7NY3sbVANixMyI0nku20/n5/ZRf3KI2k6JEDWQtxcbdGuAqu3TAXG+/799Oyyas1B1MnMiA+XyxHp9q0PUKGPiRAau1fZbLRZV09wZcT8/gHk8QQAxXn8VgaDqcUmU6O/r28nbVwXAqca2mRNtPAF5+zoP2MeN9Fy4NgC6RfcbgE7XITBRYTtOE3U3C2DVff7pk+PkUxgAbvtnPXJaD6DxulMLwOhPS/M3MQkgg1ZFrIXnmfaZoOfpKiFgzeZD/WuKqQEGrfJYkyWf6vlG3xUgTuscnkNkQsb599q124kdpMUjCa/XARHs1gZymVtGt3wLkiFv8rUgTxitYCex5EVGec0Y9VmoDTFBSQte2TfXGXlf7hbdaUM9Sk7fisEN9qfBBTK+FZcvM9fQSdkl2vj4W2oX/bRogO3XasiNH7R0eW7fgRM834ImTg+Lg6BEnx4vz81rhr+MYPBBQg1v8GndEOrthxaCTxNAOut8WKLGZQl+MPz88Q9tAO/hVuSeqQAAAABJRU5ErkJggg==",
			"Filename": "logo.png",
			"ContentID": "inline-cid",
			"ContentType": "overridden/type"
		  }
		],
		"ReplyTo": [
		  {
			"Email": "secretary@example.com",
			"Name": "Secretary"
		  }
		],
		"Tags": [
		  "Tag 1",
		  "Tag 2"
		]
	  }`

	t.Log("Sending message via HTTP API")
	b, err := clientPost(ts.URL+"/api/v1/send", jsonData)
	if err != nil {
		t.Errorf("Expected nil, received %s", err.Error())
	}

	resp := apiv1.SendMessageConfirmation{}

	if err := json.Unmarshal(b, &resp); err != nil {
		t.Error(err.Error())
		return
	}

	t.Logf("Fetching response for message %s", resp.ID)
	msg, err := fetchMessage(ts.URL + "/api/v1/message/" + resp.ID)
	if err != nil {
		t.Error(err.Error())
	}

	t.Logf("Testing response for message %s", resp.ID)
	assertEqual(t, `Mailpit message via the HTTP API`, msg.Subject, "wrong subject")
	assertEqual(t, `This is the text body`, msg.Text, "wrong text")
	assertEqual(t, `<p style="font-family: arial">Mailpit is <b>awesome</b>!</p>`, msg.HTML, "wrong HTML")
	assertEqual(t, `"John Doe" <john@example.com>`, msg.From.String(), "wrong HTML")
	assertEqual(t, 1, len(msg.To), "wrong To count")
	assertEqual(t, `"Jane Doe" <jane@example.com>`, msg.To[0].String(), "wrong To address")
	assertEqual(t, 2, len(msg.Cc), "wrong Cc count")
	assertEqual(t, `"Manager 1" <manager1@example.com>`, msg.Cc[0].String(), "wrong Cc address")
	assertEqual(t, `"Manager 2" <manager2@example.com>`, msg.Cc[1].String(), "wrong Cc address")
	assertEqual(t, 1, len(msg.Bcc), "wrong Bcc count")
	assertEqual(t, `<jack@example.com>`, msg.Bcc[0].String(), "wrong Bcc address")
	assertEqual(t, 1, len(msg.ReplyTo), "wrong Reply-To count")
	assertEqual(t, `"Secretary" <secretary@example.com>`, msg.ReplyTo[0].String(), "wrong Reply-To address")
	assertEqual(t, 2, len(msg.Tags), "wrong Tags count")
	assertEqual(t, `Tag 1,Tag 2`, strings.Join(msg.Tags, ","), "wrong Tags")
	assertEqual(t, 1, len(msg.Attachments), "wrong Attachment count")
	assertEqual(t, `Attached File.txt`, msg.Attachments[0].FileName, "wrong Attachment name")
	assertEqual(t, `text/plain`, msg.Attachments[0].ContentType, "wrong Content-Type")
	assertEqual(t, 1, len(msg.Inline), "wrong inline Attachment count")
	assertEqual(t, `logo.png`, msg.Inline[0].FileName, "wrong Attachment name")
	assertEqual(t, `overridden/type`, msg.Inline[0].ContentType, "wrong Content-Type")

	attachmentBytes, err := clientGet(ts.URL + "/api/v1/message/" + resp.ID + "/part/" + msg.Attachments[0].PartID)
	if err != nil {
		t.Error(err.Error())
	}
	assertEqual(t, `This is a plain text attachment`, string(attachmentBytes), "wrong Attachment content")
}

func setup() {
	logger.NoLogging = true
	config.MaxMessages = 0
	config.Database = os.Getenv("MP_DATABASE")

	if err := storage.InitDB(); err != nil {
		panic(err)
	}

	if err := storage.DeleteAllMessages(); err != nil {
		panic(err)
	}
}

func assertStatsEqual(t *testing.T, uri string, unread, total int) {
	m := apiv1.MessagesSummary{}

	data, err := clientGet(uri)
	if err != nil {
		t.Error(err.Error())
		return
	}

	if err := json.Unmarshal(data, &m); err != nil {
		t.Error(err.Error())
		return
	}

	assertEqual(t, uint64(unread), m.Unread, "wrong unread count")
	assertEqual(t, uint64(total), m.Total, "wrong total count")
}

func assertSearchEqual(t *testing.T, uri, query string, count int) {
	t.Logf("Test search: %s", query)
	m := apiv1.MessagesSummary{}

	limit := fmt.Sprintf("%d", count)

	data, err := clientGet(uri + "?query=" + url.QueryEscape(query) + "&limit=" + limit)
	if err != nil {
		t.Error(err.Error())
		return
	}

	if err := json.Unmarshal(data, &m); err != nil {
		t.Error(err.Error())
		return
	}

	assertEqual(t, uint64(count), m.MessagesCount, "wrong search results count")
}

func insertEmailData(t *testing.T) {
	for i := 0; i < 100; i++ {
		msg := enmime.Builder().
			From(fmt.Sprintf("From %d", i), fmt.Sprintf("from-%d@example.com", i)).
			Subject(fmt.Sprintf("Subject line %d end", i)).
			Text([]byte(fmt.Sprintf("This is the email body %d <jdsauk;dwqmdqw;>.", i))).
			To(fmt.Sprintf("To %d", i), fmt.Sprintf("to-%d@example.com", i))

		env, err := msg.Build()
		if err != nil {
			t.Log("error ", err)
			t.Fail()
		}

		buf := new(bytes.Buffer)

		if err := env.Encode(buf); err != nil {
			t.Log("error ", err)
			t.Fail()
		}

		bufBytes := buf.Bytes()

		id, err := storage.Store(&bufBytes)
		if err != nil {
			t.Log("error ", err)
			t.Fail()
		}

		if _, err := storage.SetMessageTags(id, []string{fmt.Sprintf("Test tag %03d", i)}); err != nil {
			t.Log("error ", err)
			t.Fail()
		}
	}
}

func fetchMessage(url string) (storage.Message, error) {
	m := storage.Message{}

	data, err := clientGet(url)
	if err != nil {
		return m, err
	}

	if err := json.Unmarshal(data, &m); err != nil {
		return m, err
	}

	return m, nil
}

func fetchMessages(url string) (apiv1.MessagesSummary, error) {
	m := apiv1.MessagesSummary{}

	data, err := clientGet(url)
	if err != nil {
		return m, err
	}

	if err := json.Unmarshal(data, &m); err != nil {
		return m, err
	}

	return m, nil
}

func clientGet(url string) ([]byte, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode)
	}

	defer resp.Body.Close()

	data, err := io.ReadAll(resp.Body)

	return data, err
}

func clientDelete(url, body string) ([]byte, error) {
	client := new(http.Client)

	b := strings.NewReader(body)
	req, err := http.NewRequest("DELETE", url, b)
	if err != nil {
		return nil, err
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode)
	}

	data, err := io.ReadAll(resp.Body)

	return data, err
}

func clientPut(url, body string) ([]byte, error) {
	client := new(http.Client)

	b := strings.NewReader(body)
	req, err := http.NewRequest("PUT", url, b)
	if err != nil {
		return nil, err
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode)
	}

	data, err := io.ReadAll(resp.Body)

	return data, err
}

func clientPost(url, body string) ([]byte, error) {
	client := new(http.Client)

	b := strings.NewReader(body)
	req, err := http.NewRequest("POST", url, b)
	if err != nil {
		return nil, err
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode)
	}

	data, err := io.ReadAll(resp.Body)

	return data, err
}

func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
	if a == b {
		return
	}
	message = fmt.Sprintf("%s: \"%v\" != \"%v\"", message, a, b)
	t.Fatal(message)
}