package databag

import (
	"bytes"
	"encoding/json"
	"errors"
	"github.com/gorilla/mux"
	"github.com/gorilla/websocket"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"strings"
	"time"
)

const testReadDeadline = 2
const testRevisionWait = 100

type testCard struct {
	GUID    string
	Token   string
	CardID  string
	GroupID string
}

type testContact struct {
	GUID      string
	Token     string
	Revisions chan *Revision
	A         testCard
	B         testCard
	C         testCard
	D         testCard
}

//TestGroup provides the setup accounts for a test case
type TestGroup struct {
	A testContact
	B testContact
	C testContact
	D testContact
}

//GetTestRevision retrieves the notified revisions
func GetTestRevision(status chan *Revision) (rev *Revision) {
	time.Sleep(testRevisionWait * time.Millisecond)
	for {
		select {
		case r := <-status:
			rev = r
		default:
			return
		}
	}
}

//APITestData is a helper function for testing asset download
func APITestData(
	endpoint func(http.ResponseWriter, *http.Request),
	requestType string,
	name string,
	params *map[string]string,
	body interface{},
	tokenType string,
	token string,
	start int64,
	end int64,
) (data []byte, hdr map[string][]string, err error) {

	var r *http.Request
	var w *httptest.ResponseRecorder

	if tokenType == APPTokenAgent {
		if !strings.Contains(name, "?") {
			name += "?"
		} else {
			name += "&"
		}
		name += "agent=" + token
	} else if tokenType == APPTokenContact {
		if !strings.Contains(name, "?") {
			name += "?"
		} else {
			name += "&"
		}
		name += "contact=" + token
	}

	if r, w, err = NewRequest(requestType, name, body); err != nil {
		return
	}
	if params != nil {
		r = mux.SetURLVars(r, *params)
	}
	if token != "" {
		r.Header.Add("TokenType", tokenType)
		SetBearerAuth(r, token)
	}
	if start != 0 || end != 0 {
		byteRange := "bytes=" + strconv.FormatInt(start, 10) + "-" + strconv.FormatInt(end, 10)
		r.Header.Add("Range", byteRange)
	}
	endpoint(w, r)

	resp := w.Result()
	if resp.StatusCode != 200 && resp.StatusCode != 206 {
		err = errors.New("response failed")
		return
	}
	hdr = resp.Header
	data, err = ioutil.ReadAll(resp.Body)
	return
}

//APITestMsg is a helper function for an endpoint test
func APITestMsg(
	endpoint func(http.ResponseWriter, *http.Request),
	requestType string,
	name string,
	params *map[string]string,
	body interface{},
	tokenType string,
	token string,
	response interface{},
	responseHeader *map[string][]string,
) (err error) {

	var r *http.Request
	var w *httptest.ResponseRecorder

	if tokenType == APPTokenAgent {
		if !strings.Contains(name, "?") {
			name += "?"
		} else {
			name += "&"
		}
		name += "agent=" + token
	} else if tokenType == APPTokenContact {
		if !strings.Contains(name, "?") {
			name += "?"
		} else {
			name += "&"
		}
		name += "contact=" + token
	}

	if r, w, err = NewRequest(requestType, name, body); err != nil {
		return
	}
	if params != nil {
		r = mux.SetURLVars(r, *params)
	}
	if tokenType != "" {
		r.Header.Add("TokenType", tokenType)
	}
	if token != "" {
		SetBearerAuth(r, token)
	}
	endpoint(w, r)

	resp := w.Result()
	if resp.StatusCode != 200 {
		err = errors.New("response failed")
		return
	}
	if responseHeader != nil {
		*responseHeader = resp.Header
	}
	if response == nil {
		return
	}
	dec := json.NewDecoder(resp.Body)
	dec.Decode(response)
	return
}

