package databag

import (
	"encoding/json"
	"errors"
	"github.com/gorilla/mux"
	"github.com/gorilla/websocket"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"sync"
	"time"
)

const testTimeout = 5

type testCondition struct {
	check   func(*TestApp) bool
	channel chan bool
}

type testTopic struct {
	topic Topic
	tags  map[string]*Tag
}

type testChannel struct {
	channel Channel
	topics  map[string]*testTopic
}

//TestContactData holds contact data for a test
type TestContactData struct {
	token               string
	card                Card
	profileRevision     int64
	viewRevision        int64
	articleRevision     int64
	channelRevision     int64
	cardDetailRevision  int64
	cardProfileRevision int64
	articles            map[string]*Article
	channels            map[string]*testChannel
	offsync             bool
}

func (c *TestContactData) updateContact() (err error) {

	if c.cardDetailRevision != c.card.Data.DetailRevision {
		if err = c.updateContactCardDetail(); err != nil {
			return
		}
		c.cardDetailRevision = c.card.Data.DetailRevision
	}

	if c.cardProfileRevision != c.card.Data.ProfileRevision {
		if err = c.updateContactCardProfile(); err != nil {
			return
		}
		c.cardProfileRevision = c.card.Data.ProfileRevision
	}

	// sync rest only if connected
	if c.card.Data.CardDetail.Status != APPCardConnected {
		return
	}

	if c.profileRevision != c.card.Data.NotifiedProfile {
		if err = c.updateContactProfile(); err != nil {
			return
		}
		c.profileRevision = c.card.Data.NotifiedProfile
	}

	if c.viewRevision != c.card.Data.NotifiedView {
		if err = c.updateContactArticle(); err != nil {
			return
		} else if err = c.updateContactChannels(); err != nil {
			return
		}
		c.articleRevision = c.card.Data.NotifiedArticle
		c.channelRevision = c.card.Data.NotifiedChannel
		c.viewRevision = c.card.Data.NotifiedView
	}

	if c.articleRevision != c.card.Data.NotifiedArticle {
		if err = c.updateContactArticle(); err != nil {
			return
		}
		c.articleRevision = c.card.Data.NotifiedArticle
	}

	if c.channelRevision != c.card.Data.NotifiedChannel {
		if err = c.updateContactChannels(); err != nil {
			return
		}
		c.channelRevision = c.card.Data.NotifiedChannel
	}

	return
}

func (c *TestContactData) updateContactProfile() (err error) {
	var msg DataMessage
	token := c.card.Data.CardProfile.GUID + "." + c.card.Data.CardDetail.Token
	params := &TestAPIParams{query: "/profile/message", tokenType: APPTokenContact, token: token}
	response := &TestAPIResponse{data: &msg}
	if err = TestAPIRequest(GetProfileMessage, params, response); err != nil {
		return
	}
	params = &TestAPIParams{restType: "PUT", query: "/contact/cards/{cardID}/profile", tokenType: APPTokenAgent, token: c.token,
		path: map[string]string{"cardID": c.card.ID}, body: &msg}
	response = &TestAPIResponse{}
	if err = TestAPIRequest(SetCardProfile, params, response); err != nil {
		return
	}
	return
}

func (c *TestContactData) updateContactArticle() (err error) {
	var articles []Article
	if c.articleRevision == 0 || c.viewRevision != c.card.Data.NotifiedView {
		token := c.card.Data.CardProfile.GUID + "." + c.card.Data.CardDetail.Token
		params := &TestAPIParams{query: "/articles", tokenType: APPTokenContact, token: token}
		response := &TestAPIResponse{data: &articles}
		if err = TestAPIRequest(GetArticles, params, response); err != nil {
			return
		}
		c.articles = make(map[string]*Article)
	} else {
		token := c.card.Data.CardProfile.GUID + "." + c.card.Data.CardDetail.Token
		viewRevision := strconv.FormatInt(c.viewRevision, 10)
		articleRevision := strconv.FormatInt(c.articleRevision, 10)
		params := &TestAPIParams{query: "/articles?articleRevision=" + articleRevision + "&viewRevision=" + viewRevision,
			tokenType: APPTokenContact, token: token}
		response := &TestAPIResponse{data: &articles}
		if err = TestAPIRequest(GetArticles, params, response); err != nil {
			return
		}
	}
	for _, article := range articles {
		if article.Data == nil {
			delete(c.articles, article.ID)
		} else {
			c.articles[article.ID] = &article
		}
	}
	return nil
}

