// Copyright 2012-present Oliver Eilhard. All rights reserved.
// Use of this source code is governed by a MIT-license.
// See http://olivere.mit-license.org/license.txt for details.

package elastic

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"os"
	"time"
)

const (
	testIndexName  = "elastic-test"
	testIndexName2 = "elastic-test2"
	testIndexName3 = "elastic-test3"
	testIndexName4 = "elastic-test4"
	testMapping    = `
{
	"settings":{
		"number_of_shards":1,
		"number_of_replicas":0
	},
	"mappings":{
		"doc":{
			"properties":{
				"user":{
					"type":"keyword"
				},
				"message":{
					"type":"text",
					"store": true,
					"fielddata": true
				},
				"tags":{
					"type":"keyword"
				},
				"location":{
					"type":"geo_point"
				},
				"suggest_field":{
					"type":"completion",
					"contexts":[
						{
							"name":"user_name",
							"type":"category"
						}
					]
				}
			}
		}
	}
}
`

	testNoSourceIndexName = "elastic-nosource-test"
	testNoSourceMapping   = `
{
	"settings":{
		"number_of_shards":1,
		"number_of_replicas":0
	},
	"mappings":{
		"doc":{
			"_source": {
				"enabled": false
			},
			"properties":{
				"user":{
					"type":"keyword"
				},
				"message":{
					"type":"text",
					"store": true,
					"fielddata": true
				},
				"tags":{
					"type":"keyword"
				},
				"location":{
					"type":"geo_point"
				},
				"suggest_field":{
					"type":"completion",
					"contexts":[
						{
							"name":"user_name",
							"type":"category"
						}
					]
				}
			}
		}
	}
}
`

	testJoinIndex   = "elastic-joins"
	testJoinMapping = `
	{
		"settings":{
			"number_of_shards":1,
			"number_of_replicas":0
		},
		"mappings":{
			"doc":{
				"properties":{
					"message":{
						"type":"text"
					},
					"my_join_field": {
						"type": "join",
						"relations": {
							"question": "answer"
						}
					}
				}
			}
		}
	}
`

	testOrderIndex   = "elastic-orders"
	testOrderMapping = `
{
	"settings":{
		"number_of_shards":1,
		"number_of_replicas":0
	},
	"mappings":{
		"doc":{
			"properties":{
				"article":{
					"type":"text"
				},
				"manufacturer":{
					"type":"keyword"
				},
				"price":{
					"type":"float"
				},
				"time":{
					"type":"date",
					"format": "YYYY-MM-dd"
				}
			}
		}
	}
}
`

	/*
	   	testDoctypeIndex   = "elastic-doctypes"
	   	testDoctypeMapping = `
	   {
	   	"settings":{
	   		"number_of_shards":1,
	   		"number_of_replicas":0
	   	},
	   	"mappings":{
	   		"doc":{
	   			"properties":{
	   				"message":{
	   					"type":"text",
	   					"store": true,
	   					"fielddata": true
	   				}
	   			}
	   		}
	   	}
	   }
	   `
	*/

	testQueryIndex   = "elastic-queries"
	testQueryMapping = `
{
	"settings":{
		"number_of_shards":1,
		"number_of_replicas":0
	},
	"mappings":{
		"doc":{
			"properties":{
				"message":{
					"type":"text",
					"store": true,
					"fielddata": true
				},
				"query": {
					"type":	"percolator"
				}
			}
		}
	}
}
`
)

type tweet struct {
	User     string        `json:"user"`
	Message  string        `json:"message"`
	Retweets int           `json:"retweets"`
	Image    string        `json:"image,omitempty"`
	Created  time.Time     `json:"created,omitempty"`
	Tags     []string      `json:"tags,omitempty"`
	Location string        `json:"location,omitempty"`
	Suggest  *SuggestField `json:"suggest_field,omitempty"`
}

func (t tweet) String() string {
	return fmt.Sprintf("tweet{User:%q,Message:%q,Retweets:%d}", t.User, t.Message, t.Retweets)
}

type comment struct {
	User    string    `json:"user"`
	Comment string    `json:"comment"`
	Created time.Time `json:"created,omitempty"`
}