//APITestUpload is a helper function for asset upload test
func APITestUpload(
	endpoint func(http.ResponseWriter, *http.Request),
	requestType string,
	name string,
	params *map[string]string,
	body []byte,
	tokenType string,
	token string,
	response interface{},
	responseHeader *map[string][]string,
) (err error) {

	data := bytes.Buffer{}
	writer := multipart.NewWriter(&data)
	part, err := writer.CreateFormFile("asset", "asset")
	if err != nil {
		return err
	}
	part.Write(body)
	if err = writer.Close(); err != nil {
		return
	}

	if tokenType == APPTokenAgent {
		if !strings.Contains(name, "?") {
			name += "?"
		} else {
			name += "&"
		}
		name += "agent=" + token
	} else if tokenType == APPTokenContact {
		if !strings.Contains(name, "?") {
			name += "?"
		} else {
			name += "&"
		}
		name += "contact=" + token
	}

	w := httptest.NewRecorder()
	r := httptest.NewRequest(requestType, name, &data)

	if params != nil {
		r = mux.SetURLVars(r, *params)
	}
	r.Header.Set("Content-Type", writer.FormDataContentType())
	endpoint(w, r)

	resp := w.Result()
	if resp.StatusCode != 200 {
		err = errors.New("response failed")
		return
	}
	if responseHeader != nil {
		*responseHeader = resp.Header
	}
	if response == nil {
		return
	}
	dec := json.NewDecoder(resp.Body)
	dec.Decode(response)
	return
}

//
//    A --- connected,group               connected,group --- B
//    | \                                                    /|
//    |   requested,nogroup                  confirmed,group  |
//    |                                                       |
//     connected,group                                       ,
//                                  |
//                                --x--
//                                  |
//     connected,group                                       ,
//    |                                                       |
//    |  ,                                   pending,nogroup  |
//    |/                                                     \|
//    C --- connected,group               connected,group --- D
//