func (c *TestContactData) updateContactChannels() (err error) {
	var channels []Channel
	if c.channelRevision == 0 || c.viewRevision != c.card.Data.NotifiedView {
		token := c.card.Data.CardProfile.GUID + "." + c.card.Data.CardDetail.Token
		params := &TestAPIParams{query: "/channels", tokenType: APPTokenContact, token: token}
		response := &TestAPIResponse{data: &channels}
		if err = TestAPIRequest(GetChannels, params, response); err != nil {
			return
		}
	} else {
		token := c.card.Data.CardProfile.GUID + "." + c.card.Data.CardDetail.Token
		viewRevision := strconv.FormatInt(c.viewRevision, 10)
		channelRevision := strconv.FormatInt(c.channelRevision, 10)
		params := &TestAPIParams{query: "/channels?channelRevision=" + channelRevision + "&viewRevision=" + viewRevision,
			tokenType: APPTokenContact, token: token}
		response := &TestAPIResponse{data: &channels}
		if err = TestAPIRequest(GetChannels, params, response); err != nil {
			return
		}
	}

	for _, channel := range channels {
		if channel.Data == nil {
			delete(c.channels, channel.ID)
		} else {
			tc, set := c.channels[channel.ID]
			if set {
				if channel.Revision != tc.channel.Revision {
					if err = c.updateContactChannel(tc, &channel); err != nil {
						return
					}
					tc.channel.Revision = channel.Revision
				}
			} else {
				tc := &testChannel{channel: Channel{ID: channel.ID, Data: &ChannelData{}}}
				c.channels[channel.ID] = tc
				if err = c.updateContactChannel(tc, &channel); err != nil {
					return
				}
				tc.channel.Revision = channel.Revision
			}
		}
	}
	return
}

func (c *TestContactData) updateContactChannel(storeChannel *testChannel, channel *Channel) (err error) {
	if storeChannel.channel.Revision != channel.Revision {
		if storeChannel.channel.Data.TopicRevision != channel.Data.TopicRevision {
			if err = c.updateContactChannelTopics(storeChannel); err != nil {
				return
			}
			storeChannel.channel.Data.TopicRevision = channel.Data.TopicRevision
		}
		if channel.Data.ChannelDetail != nil {
			storeChannel.channel.Data.ChannelDetail = channel.Data.ChannelDetail
			storeChannel.channel.Data.DetailRevision = channel.Data.DetailRevision
		} else if storeChannel.channel.Data.DetailRevision != channel.Data.DetailRevision {
			token := c.card.Data.CardProfile.GUID + "." + c.card.Data.CardDetail.Token
			params := &TestAPIParams{query: "/channel/{channelID}", path: map[string]string{"channelID": channel.ID},
				tokenType: APPTokenContact, token: token}
			detail := &ChannelDetail{}
			response := &TestAPIResponse{data: &detail}
			if err = TestAPIRequest(GetChannelDetail, params, response); err != nil {
				return
			}
			if channel.Data == nil {
				err = errors.New("channel removed during update")
				return
			}
			storeChannel.channel.Data.ChannelDetail = detail
			storeChannel.channel.Data.DetailRevision = channel.Data.DetailRevision
		}
	}
	return
}

func (c *TestContactData) updateContactChannelTopics(storeChannel *testChannel) (err error) {
	var topics []Topic
	if storeChannel.channel.Revision == 0 {
		token := c.card.Data.CardProfile.GUID + "." + c.card.Data.CardDetail.Token
		params := &TestAPIParams{query: "/channels/{channelID}/topics",
			path: map[string]string{"channelID": storeChannel.channel.ID}, tokenType: APPTokenContact, token: token}
		response := &TestAPIResponse{data: &topics}
		if err = TestAPIRequest(GetChannelTopics, params, response); err != nil {
			return
		}
		storeChannel.topics = make(map[string]*testTopic)
	} else {
		token := c.card.Data.CardProfile.GUID + "." + c.card.Data.CardDetail.Token
		revision := strconv.FormatInt(storeChannel.channel.Revision, 10)
		params := &TestAPIParams{query: "/channels/{channelID}/topics?revision=" + revision,
			path: map[string]string{"channelID": storeChannel.channel.ID}, tokenType: APPTokenContact, token: token}
		response := &TestAPIResponse{data: &topics}
		if err = TestAPIRequest(GetChannelTopics, params, response); err != nil {
			return
		}
	}

	for _, topic := range topics {
		if topic.Data == nil {
			delete(storeChannel.topics, topic.ID)
		} else {
			storeTopic, set := storeChannel.topics[topic.ID]
			if set {
				if topic.Revision != storeTopic.topic.Revision {
					if err = c.updateContactChannelTopic(storeChannel, storeTopic, &topic); err != nil {
						return
					}
					storeTopic.topic.Revision = topic.Revision
				}
			} else {
				storeTopic := &testTopic{topic: Topic{ID: topic.ID, Data: &TopicData{}}}
				storeChannel.topics[topic.ID] = storeTopic
				if err = c.updateContactChannelTopic(storeChannel, storeTopic, &topic); err != nil {
					return
				}
				storeTopic.topic.Revision = topic.Revision
			}
		}
	}
	return
}