func (c comment) String() string {
	return fmt.Sprintf("comment{User:%q,Comment:%q}", c.User, c.Comment)
}

type joinDoc struct {
	Message   string      `json:"message"`
	JoinField interface{} `json:"my_join_field,omitempty"`
}

type joinField struct {
	Name   string `json:"name"`
	Parent string `json:"parent,omitempty"`
}

type order struct {
	Article      string  `json:"article"`
	Manufacturer string  `json:"manufacturer"`
	Price        float64 `json:"price"`
	Time         string  `json:"time,omitempty"`
}

func (o order) String() string {
	return fmt.Sprintf("order{Article:%q,Manufacturer:%q,Price:%v,Time:%v}", o.Article, o.Manufacturer, o.Price, o.Time)
}

// doctype is required for Percolate tests.
type doctype struct {
	Message string `json:"message"`
}

// queries is required for Percolate tests.
type queries struct {
	Query string `json:"query"`
}

func isTravis() bool {
	return os.Getenv("TRAVIS") != ""
}

func travisGoVersion() string {
	return os.Getenv("TRAVIS_GO_VERSION")
}

type logger interface {
	Error(args ...interface{})
	Errorf(format string, args ...interface{})
	Fatal(args ...interface{})
	Fatalf(format string, args ...interface{})
	Fail()
	FailNow()
	Log(args ...interface{})
	Logf(format string, args ...interface{})
}

func setupTestClient(t logger, options ...ClientOptionFunc) (client *Client) {
	var err error

	client, err = NewClient(options...)
	if err != nil {
		t.Fatal(err)
	}

	client.DeleteIndex(testIndexName).Do(context.TODO())
	client.DeleteIndex(testIndexName2).Do(context.TODO())
	client.DeleteIndex(testIndexName3).Do(context.TODO())
	client.DeleteIndex(testIndexName4).Do(context.TODO())
	client.DeleteIndex(testOrderIndex).Do(context.TODO())
	client.DeleteIndex(testNoSourceIndexName).Do(context.TODO())
	//client.DeleteIndex(testDoctypeIndex).Do(context.TODO())
	client.DeleteIndex(testQueryIndex).Do(context.TODO())
	client.DeleteIndex(testJoinIndex).Do(context.TODO())

	return client
}

func setupTestClientAndCreateIndex(t logger, options ...ClientOptionFunc) *Client {
	client := setupTestClient(t, options...)

	// Create index
	createIndex, err := client.CreateIndex(testIndexName).Body(testMapping).Do(context.TODO())
	if err != nil {
		t.Fatal(err)
	}
	if createIndex == nil {
		t.Errorf("expected result to be != nil; got: %v", createIndex)
	}

	// Create second index
	createIndex2, err := client.CreateIndex(testIndexName2).Body(testMapping).Do(context.TODO())
	if err != nil {
		t.Fatal(err)
	}
	if createIndex2 == nil {
		t.Errorf("expected result to be != nil; got: %v", createIndex2)
	}

	// Create no source index
	createNoSourceIndex, err := client.CreateIndex(testNoSourceIndexName).Body(testNoSourceMapping).Do(context.TODO())
	if err != nil {
		t.Fatal(err)
	}
	if createNoSourceIndex == nil {
		t.Errorf("expected result to be != nil; got: %v", createNoSourceIndex)
	}

	// Create order index
	createOrderIndex, err := client.CreateIndex(testOrderIndex).Body(testOrderMapping).Do(context.TODO())
	if err != nil {
		t.Fatal(err)
	}
	if createOrderIndex == nil {
		t.Errorf("expected result to be != nil; got: %v", createOrderIndex)
	}

	return client
}

func setupTestClientAndCreateIndexAndLog(t logger, options ...ClientOptionFunc) *Client {
	return setupTestClientAndCreateIndex(t, SetTraceLog(log.New(os.Stdout, "", 0)))
}