// AddTestGroup creates and connects test accounts for useCases
func AddTestGroup(prefix string) (*TestGroup, error) {
	var err error
	var rev *Revision
	var ws *websocket.Conn

	// allocate contacts
	g := &TestGroup{}
	if g.A.GUID, g.A.Token, err = addTestAccount(prefix + "A"); err != nil {
		return g, err
	}
	if g.B.GUID, g.B.Token, err = addTestAccount(prefix + "B"); err != nil {
		return g, err
	}
	if g.C.GUID, g.C.Token, err = addTestAccount(prefix + "C"); err != nil {
		return g, err
	}
	if g.D.GUID, g.D.Token, err = addTestAccount(prefix + "D"); err != nil {
		return g, err
	}

	// setup A
	if g.A.B.CardID, err = addTestCard(g.A.Token, g.B.Token); err != nil {
		return g, err
	}
	if err = openTestCard(g.A.Token, g.A.B.CardID); err != nil {
		return g, err
	}
	if g.A.B.GroupID, err = groupTestCard(g.A.Token, g.A.B.CardID); err != nil {
		return g, err
	}
	if g.A.C.CardID, err = addTestCard(g.A.Token, g.C.Token); err != nil {
		return g, err
	}
	if err = openTestCard(g.A.Token, g.A.C.CardID); err != nil {
		return g, err
	}
	if g.A.C.GroupID, err = groupTestCard(g.A.Token, g.A.C.CardID); err != nil {
		return g, err
	}
	if g.A.D.CardID, err = addTestCard(g.A.Token, g.D.Token); err != nil {
		return g, err
	}
	if err = openTestCard(g.A.Token, g.A.D.CardID); err != nil {
		return g, err
	}

	// setup B
	if g.B.A.CardID, err = addTestCard(g.B.Token, g.A.Token); err != nil {
		return g, err
	}
	if err = openTestCard(g.B.Token, g.B.A.CardID); err != nil {
		return g, err
	}
	if g.B.A.GroupID, err = groupTestCard(g.B.Token, g.B.A.CardID); err != nil {
		return g, err
	}
	if g.B.C.CardID, err = addTestCard(g.B.Token, g.C.Token); err != nil {
		return g, err
	}
	if g.B.C.GroupID, err = groupTestCard(g.B.Token, g.B.C.CardID); err != nil {
		return g, err
	}

	// setup C
	if g.C.D.CardID, err = addTestCard(g.C.Token, g.D.Token); err != nil {
		return g, err
	}
	if err = openTestCard(g.C.Token, g.C.D.CardID); err != nil {
		return g, err
	}
	if g.C.D.GroupID, err = groupTestCard(g.C.Token, g.C.D.CardID); err != nil {
		return g, err
	}
	if g.C.A.CardID, err = addTestCard(g.C.Token, g.A.Token); err != nil {
		return g, err
	}
	if err = openTestCard(g.C.Token, g.C.A.CardID); err != nil {
		return g, err
	}
	if g.C.A.GroupID, err = groupTestCard(g.C.Token, g.C.A.CardID); err != nil {
		return g, err
	}

	// setup D
	if g.D.C.CardID, err = addTestCard(g.D.Token, g.C.Token); err != nil {
		return g, err
	}
	if err = openTestCard(g.D.Token, g.D.C.CardID); err != nil {
		return g, err
	}
	if g.D.C.GroupID, err = groupTestCard(g.D.Token, g.D.C.CardID); err != nil {
		return g, err
	}
	if g.D.A.CardID, err = getCardID(g.D.Token, g.A.GUID); err != nil {
		return g, err
	}

	// get contact tokens
	if g.A.B.Token, err = getCardToken(g.A.Token, g.A.B.CardID); err != nil {
		return g, err
	}
	if g.B.A.Token, err = getCardToken(g.B.Token, g.B.A.CardID); err != nil {
		return g, err
	}
	if g.C.A.Token, err = getCardToken(g.C.Token, g.C.A.CardID); err != nil {
		return g, err
	}
	if g.C.D.Token, err = getCardToken(g.C.Token, g.C.D.CardID); err != nil {
		return g, err
	}
	if g.D.C.Token, err = getCardToken(g.D.Token, g.D.C.CardID); err != nil {
		return g, err
	}

	// connect websockets
	rev = &Revision{}
	if ws, err = StatusConnection(g.A.Token, rev); err != nil {
		return g, err
	}
	g.A.Revisions = make(chan *Revision, 64)
	g.A.Revisions <- rev
	go monitorStatus(ws, &g.A)
	rev = &Revision{}
	if ws, err = StatusConnection(g.B.Token, rev); err != nil {
		return g, err
	}
	g.B.Revisions = make(chan *Revision, 64)
	g.B.Revisions <- rev
	go monitorStatus(ws, &g.B)
	rev = &Revision{}
	if ws, err = StatusConnection(g.C.Token, rev); err != nil {
		return g, err
	}
	g.C.Revisions = make(chan *Revision, 64)
	g.C.Revisions <- rev
	go monitorStatus(ws, &g.C)
	rev = &Revision{}
	if ws, err = StatusConnection(g.D.Token, rev); err != nil {
		return g, err
	}
	g.D.Revisions = make(chan *Revision, 64)
	g.D.Revisions <- rev
	go monitorStatus(ws, &g.D)

	return g, nil
}

func monitorStatus(ws *websocket.Conn, contact *testContact) {
	var data []byte
	var dataType int
	var err error

	// reset any timeout
	ws.SetReadDeadline(time.Time{})

	// read revision update
	for {
		if dataType, data, err = ws.ReadMessage(); err != nil {
			LogMsg("failed to read status connection")
			return
		}
		if dataType != websocket.TextMessage {
			LogMsg("invalid status data type")
			return
		}
		rev := &Revision{}
		if err = json.Unmarshal(data, rev); err != nil {
			LogMsg("invalid status data")
			return
		}
		contact.Revisions <- rev
	}
}

func getCardToken(account string, cardID string) (token string, err error) {
	var r *http.Request
	var w *httptest.ResponseRecorder
	var cardDetail CardDetail
	var cardProfile CardProfile
	vars := make(map[string]string)
	vars["cardID"] = cardID

	if r, w, err = NewRequest("GET", "/contact/cards/{cardID}/detail?agent="+account, nil); err != nil {
		return
	}
	r = mux.SetURLVars(r, vars)
	GetCardDetail(w, r)
	if err = ReadResponse(w, &cardDetail); err != nil {
		return
	}
	if cardDetail.Status != APPCardConnected {
		err = errors.New("card not connected")
		return
	}

	if r, w, err = NewRequest("GET", "/contact/cards/{cardID}/profile?agent="+account, nil); err != nil {
		return
	}
	r = mux.SetURLVars(r, vars)
	GetCardProfile(w, r)
	if err = ReadResponse(w, &cardProfile); err != nil {
		return
	}

	token = cardProfile.GUID + "." + cardDetail.Token
	return
}