func (c *TestContactData) updateContactChannelTopic(storeChannel *testChannel, storeTopic *testTopic, topic *Topic) (err error) {
	if storeTopic.topic.Revision != topic.Revision {
		if storeTopic.topic.Data.TagRevision != topic.Data.TagRevision {
			if err = c.updateContactChannelTopicTags(storeChannel, storeTopic); err != nil {
				return
			}
			storeTopic.topic.Data.TagRevision = topic.Data.TagRevision
		}
		if topic.Data.TopicDetail != nil {
			storeTopic.topic.Data.TopicDetail = topic.Data.TopicDetail
			storeTopic.topic.Data.DetailRevision = topic.Data.DetailRevision
		} else if storeTopic.topic.Data.DetailRevision != topic.Data.DetailRevision {
			token := c.card.Data.CardProfile.GUID + "." + c.card.Data.CardDetail.Token
			params := &TestAPIParams{query: "/channels/{channelID}/topics/{topicID}",
				path:      map[string]string{"channelID": storeChannel.channel.ID, "topicID": topic.ID},
				tokenType: APPTokenContact, token: token}
			topic := Topic{}
			response := &TestAPIResponse{data: &topic}
			if err = TestAPIRequest(GetChannelTopic, params, response); err != nil {
				return
			}
			if topic.Data == nil {
				err = errors.New("topic removed during update")
				return
			}
			storeTopic.topic.Data.TopicDetail = topic.Data.TopicDetail
			storeTopic.topic.Data.DetailRevision = topic.Data.DetailRevision
		}
	}
	return
}

func (c *TestContactData) updateContactChannelTopicTags(storeChannel *testChannel, storeTopic *testTopic) (err error) {
	var tags []Tag
	if storeTopic.topic.Revision == 0 {
		token := c.card.Data.CardProfile.GUID + "." + c.card.Data.CardDetail.Token
		params := &TestAPIParams{query: "/channels/{channelID}/topics/{topicID}/tags",
			path:      map[string]string{"channelID": storeChannel.channel.ID, "topicID": storeTopic.topic.ID},
			tokenType: APPTokenContact, token: token}
		response := &TestAPIResponse{data: &tags}
		if err = TestAPIRequest(GetChannelTopicTags, params, response); err != nil {
			return
		}
		storeTopic.tags = make(map[string]*Tag)
	} else {
		revision := strconv.FormatInt(storeTopic.topic.Revision, 10)
		token := c.card.Data.CardProfile.GUID + "." + c.card.Data.CardDetail.Token
		params := &TestAPIParams{query: "/channels/{channelID}/topics/{topicID}/tags?revision=" + revision,
			path:      map[string]string{"channelID": storeChannel.channel.ID, "topicID": storeTopic.topic.ID},
			tokenType: APPTokenContact, token: token}
		response := &TestAPIResponse{data: &tags}
		if err = TestAPIRequest(GetChannelTopicTags, params, response); err != nil {
			return
		}
	}

	for _, tag := range tags {
		if tag.Data == nil {
			delete(storeTopic.tags, tag.ID)
		} else {
			storeTopic.tags[tag.ID] = &tag
		}
	}
	return
}

func (c *TestContactData) updateContactCardDetail() (err error) {
	var cardDetail CardDetail
	params := &TestAPIParams{query: "/contact/cards/{cardID}/detail", tokenType: APPTokenAgent, token: c.token,
		path: map[string]string{"cardID": c.card.ID}}
	response := &TestAPIResponse{data: &cardDetail}
	if err = TestAPIRequest(GetCardDetail, params, response); err != nil {
		return
	}
	c.card.Data.CardDetail = &cardDetail
	return
}