func setupTestClientAndCreateIndexAndAddDocs(t logger, options ...ClientOptionFunc) *Client {
	client := setupTestClientAndCreateIndex(t, options...)

	// Add tweets
	tweet1 := tweet{User: "olivere", Message: "Welcome to Golang and Elasticsearch."}
	tweet2 := tweet{User: "olivere", Message: "Another unrelated topic."}
	tweet3 := tweet{User: "sandrae", Message: "Cycling is fun."}
	//comment1 := comment{User: "nico", Comment: "You bet."}

	_, err := client.Index().Index(testIndexName).Type("doc").Id("1").BodyJson(&tweet1).Do(context.TODO())
	if err != nil {
		t.Fatal(err)
	}
	_, err = client.Index().Index(testIndexName).Type("doc").Id("2").BodyJson(&tweet2).Do(context.TODO())
	if err != nil {
		t.Fatal(err)
	}
	_, err = client.Index().Index(testIndexName).Type("doc").Id("3").Routing("someroutingkey").BodyJson(&tweet3).Do(context.TODO())
	if err != nil {
		t.Fatal(err)
	}
	/*
		_, err = client.Index().Index(testIndexName).Type("comment").Id("1").Parent("3").BodyJson(&comment1).Do(context.TODO())
		if err != nil {
			t.Fatal(err)
		}
	*/

	// Add orders
	var orders []order
	orders = append(orders, order{Article: "Apple MacBook", Manufacturer: "Apple", Price: 1290, Time: "2015-01-18"})
	orders = append(orders, order{Article: "Paper", Manufacturer: "Canon", Price: 100, Time: "2015-03-01"})
	orders = append(orders, order{Article: "Apple iPad", Manufacturer: "Apple", Price: 499, Time: "2015-04-12"})
	orders = append(orders, order{Article: "Dell XPS 13", Manufacturer: "Dell", Price: 1600, Time: "2015-04-18"})
	orders = append(orders, order{Article: "Apple Watch", Manufacturer: "Apple", Price: 349, Time: "2015-04-29"})
	orders = append(orders, order{Article: "Samsung TV", Manufacturer: "Samsung", Price: 790, Time: "2015-05-03"})
	orders = append(orders, order{Article: "Hoodie", Manufacturer: "h&m", Price: 49, Time: "2015-06-03"})
	orders = append(orders, order{Article: "T-Shirt", Manufacturer: "h&m", Price: 19, Time: "2015-06-18"})
	for i, o := range orders {
		id := fmt.Sprintf("%d", i)
		_, err = client.Index().Index(testOrderIndex).Type("doc").Id(id).BodyJson(&o).Do(context.TODO())
		if err != nil {
			t.Fatal(err)
		}
	}

	// Flush
	_, err = client.Flush().Index(testIndexName, testOrderIndex).Do(context.TODO())
	if err != nil {
		t.Fatal(err)
	}
	return client
}

func setupTestClientAndCreateIndexAndAddDocsNoSource(t logger, options ...ClientOptionFunc) *Client {
	client := setupTestClientAndCreateIndex(t, options...)

	// Add tweets
	tweet1 := tweet{User: "olivere", Message: "Welcome to Golang and Elasticsearch."}
	tweet2 := tweet{User: "olivere", Message: "Another unrelated topic."}

	_, err := client.Index().Index(testNoSourceIndexName).Type("doc").Id("1").BodyJson(&tweet1).Do(context.TODO())
	if err != nil {
		t.Fatal(err)
	}
	_, err = client.Index().Index(testNoSourceIndexName).Type("doc").Id("2").BodyJson(&tweet2).Do(context.TODO())
	if err != nil {
		t.Fatal(err)
	}
	// Flush
	_, err = client.Flush().Index(testNoSourceIndexName).Do(context.TODO())
	if err != nil {
		t.Fatal(err)
	}

	return client
}

var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func randomString(n int) string {
	b := make([]rune, n)
	for i := range b {
		b[i] = letters[rand.Intn(len(letters))]
	}
	return string(b)
}

type lexicographically struct {
	strings []string
}

func (l lexicographically) Len() int {
	return len(l.strings)
}

func (l lexicographically) Less(i, j int) bool {
	return l.strings[i] < l.strings[j]
}

func (l lexicographically) Swap(i, j int) {
	l.strings[i], l.strings[j] = l.strings[j], l.strings[i]
}