func getCardID(account string, guid string) (cardID string, err error) {
	var r *http.Request
	var w *httptest.ResponseRecorder
	var cards []Card

	if r, w, err = NewRequest("GET", "/contact/cards?agent="+account, nil); err != nil {
		return
	}
	GetCards(w, r)
	if err = ReadResponse(w, &cards); err != nil {
		return
	}

	for _, card := range cards {
		if card.Data.CardProfile.GUID == guid {
			cardID = card.ID
			return
		}
	}
	err = errors.New("card not found")
	return
}

func groupTestCard(account string, cardID string) (groupID string, err error) {
	var r *http.Request
	var w *httptest.ResponseRecorder
	var subject *Subject
	var group Group
	var cardData CardData
	vars := make(map[string]string)

	// add new group
	subject = &Subject{
		DataType: "imagroup",
		Data:     "group data with name and logo",
	}
	if r, w, err = NewRequest("POST", "/share/groups?agent="+account, subject); err != nil {
		return
	}
	AddGroup(w, r)
	if err = ReadResponse(w, &group); err != nil {
		return
	}
	groupID = group.ID

	// set contact group
	if r, w, err = NewRequest("PUT", "/contact/cards/{cardID}/groups/{groupID}?agent="+account, nil); err != nil {
		return
	}
	vars["groupID"] = group.ID
	vars["cardID"] = cardID
	r = mux.SetURLVars(r, vars)
	SetCardGroup(w, r)
	if err = ReadResponse(w, &cardData); err != nil {
		return
	}
	return
}

func openTestCard(account string, cardID string) (err error) {
	var r *http.Request
	var w *httptest.ResponseRecorder
	var msg DataMessage
	var card Card
	var vars = map[string]string{"cardID": cardID}
	var contactStatus ContactStatus

	// set to connecting state
	if r, w, err = NewRequest("PUT", "/contact/cards/{cardID}/status?agent="+account, APPCardConnecting); err != nil {
		return
	}
	r = mux.SetURLVars(r, vars)
	SetCardStatus(w, r)
	if err = ReadResponse(w, &card); err != nil {
		return
	}

	// get open message
	if r, w, err = NewRequest("GET", "/contact/cards/{cardID}/openMessage?agent="+account, nil); err != nil {
		return
	}
	r = mux.SetURLVars(r, vars)
	GetOpenMessage(w, r)
	if err = ReadResponse(w, &msg); err != nil {
		return
	}

	// set open message
	if r, w, err = NewRequest("PUT", "/contact/openMessage", msg); err != nil {
		return
	}
	SetOpenMessage(w, r)
	if err = ReadResponse(w, &contactStatus); err != nil {
		return
	}

	// update status if connected
	if contactStatus.Status == APPCardConnected {
		view := "viewRevision=" + strconv.FormatInt(contactStatus.ViewRevision, 10)
		article := "articleRevision=" + strconv.FormatInt(contactStatus.ArticleRevision, 10)
		channel := "channelRevision=" + strconv.FormatInt(contactStatus.ChannelRevision, 10)
		profile := "profileRevision=" + strconv.FormatInt(contactStatus.ProfileRevision, 10)
		if r, w, err = NewRequest("PUT", "/contact/cards/{cardID}/status?agent="+account+"&token="+contactStatus.Token+"&"+view+"&"+article+"&"+channel+"&"+profile, APPCardConnected); err != nil {
			return
		}
		r = mux.SetURLVars(r, vars)
		SetCardStatus(w, r)
		if err = ReadResponse(w, &card); err != nil {
			return
		}
	}
	return
}