func (c *TestContactData) updateContactCardProfile() (err error) {
	var cardProfile CardProfile
	params := &TestAPIParams{query: "/contact/cards/{cardID}/profile", tokenType: APPTokenAgent, token: c.token,
		path: map[string]string{"cardID": c.card.ID}}
	response := &TestAPIResponse{data: &cardProfile}
	if err = TestAPIRequest(GetCardProfile, params, response); err != nil {
		return
	}
	c.card.Data.CardProfile = &cardProfile
	return
}

//NewTestApp allocate a new TestApp structure
func NewTestApp() *TestApp {
	return &TestApp{
		groups:   make(map[string]*Group),
		articles: make(map[string]*Article),
		channels: make(map[string]*testChannel),
		contacts: make(map[string]*TestContactData),
	}
}

//TestApp holds an instance of test app use case
type TestApp struct {
	name     string
	revision Revision
	profile  Profile
	groups   map[string]*Group
	articles map[string]*Article
	channels map[string]*testChannel
	contacts map[string]*TestContactData

	token string

	mutex     sync.Mutex
	condition *testCondition
}

func (a *TestApp) updateProfile() (err error) {
	params := &TestAPIParams{query: "/profile", tokenType: APPTokenAgent, token: a.token}
	response := &TestAPIResponse{data: &a.profile}
	err = TestAPIRequest(GetProfile, params, response)
	return
}

func (a *TestApp) updateGroups() (err error) {
	var groups []Group
	if a.revision.Group == 0 {
		params := &TestAPIParams{query: "/groups", tokenType: APPTokenAgent, token: a.token}
		response := &TestAPIResponse{data: &groups}
		if err = TestAPIRequest(GetGroups, params, response); err != nil {
			return
		}
	} else {
		revision := strconv.FormatInt(a.revision.Group, 10)
		params := &TestAPIParams{query: "/groups?revision=" + revision, tokenType: APPTokenAgent, token: a.token}
		response := &TestAPIResponse{data: &groups}
		if err = TestAPIRequest(GetGroups, params, response); err != nil {
			return
		}
	}
	for _, group := range groups {
		if group.Data == nil {
			delete(a.groups, group.ID)
		} else {
			a.groups[group.ID] = &group
		}
	}
	return
}

func (a *TestApp) updateArticles() (err error) {
	var articles []Article
	if a.revision.Article == 0 {
		params := &TestAPIParams{query: "/articles", tokenType: APPTokenAgent, token: a.token}
		response := &TestAPIResponse{data: &articles}
		if err = TestAPIRequest(GetArticles, params, response); err != nil {
			return
		}
	} else {
		revision := strconv.FormatInt(a.revision.Article, 10)
		params := &TestAPIParams{query: "/articles?articleRevision=" + revision, tokenType: APPTokenAgent, token: a.token}
		response := &TestAPIResponse{data: &articles}
		if err = TestAPIRequest(GetArticles, params, response); err != nil {
			return
		}
	}
	for _, article := range articles {
		if article.Data == nil {
			delete(a.articles, article.ID)
		} else {
			a.articles[article.ID] = &article
		}
	}
	return
}

func (a *TestApp) updateChannels() (err error) {
	var channels []Channel
	if a.revision.Channel == 0 {
		params := &TestAPIParams{query: "/channels", tokenType: APPTokenAgent, token: a.token}
		response := &TestAPIResponse{data: &channels}
		if err = TestAPIRequest(GetChannels, params, response); err != nil {
			return
		}
	} else {
		revision := strconv.FormatInt(a.revision.Channel, 10)
		params := &TestAPIParams{query: "/channels?channelRevision=" + revision, tokenType: APPTokenAgent, token: a.token}
		response := &TestAPIResponse{data: &channels}
		if err = TestAPIRequest(GetChannels, params, response); err != nil {
			return
		}
	}

	for _, channel := range channels {
		if channel.Data == nil {
			delete(a.channels, channel.ID)
		} else {
			storeChannel, set := a.channels[channel.ID]
			if set {
				if channel.Revision != storeChannel.channel.Revision {
					if err = a.updateChannel(storeChannel, &channel); err != nil {
						return
					}
					storeChannel.channel.Revision = channel.Revision
				}
			} else {
				storeChannel := &testChannel{channel: Channel{ID: channel.ID, Data: &ChannelData{}}}
				a.channels[channel.ID] = storeChannel
				if err = a.updateChannel(storeChannel, &channel); err != nil {
					return
				}
				storeChannel.channel.Revision = channel.Revision
			}
		}
	}
	return
}

