package rest

import (
	"bytes"
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"net/mail"
	"time"

	"github.com/jhillyerd/enmime"
	"github.com/jhillyerd/inbucket/config"
	"github.com/jhillyerd/inbucket/datastore"
	"github.com/jhillyerd/inbucket/httpd"
	"github.com/jhillyerd/inbucket/msghub"
)

type InputMessageData struct {
	Mailbox, ID, From, Subject string
	To                         []string
	Date                       time.Time
	Size                       int
	Header                     mail.Header
	HTML, Text                 string
}

func (d *InputMessageData) MockMessage() *datastore.MockMessage {
	msg := &datastore.MockMessage{}
	msg.On("ID").Return(d.ID)
	msg.On("From").Return(d.From)
	msg.On("To").Return(d.To)
	msg.On("Subject").Return(d.Subject)
	msg.On("Date").Return(d.Date)
	msg.On("Size").Return(d.Size)
	gomsg := &mail.Message{
		Header: d.Header,
	}
	msg.On("ReadHeader").Return(gomsg, nil)
	body := &enmime.Envelope{
		Text: d.Text,
		HTML: d.HTML,
	}
	msg.On("ReadBody").Return(body, nil)
	return msg
}

// isJSONStringEqual is a utility function to return a nicely formatted message when
// comparing a string to a value received from a JSON map.
func isJSONStringEqual(key, expected string, received interface{}) (message string, ok bool) {
	if value, ok := received.(string); ok {
		if expected == value {
			return "", true
		}
		return fmt.Sprintf("Expected value of key %v to be %q, got %q", key, expected, value), false
	}
	return fmt.Sprintf("Expected value of key %v to be a string, got %T", key, received), false
}

// isJSONNumberEqual is a utility function to return a nicely formatted message when
// comparing an float64 to a value received from a JSON map.
func isJSONNumberEqual(key string, expected float64, received interface{}) (message string, ok bool) {
	if value, ok := received.(float64); ok {
		if expected == value {
			return "", true
		}
		return fmt.Sprintf("Expected %v to be %v, got %v", key, expected, value), false
	}
	return fmt.Sprintf("Expected %v to be a string, got %T", key, received), false
}

// CompareToJSONHeaderMap compares InputMessageData to a header map decoded from JSON,
// returning a list of things that did not match.
func (d *InputMessageData) CompareToJSONHeaderMap(json interface{}) (errors []string) {
	if m, ok := json.(map[string]interface{}); ok {
		if msg, ok := isJSONStringEqual(mailboxKey, d.Mailbox, m[mailboxKey]); !ok {
			errors = append(errors, msg)
		}
		if msg, ok := isJSONStringEqual(idKey, d.ID, m[idKey]); !ok {
			errors = append(errors, msg)
		}
		if msg, ok := isJSONStringEqual(fromKey, d.From, m[fromKey]); !ok {
			errors = append(errors, msg)
		}
		for i, inputTo := range d.To {
			if msg, ok := isJSONStringEqual(toKey, inputTo, m[toKey].([]interface{})[i]); !ok {
				errors = append(errors, msg)
			}
		}
		if msg, ok := isJSONStringEqual(subjectKey, d.Subject, m[subjectKey]); !ok {
			errors = append(errors, msg)
		}
		exDate := d.Date.Format("2006-01-02T15:04:05.999999999-07:00")
		if msg, ok := isJSONStringEqual(dateKey, exDate, m[dateKey]); !ok {
			errors = append(errors, msg)
		}
		if msg, ok := isJSONNumberEqual(sizeKey, float64(d.Size), m[sizeKey]); !ok {
			errors = append(errors, msg)
		}
		return errors
	}
	panic(fmt.Sprintf("Expected map[string]interface{} in json, got %T", json))
}

// CompareToJSONMessageMap compares InputMessageData to a message map decoded from JSON,
// returning a list of things that did not match.
func (d *InputMessageData) CompareToJSONMessageMap(json interface{}) (errors []string) {
	// We need to check the same values as header first
	errors = d.CompareToJSONHeaderMap(json)

	if m, ok := json.(map[string]interface{}); ok {
		// Get nested body map
		if m[bodyKey] != nil {
			if body, ok := m[bodyKey].(map[string]interface{}); ok {
				if msg, ok := isJSONStringEqual(textKey, d.Text, body[textKey]); !ok {
					errors = append(errors, msg)
				}
				if msg, ok := isJSONStringEqual(htmlKey, d.HTML, body[htmlKey]); !ok {
					errors = append(errors, msg)
				}
			} else {
				panic(fmt.Sprintf("Expected map[string]interface{} in json key %q, got %T",
					bodyKey, m[bodyKey]))
			}
		} else {
			errors = append(errors, fmt.Sprintf("Expected body in JSON %q but it was nil", bodyKey))
		}
		exDate := d.Date.Format("2006-01-02T15:04:05.999999999-07:00")
		if msg, ok := isJSONStringEqual(dateKey, exDate, m[dateKey]); !ok {
			errors = append(errors, msg)
		}
		if msg, ok := isJSONNumberEqual(sizeKey, float64(d.Size), m[sizeKey]); !ok {
			errors = append(errors, msg)
		}

		// Get nested header map
		if m[headerKey] != nil {
			if header, ok := m[headerKey].(map[string]interface{}); ok {
				// Loop over input (expected) header names
				for name, keyInputHeaders := range d.Header {
					// Make sure expected header name exists in received JSON
					if keyOutputVals, ok := header[name]; ok {
						if keyOutputHeaders, ok := keyOutputVals.([]interface{}); ok {
							// Loop over input (expected) header values
							for _, inputHeader := range keyInputHeaders {
								hasValue := false
								// Look for expected value in received headers
								for _, outputHeader := range keyOutputHeaders {
									if inputHeader == outputHeader {
										hasValue = true
										break
									}
								}
								if !hasValue {
									errors = append(errors, fmt.Sprintf(
										"JSON %v[%q] missing value %q", headerKey, name, inputHeader))
								}
							}
						} else {
							// keyOutputValues was not a slice of interface{}
							panic(fmt.Sprintf("Expected []interface{} in %v[%q], got %T", headerKey,
								name, keyOutputVals))
						}
					} else {
						errors = append(errors, fmt.Sprintf("JSON %v missing key %q", headerKey, name))
					}
				}
			}
		} else {
			errors = append(errors, fmt.Sprintf("Expected header in JSON %q but it was nil", headerKey))
		}
	} else {
		panic(fmt.Sprintf("Expected map[string]interface{} in json, got %T", json))
	}

	return errors
}

func testRestGet(url string) (*httptest.ResponseRecorder, error) {
	req, err := http.NewRequest("GET", url, nil)
	req.Header.Add("Accept", "application/json")
	if err != nil {
		return nil, err
	}

	w := httptest.NewRecorder()
	httpd.Router.ServeHTTP(w, req)
	return w, nil
}

func setupWebServer(ds datastore.DataStore) *bytes.Buffer {
	// Capture log output
	buf := new(bytes.Buffer)
	log.SetOutput(buf)

	// Have to reset default mux to prevent duplicate routes
	http.DefaultServeMux = http.NewServeMux()
	cfg := config.WebConfig{
		TemplateDir: "../themes/bootstrap/templates",
		PublicDir:   "../themes/bootstrap/public",
	}
	shutdownChan := make(chan bool)
	httpd.Initialize(cfg, shutdownChan, ds, &msghub.Hub{})
	SetupRoutes(httpd.Router)

	return buf
}