func addTestCard(account string, contact string) (cardID string, err error) {
	var r *http.Request
	var w *httptest.ResponseRecorder
	var msg DataMessage
	var card Card

	// get A identity message
	if r, w, err = NewRequest("GET", "/profile/message?agent="+contact, nil); err != nil {
		return
	}
	GetProfileMessage(w, r)
	if err = ReadResponse(w, &msg); err != nil {
		return
	}

	// add A card in B
	if r, w, err = NewRequest("POST", "/contact/cards?agent="+account, &msg); err != nil {
		return
	}
	AddCard(w, r)
	if err = ReadResponse(w, &card); err != nil {
		return
	}
	cardID = card.ID
	return
}

func addTestAccount(username string) (guid string, token string, err error) {
	var r *http.Request
	var w *httptest.ResponseRecorder

	var access LoginAccess
	var claim Claim
	var msg DataMessage
	var profile Profile
	var login = username + ":pass"

  // admin login
  r, w, _ = NewRequest("PUT", "/admin/access?token=pass", nil);
  SetAdminAccess(w, r)
  var session string
  if ReadResponse(w, &session) != nil {
    panic("failed to login as admin")
  }

	// get account token
	if r, w, err = NewRequest("POST", "/admin/accounts?token=" + session, nil); err != nil {
		return
	}
	AddNodeAccount(w, r)
	if err = ReadResponse(w, &access); err != nil {
		return
	}

	// set account profile
	if r, w, err = NewRequest("POST", "/account/profile", nil); err != nil {
		return
	}
	SetBearerAuth(r, access.AppToken)
	SetCredentials(r, login)
	AddAccount(w, r)
	if err = ReadResponse(w, &profile); err != nil {
		return
	}
	guid = profile.GUID

	// acquire new token for attaching app
  notifications := []Notification{}
	if r, w, err = NewRequest("POST", "/account/apps", &notifications); err != nil {
		return
	}
	SetBasicAuth(r, login)
	AddAccountApp(w, r)
	if err = ReadResponse(w, &access); err != nil {
		return
	}
	token = access.AppToken

  // set to searchable
  searchable := false
  if r, w, err = NewRequest("PUT", "/account/searchable?agent="+token, &searchable); err != nil {
    return
  }
  SetAccountSearchable(w, r);

	// authorize claim
	if r, w, err = NewRequest("PUT", "/authorize?agent="+token, "1234abcd"); err != nil {
		return
	}
	Authorize(w, r)
	if err = ReadResponse(w, &msg); err != nil {
		return
	}
	signer, messageType, _, res := ReadDataMessage(&msg, &claim)
	if res != nil || signer != guid || messageType != APPMsgAuthenticate || claim.Token != "1234abcd" {
		err = errors.New("invalid authenticated claim")
		return
	}
	return
}

//NewRequest provides a lower level helper function for an endpoint test
func NewRequest(rest string, path string, obj interface{}) (*http.Request, *httptest.ResponseRecorder, error) {
	w := httptest.NewRecorder()
	if obj != nil {
		body, err := json.Marshal(obj)
		if err != nil {
			return nil, nil, err
		}
		reader := strings.NewReader(string(body))
		return httptest.NewRequest(rest, path, reader), w, nil
	}

	return httptest.NewRequest(rest, path, nil), w, nil
}

// Websocket test support
type statusHandler struct{}

func (h *statusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	Status(w, r)
}

//StatusConnection provides a test interface for the websocket updates
func StatusConnection(token string, rev *Revision) (ws *websocket.Conn, err error) {
	var data []byte
	var dataType int

	// connect to websocket
	s := httptest.NewServer(&statusHandler{})
	wsURL, _ := url.Parse(s.URL)
	wsURL.Scheme = "ws"
	if ws, _, err = websocket.DefaultDialer.Dial(wsURL.String(), nil); err != nil {
		return
	}

	// send authentication
	announce := Announce{AppToken: token}
	if data, err = json.Marshal(&announce); err != nil {
		return
	}
	ws.WriteMessage(websocket.TextMessage, data)

	// read revision response
	ws.SetReadDeadline(time.Now().Add(testReadDeadline * time.Second))
	if dataType, data, err = ws.ReadMessage(); err != nil {
		return
	}
	if dataType != websocket.TextMessage {
		err = errors.New("invalid status data type")
		return
	}
	if err = json.Unmarshal(data, rev); err != nil {
		return
	}
	return
}