func (a *TestApp) updateChannel(storeChannel *testChannel, channel *Channel) (err error) {
	if storeChannel.channel.Revision != channel.Revision {
		if storeChannel.channel.Data.TopicRevision != channel.Data.TopicRevision {
			if err = a.updateChannelTopics(storeChannel); err != nil {
				return
			}
			storeChannel.channel.Data.TopicRevision = channel.Data.TopicRevision
		}
		if channel.Data.ChannelDetail != nil {
			storeChannel.channel.Data.ChannelDetail = channel.Data.ChannelDetail
			storeChannel.channel.Data.DetailRevision = channel.Data.DetailRevision
		} else if storeChannel.channel.Data.DetailRevision != channel.Data.DetailRevision {
			params := &TestAPIParams{query: "/channel/{channelID}", path: map[string]string{"channelID": channel.ID},
				tokenType: APPTokenAgent, token: a.token}
			detail := &ChannelDetail{}
			response := &TestAPIResponse{data: &detail}
			if err = TestAPIRequest(GetChannelDetail, params, response); err != nil {
				return
			}
			if channel.Data == nil {
				err = errors.New("channel removed during update")
				return
			}
			storeChannel.channel.Data.ChannelDetail = detail
			storeChannel.channel.Data.DetailRevision = channel.Data.DetailRevision
		}
	}
	return
}

func (a *TestApp) updateChannelTopics(storeChannel *testChannel) (err error) {
	var topics []Topic
	if storeChannel.channel.Revision == 0 {
		params := &TestAPIParams{query: "/channels/{channelID}/topics",
			path: map[string]string{"channelID": storeChannel.channel.ID}, tokenType: APPTokenAgent, token: a.token}
		response := &TestAPIResponse{data: &topics}
		if err = TestAPIRequest(GetChannelTopics, params, response); err != nil {
			return
		}
		storeChannel.topics = make(map[string]*testTopic)
	} else {
		revision := strconv.FormatInt(storeChannel.channel.Revision, 10)
		params := &TestAPIParams{query: "/channels/{channelID}/topics?revision=" + revision,
			path: map[string]string{"channelID": storeChannel.channel.ID}, tokenType: APPTokenAgent, token: a.token}
		response := &TestAPIResponse{data: &topics}
		if err = TestAPIRequest(GetChannelTopics, params, response); err != nil {
			return
		}
	}

	for _, topic := range topics {
		if topic.Data == nil {
			delete(storeChannel.topics, topic.ID)
		} else {
			storeTopic, set := storeChannel.topics[topic.ID]
			if set {
				if topic.Revision != storeTopic.topic.Revision {
					if err = a.updateChannelTopic(storeChannel, storeTopic, &topic); err != nil {
						return
					}
					storeTopic.topic.Revision = topic.Revision
				}
			} else {
				storeTopic := &testTopic{topic: Topic{ID: topic.ID, Data: &TopicData{}}}
				storeChannel.topics[topic.ID] = storeTopic
				if err = a.updateChannelTopic(storeChannel, storeTopic, &topic); err != nil {
					return
				}
				storeTopic.topic.Revision = topic.Revision
			}
		}
	}
	return
}

func (a *TestApp) updateChannelTopic(storeChannel *testChannel, storeTopic *testTopic, topic *Topic) (err error) {
	if storeTopic.topic.Revision != topic.Revision {
		if storeTopic.topic.Data.TagRevision != topic.Data.TagRevision {
			if err = a.updateChannelTopicTags(storeChannel, storeTopic); err != nil {
				return
			}
			storeTopic.topic.Data.TagRevision = topic.Data.TagRevision
		}
		if topic.Data.TopicDetail != nil {
			storeTopic.topic.Data.TopicDetail = topic.Data.TopicDetail
			storeTopic.topic.Data.DetailRevision = topic.Data.DetailRevision
		} else if storeTopic.topic.Data.DetailRevision != topic.Data.DetailRevision {
			params := &TestAPIParams{query: "/channels/{channelID}/topics/{topicID}",
				path:      map[string]string{"channelID": storeChannel.channel.ID, "topicID": topic.ID},
				tokenType: APPTokenAgent, token: a.token}
			topic := Topic{}
			response := &TestAPIResponse{data: &topic}
			if err = TestAPIRequest(GetChannelTopic, params, response); err != nil {
				return
			}
			if topic.Data == nil {
				err = errors.New("topic removed during update")
				return
			}
			storeTopic.topic.Data.TopicDetail = topic.Data.TopicDetail
			storeTopic.topic.Data.DetailRevision = topic.Data.DetailRevision
		}
	}
	return
}

func (a *TestApp) updateChannelTopicTags(storeChannel *testChannel, storeTopic *testTopic) (err error) {
	var tags []Tag
	if storeTopic.topic.Revision == 0 {
		params := &TestAPIParams{query: "/channels/{channelID}/topics/{topicID}/tags",
			path:      map[string]string{"channelID": storeChannel.channel.ID, "topicID": storeTopic.topic.ID},
			tokenType: APPTokenAgent, token: a.token}
		response := &TestAPIResponse{data: &tags}
		if err = TestAPIRequest(GetChannelTopicTags, params, response); err != nil {
			return
		}
		storeTopic.tags = make(map[string]*Tag)
	} else {
		revision := strconv.FormatInt(storeTopic.topic.Revision, 10)
		params := &TestAPIParams{query: "/channels/{channelID}/topics/{topicID}/tags?revision=" + revision,
			path:      map[string]string{"channelID": storeChannel.channel.ID, "topicID": storeTopic.topic.ID},
			tokenType: APPTokenAgent, token: a.token}
		response := &TestAPIResponse{data: &tags}
		if err = TestAPIRequest(GetChannelTopicTags, params, response); err != nil {
			return
		}
	}

	for _, tag := range tags {
		if tag.Data == nil {
			delete(storeTopic.tags, tag.ID)
		} else {
			storeTopic.tags[tag.ID] = &tag
		}
	}
	return
}

func (a *TestApp) updateCards() (err error) {
	var cards []Card
	if a.revision.Card == 0 {
		params := &TestAPIParams{query: "/cards", tokenType: APPTokenAgent, token: a.token}
		response := &TestAPIResponse{data: &cards}
		if err = TestAPIRequest(GetCards, params, response); err != nil {
			return
		}
		for _, card := range cards {
			if card.Data == nil {
				delete(a.contacts, card.ID)
			} else {
				// set new card
				contactData := &TestContactData{card: card, articles: make(map[string]*Article), channels: make(map[string]*testChannel),
					cardDetailRevision: card.Data.DetailRevision, cardProfileRevision: card.Data.ProfileRevision,
					profileRevision: card.Data.ProfileRevision, token: a.token}
				a.contacts[card.ID] = contactData
				if err = contactData.updateContact(); err != nil {
					contactData.offsync = true
					PrintMsg(err)
				}
			}
		}
	} else {
		revision := strconv.FormatInt(a.revision.Card, 10)
		params := &TestAPIParams{query: "/cards?revision=" + revision, tokenType: APPTokenAgent, token: a.token}
		response := &TestAPIResponse{data: &cards}
		if err = TestAPIRequest(GetCards, params, response); err != nil {
			return
		}
		for _, card := range cards {
			if card.Data == nil {
				delete(a.contacts, card.ID)
			} else {
				contactData, set := a.contacts[card.ID]
				if !set {
					// download new card
					params := &TestAPIParams{query: "/cards/{cardID}", path: map[string]string{"cardID": card.ID},
						tokenType: APPTokenAgent, token: a.token}
					response := &TestAPIResponse{data: &card}
					if err = TestAPIRequest(GetCard, params, response); err != nil {
						return
					}
					contactData := &TestContactData{card: card, articles: make(map[string]*Article), channels: make(map[string]*testChannel),
						cardDetailRevision: card.Data.DetailRevision, cardProfileRevision: card.Data.ProfileRevision,
						profileRevision: card.Data.ProfileRevision, token: a.token}
					a.contacts[card.ID] = contactData
					if err = contactData.updateContact(); err != nil {
						contactData.offsync = true
						PrintMsg(err)
					}
				} else {
					// update existing card revisions
					contactData.card.Data.NotifiedProfile = card.Data.NotifiedProfile
					contactData.card.Data.NotifiedArticle = card.Data.NotifiedArticle
					contactData.card.Data.NotifiedChannel = card.Data.NotifiedChannel
					contactData.card.Data.NotifiedView = card.Data.NotifiedView
					contactData.card.Data.DetailRevision = card.Data.DetailRevision
					contactData.card.Data.ProfileRevision = card.Data.ProfileRevision
					if err = contactData.updateContact(); err != nil {
						contactData.offsync = true
						PrintMsg(err)
					}
				}
			}
		}
	}
	return
}

func (a *TestApp) updateApp(rev *Revision) {
	a.mutex.Lock()
	defer a.mutex.Unlock()

	if rev.Profile != a.revision.Profile {
		if err := a.updateProfile(); err != nil {
			PrintMsg(err)
		} else {
			a.revision.Profile = rev.Profile
		}
	}

	if rev.Group != a.revision.Group {
		if err := a.updateGroups(); err != nil {
			PrintMsg(err)
		} else {
			a.revision.Group = rev.Group
		}
	}

	if rev.Article != a.revision.Article {
		if err := a.updateArticles(); err != nil {
			PrintMsg(err)
		} else {
			a.revision.Article = rev.Article
		}
	}

	if rev.Card != a.revision.Card {
		if err := a.updateCards(); err != nil {
			PrintMsg(err)
		} else {
			a.revision.Card = rev.Card
		}
	}

	if rev.Channel != a.revision.Channel {
		if err := a.updateChannels(); err != nil {
			PrintMsg(err)
		} else {
			a.revision.Channel = rev.Channel
		}
	}

	if a.condition != nil {
		if a.condition.check(a) {
			select {
			case a.condition.channel <- true:
			default:
			}
		}
	}
}

//Connect provides a test handler for the websocket connection
func (a *TestApp) Connect(token string) error {
	var revision Revision
	var data []byte
	var dataType int

	a.token = token

	// connect websocket
	ws, err := StatusConnection(token, &revision)
	if err != nil {
		return err
	}
	a.updateApp(&revision)

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

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

}

func (a *TestApp) setCondition(test *testCondition) {
	a.mutex.Lock()
	if test.check(a) {
		test.channel <- true
	} else {
		a.condition = test
	}
	a.mutex.Unlock()
}

func (a *TestApp) clearCondition() {
	a.mutex.Lock()
	a.condition = nil
	a.mutex.Unlock()
}

//WaitFor provides a helper waiting for a condition to be met
func (a *TestApp) WaitFor(check func(*TestApp) bool) error {
	var done = make(chan bool, 1)
	var wake = make(chan bool, 1)
	a.setCondition(&testCondition{channel: done, check: check})
	go func() {
		time.Sleep(testTimeout * time.Second)
		wake <- true
	}()
	select {
	case <-done:
		a.clearCondition()
		return nil
	case <-wake:
		a.clearCondition()
		return errors.New("timeout waiting for condition")
	}
}

/*** endpoint test function ***/

//TestAPIParams holds the config for an endpoint test
type TestAPIParams struct {
	restType      string
	path          map[string]string
	query         string
	body          interface{}
	tokenType     string
	token         string
	authorization string
	credentials   string
}

//TestAPIResponse holds the endpoint test response
type TestAPIResponse struct {
	code   int
	data   interface{}
	header map[string][]string
}

//TestAPIRequest tests and endpoint with provided params
func TestAPIRequest(endpoint func(http.ResponseWriter, *http.Request), params *TestAPIParams, resp *TestAPIResponse) (err error) {

	var r *http.Request
	var w *httptest.ResponseRecorder
	rest := params.restType
	if rest == "" {
		rest = "GET"
	}

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

	if r, w, err = NewRequest(rest, params.query, params.body); err != nil {
		return
	}
	r = mux.SetURLVars(r, params.path)

	if params.tokenType != "" {
		r.Header.Add("TokenType", params.tokenType)
	}
	if params.token != "" {
		SetBearerAuth(r, params.token)
	}

	if params.authorization != "" {
		SetBasicAuth(r, params.authorization)
	}
	if params.credentials != "" {
		SetCredentials(r, params.credentials)
	}

	endpoint(w, r)

	res := w.Result()
	if res.StatusCode != 200 && res.StatusCode != 410 {
		err = errors.New("response failed")
		return
	}
	if resp != nil {
		resp.header = res.Header
		if resp.data != nil {
			dec := json.NewDecoder(res.Body)
			if err = dec.Decode(resp.data); err != nil {
				return
			}
		}
	}
	return
}