package cli_test

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"strings"
	"testing"

	"github.com/influxdata/influxdb/client"
	"github.com/influxdata/influxdb/cmd/influx/cli"
	"github.com/influxdata/influxql"
	"github.com/peterh/liner"
)

const (
	CLIENT_VERSION = "y.y"
	SERVER_VERSION = "x.x"
)

func TestNewCLI(t *testing.T) {
	t.Parallel()
	c := cli.New(CLIENT_VERSION)

	if c == nil {
		t.Fatal("CommandLine shouldn't be nil.")
	}

	if c.ClientVersion != CLIENT_VERSION {
		t.Fatalf("CommandLine version is %s but should be %s", c.ClientVersion, CLIENT_VERSION)
	}
}

func TestRunCLI_ExecuteInsert(t *testing.T) {
	t.Parallel()
	ts := emptyTestServer()
	defer ts.Close()

	u, _ := url.Parse(ts.URL)
	h, p, _ := net.SplitHostPort(u.Host)
	c := cli.New(CLIENT_VERSION)
	c.Host = h
	c.Port, _ = strconv.Atoi(p)
	c.ClientConfig.Precision = "ms"
	c.Execute = "INSERT sensor,floor=1 value=2"
	c.IgnoreSignals = true
	c.ForceTTY = true
	if err := c.Run(); err != nil {
		t.Fatalf("Run failed with error: %s", err)
	}
}

func TestRunCLI_ExecuteInsertWithPath(t *testing.T) {
	path := "boom"
	t.Parallel()
	ts := emptyTestServerWithPath(path)
	defer ts.Close()

	u, _ := url.Parse(ts.URL)
	h, p, _ := net.SplitHostPort(u.Host)
	c := cli.New(CLIENT_VERSION)
	c.Host = h
	c.PathPrefix = path
	c.Port, _ = strconv.Atoi(p)
	c.ClientConfig.Precision = "ms"
	c.Execute = "INSERT sensor,floor=1 value=2"
	c.IgnoreSignals = true
	c.ForceTTY = true
	if err := c.Run(); err != nil {
		t.Fatalf("Run failed with error: %s", err)
	}
}

func TestRunCLI_ExecuteInsert_WithSignals(t *testing.T) {
	t.Parallel()
	ts := emptyTestServer()
	defer ts.Close()

	u, _ := url.Parse(ts.URL)
	h, p, _ := net.SplitHostPort(u.Host)
	c := cli.New(CLIENT_VERSION)
	c.Host = h
	c.Port, _ = strconv.Atoi(p)
	c.ClientConfig.Precision = "ms"
	c.Execute = "INSERT sensor,floor=1 value=2"
	c.IgnoreSignals = false
	c.ForceTTY = true
	if err := c.Run(); err != nil {
		t.Fatalf("Run failed with error: %s", err)
	}
}

func TestSetAuth(t *testing.T) {
	t.Parallel()
	c := cli.New(CLIENT_VERSION)
	config := client.NewConfig()
	client, _ := client.NewClient(config)
	c.Client = client
	u := "userx"
	p := "pwdy"
	c.SetAuth("auth " + u + " " + p)

	// validate CLI configuration
	if c.ClientConfig.Username != u {
		t.Fatalf("Username is %s but should be %s", c.ClientConfig.Username, u)
	}
	if c.ClientConfig.Password != p {
		t.Fatalf("Password is %s but should be %s", c.ClientConfig.Password, p)
	}
}

func TestSetPrecision(t *testing.T) {
	t.Parallel()
	c := cli.New(CLIENT_VERSION)
	config := client.NewConfig()
	client, _ := client.NewClient(config)
	c.Client = client

	// validate set non-default precision
	p := "ns"
	c.SetPrecision("precision " + p)
	if c.ClientConfig.Precision != p {
		t.Fatalf("Precision is %s but should be %s", c.ClientConfig.Precision, p)
	}
	up := "NS"
	c.SetPrecision("PRECISION " + up)
	if c.ClientConfig.Precision != p {
		t.Fatalf("Precision is %s but should be %s", c.ClientConfig.Precision, p)
	}
	mixed := "ns"
	c.SetPrecision("PRECISION " + mixed)
	if c.ClientConfig.Precision != p {
		t.Fatalf("Precision is %s but should be %s", c.ClientConfig.Precision, p)
	}

	// validate set default precision which equals empty string
	p = "rfc3339"
	c.SetPrecision("precision " + p)
	if c.ClientConfig.Precision != "" {
		t.Fatalf("Precision is %s but should be empty", c.ClientConfig.Precision)
	}
	p = "RFC3339"
	c.SetPrecision("precision " + p)
	if c.ClientConfig.Precision != "" {
		t.Fatalf("Precision is %s but should be empty", c.ClientConfig.Precision)
	}
}

func TestSetFormat(t *testing.T) {
	t.Parallel()
	c := cli.New(CLIENT_VERSION)
	config := client.NewConfig()
	client, _ := client.NewClient(config)
	c.Client = client

	// validate set non-default format
	f := "json"
	c.SetFormat("format " + f)
	if c.Format != f {
		t.Fatalf("Format is %s but should be %s", c.Format, f)
	}

	uf := "JSON"
	c.SetFormat("format " + uf)
	if c.Format != f {
		t.Fatalf("Format is %s but should be %s", c.Format, f)
	}
	mixed := "json"
	c.SetFormat("FORMAT " + mixed)
	if c.Format != f {
		t.Fatalf("Format is %s but should be %s", c.Format, f)
	}
}

func Test_SetChunked(t *testing.T) {
	t.Parallel()
	c := cli.New(CLIENT_VERSION)
	config := client.NewConfig()
	client, _ := client.NewClient(config)
	c.Client = client

	// make sure chunked is on by default
	if got, exp := c.Chunked, true; got != exp {
		t.Fatalf("chunked should be on by default.  got %v, exp %v", got, exp)
	}

	// turn chunked off
	if err := c.ParseCommand("Chunked"); err != nil {
		t.Fatalf("setting chunked failed: err: %s", err)
	}

	if got, exp := c.Chunked, false; got != exp {
		t.Fatalf("setting chunked failed.  got %v, exp %v", got, exp)
	}

	// turn chunked back on
	if err := c.ParseCommand("Chunked"); err != nil {
		t.Fatalf("setting chunked failed: err: %s", err)
	}

	if got, exp := c.Chunked, true; got != exp {
		t.Fatalf("setting chunked failed.  got %v, exp %v", got, exp)
	}
}

func Test_SetChunkSize(t *testing.T) {
	t.Parallel()
	c := cli.New(CLIENT_VERSION)
	config := client.NewConfig()
	client, _ := client.NewClient(config)
	c.Client = client

	// check default chunk size
	if got, exp := c.ChunkSize, 0; got != exp {
		t.Fatalf("unexpected chunk size.  got %d, exp %d", got, exp)
	}

	tests := []struct {
		command string
		exp     int
	}{
		{"chunk size 20", 20},
		{"   CHunk     siZE  55    ", 55},
		{"chunk 10", 10},
		{"     chuNK     15", 15},
		{"chunk size -60", 0},
		{"chunk size 10", 10},
		{"chunk size 0", 0},
		{"chunk size 10", 10},
		{"chunk size junk", 10},
	}

	for _, test := range tests {
		if err := c.ParseCommand(test.command); err != nil {
			t.Logf("command: %q", test.command)
			t.Fatalf("setting chunked failed: err: %s", err)
		}

		if got, exp := c.ChunkSize, test.exp; got != exp {
			t.Logf("command: %q", test.command)
			t.Fatalf("unexpected chunk size.  got %d, exp %d", got, exp)
		}
	}
}

func TestSetWriteConsistency(t *testing.T) {
	t.Parallel()
	c := cli.New(CLIENT_VERSION)
	config := client.NewConfig()
	client, _ := client.NewClient(config)
	c.Client = client

	// set valid write consistency
	consistency := "all"
	c.SetWriteConsistency("consistency " + consistency)
	if c.ClientConfig.WriteConsistency != consistency {
		t.Fatalf("WriteConsistency is %s but should be %s", c.ClientConfig.WriteConsistency, consistency)
	}

	// set different valid write consistency and validate change
	consistency = "quorum"
	c.SetWriteConsistency("consistency " + consistency)
	if c.ClientConfig.WriteConsistency != consistency {
		t.Fatalf("WriteConsistency is %s but should be %s", c.ClientConfig.WriteConsistency, consistency)
	}

	consistency = "QUORUM"
	c.SetWriteConsistency("consistency " + consistency)
	if c.ClientConfig.WriteConsistency != "quorum" {
		t.Fatalf("WriteConsistency is %s but should be %s", c.ClientConfig.WriteConsistency, "quorum")
	}

	consistency = "quorum"
	c.SetWriteConsistency("CONSISTENCY " + consistency)
	if c.ClientConfig.WriteConsistency != consistency {
		t.Fatalf("WriteConsistency is %s but should be %s", c.ClientConfig.WriteConsistency, consistency)
	}

	// set invalid write consistency and verify there was no change
	invalidConsistency := "invalid_consistency"
	c.SetWriteConsistency("consistency " + invalidConsistency)
	if c.ClientConfig.WriteConsistency == invalidConsistency {
		t.Fatalf("WriteConsistency is %s but should be %s", c.ClientConfig.WriteConsistency, consistency)
	}
}

func TestParseCommand_CommandsExist(t *testing.T) {
	t.Parallel()
	c, err := client.NewClient(client.Config{})
	if err != nil {
		t.Fatalf("unexpected error %v", err)
	}

	m := cli.CommandLine{Client: c, Line: liner.NewLiner()}
	tests := []struct {
		cmd string
	}{
		{cmd: "gopher"},
		{cmd: "auth"},
		{cmd: "help"},
		{cmd: "format"},
		{cmd: "precision"},
		{cmd: "settings"},
	}
	for _, test := range tests {
		if err := m.ParseCommand(test.cmd); err != nil {
			t.Fatalf(`Got error %v for command %q, expected nil`, err, test.cmd)
		}
	}
}

func TestParseCommand_Connect(t *testing.T) {
	t.Parallel()
	ts := emptyTestServer()
	defer ts.Close()

	u, _ := url.Parse(ts.URL)
	cmd := "connect " + u.Host
	c := cli.CommandLine{}

	// assert connection is established
	if err := c.ParseCommand(cmd); err != nil {
		t.Fatalf("There was an error while connecting to %v: %v", u.Path, err)
	}

	// assert server version is populated
	if c.ServerVersion != SERVER_VERSION {
		t.Fatalf("Server version is %s but should be %s.", c.ServerVersion, SERVER_VERSION)
	}
}

func TestParseCommand_TogglePretty(t *testing.T) {
	t.Parallel()
	c := cli.CommandLine{}
	if c.Pretty {
		t.Fatalf(`Pretty should be false.`)
	}
	c.ParseCommand("pretty")
	if !c.Pretty {
		t.Fatalf(`Pretty should be true.`)
	}
	c.ParseCommand("pretty")
	if c.Pretty {
		t.Fatalf(`Pretty should be false.`)
	}
}

func TestParseCommand_Exit(t *testing.T) {
	t.Parallel()
	tests := []struct {
		cmd string
	}{
		{cmd: "exit"},
		{cmd: " exit"},
		{cmd: "exit "},
		{cmd: "Exit "},
	}

	for _, test := range tests {
		c := cli.CommandLine{Quit: make(chan struct{}, 1)}
		c.ParseCommand(test.cmd)
		// channel should be closed
		if _, ok := <-c.Quit; ok {
			t.Fatalf(`Command "exit" failed for %q.`, test.cmd)
		}
	}
}

func TestParseCommand_Quit(t *testing.T) {
	t.Parallel()
	tests := []struct {
		cmd string
	}{
		{cmd: "quit"},
		{cmd: " quit"},
		{cmd: "quit "},
		{cmd: "Quit "},
	}

	for _, test := range tests {
		c := cli.CommandLine{Quit: make(chan struct{}, 1)}
		c.ParseCommand(test.cmd)
		// channel should be closed
		if _, ok := <-c.Quit; ok {
			t.Fatalf(`Command "quit" failed for %q.`, test.cmd)
		}
	}
}

func TestParseCommand_Use(t *testing.T) {
	t.Parallel()
	ts := emptyTestServer()
	defer ts.Close()

	u, _ := url.Parse(ts.URL)
	config := client.Config{URL: *u}
	c, err := client.NewClient(config)
	if err != nil {
		t.Fatalf("unexpected error.  expected %v, actual %v", nil, err)
	}

	tests := []struct {
		cmd string
		db  string
	}{
		{cmd: "use db", db: "db"},
		{cmd: " use db", db: "db"},
		{cmd: "use db ", db: "db"},
		{cmd: "use db;", db: "db"},
		{cmd: "use db; ", db: "db"},
		{cmd: "Use db", db: "db"},
		{cmd: `Use "db"`, db: "db"},
		{cmd: `Use "db db"`, db: "db db"},
	}

	for _, test := range tests {
		m := cli.CommandLine{Client: c}
		if err := m.ParseCommand(test.cmd); err != nil {
			t.Fatalf(`Got error %v for command %q, expected nil.`, err, test.cmd)
		}

		if m.Database != test.db {
			t.Fatalf(`Command "%s" changed database to %q. Expected %s`, test.cmd, m.Database, test.db)
		}
	}
}

func TestParseCommand_UseAuth(t *testing.T) {
	t.Parallel()
	ts := emptyTestServer()
	defer ts.Close()

	u, _ := url.Parse(ts.URL)
	tests := []struct {
		cmd      string
		user     string
		database string
	}{
		{
			cmd:      "use db",
			user:     "admin",
			database: "db",
		},
		{
			cmd:      "use blank",
			user:     "admin",
			database: "",
		},
		{
			cmd:      "use db",
			user:     "anonymous",
			database: "db",
		},
		{
			cmd:      "use blank",
			user:     "anonymous",
			database: "blank",
		},
	}

	for i, tt := range tests {
		config := client.Config{URL: *u, Username: tt.user}
		fmt.Println("using auth:", tt.user)
		c, err := client.NewClient(config)
		if err != nil {
			t.Errorf("%d. unexpected error.  expected %v, actual %v", i, nil, err)
			continue
		}
		m := cli.CommandLine{Client: c}
		m.ClientConfig.Username = tt.user

		if err := m.ParseCommand(tt.cmd); err != nil {
			t.Fatalf(`%d. Got error %v for command %q, expected nil.`, i, err, tt.cmd)
		}

		if m.Database != tt.database {
			t.Fatalf(`%d. Command "use" changed database to %q. Expected %q`, i, m.Database, tt.database)
		}
	}
}

func TestParseCommand_Consistency(t *testing.T) {
	t.Parallel()
	c := cli.CommandLine{}
	tests := []struct {
		cmd string
	}{
		{cmd: "consistency one"},
		{cmd: " consistency one"},
		{cmd: "consistency one "},
		{cmd: "consistency one;"},
		{cmd: "consistency one; "},
		{cmd: "Consistency one"},
	}

	for _, test := range tests {
		if err := c.ParseCommand(test.cmd); err != nil {
			t.Fatalf(`Got error %v for command %q, expected nil.`, err, test.cmd)
		}

		if c.ClientConfig.WriteConsistency != "one" {
			t.Fatalf(`Command "consistency" changed consistency to %q. Expected one`, c.ClientConfig.WriteConsistency)
		}
	}
}

func TestParseCommand_Insert(t *testing.T) {
	t.Parallel()
	ts := emptyTestServer()
	defer ts.Close()

	u, _ := url.Parse(ts.URL)
	config := client.Config{URL: *u}
	c, err := client.NewClient(config)
	if err != nil {
		t.Fatalf("unexpected error.  expected %v, actual %v", nil, err)
	}
	m := cli.CommandLine{Client: c}

	tests := []struct {
		cmd string
	}{
		{cmd: "INSERT cpu,host=serverA,region=us-west value=1.0"},
		{cmd: " INSERT cpu,host=serverA,region=us-west value=1.0"},
		{cmd: "INSERT   cpu,host=serverA,region=us-west value=1.0"},
		{cmd: "insert cpu,host=serverA,region=us-west    value=1.0    "},
		{cmd: "insert"},
		{cmd: "Insert "},
		{cmd: "insert c"},
		{cmd: "insert int"},
	}

	for _, test := range tests {
		if err := m.ParseCommand(test.cmd); err != nil {
			t.Fatalf(`Got error %v for command %q, expected nil.`, err, test.cmd)
		}
	}
}

func TestParseCommand_History(t *testing.T) {
	t.Parallel()
	c := cli.CommandLine{Line: liner.NewLiner()}
	defer c.Line.Close()

	// append one entry to history
	c.Line.AppendHistory("abc")

	tests := []struct {
		cmd string
	}{
		{cmd: "history"},
		{cmd: " history"},
		{cmd: "history "},
		{cmd: "History "},
	}

	for _, test := range tests {
		if err := c.ParseCommand(test.cmd); err != nil {
			t.Fatalf(`Got error %v for command %q, expected nil.`, err, test.cmd)
		}
	}

	// buf size should be at least 1
	var buf bytes.Buffer
	c.Line.WriteHistory(&buf)
	if buf.Len() < 1 {
		t.Fatal("History is borked")
	}
}

func TestParseCommand_HistoryWithBlankCommand(t *testing.T) {
	t.Parallel()
	c := cli.CommandLine{Line: liner.NewLiner()}
	defer c.Line.Close()

	// append one entry to history
	c.Line.AppendHistory("x")

	tests := []struct {
		cmd string
		err error
	}{
		{cmd: "history"},
		{cmd: " history"},
		{cmd: "history "},
		{cmd: "", err: cli.ErrBlankCommand},      // shouldn't be persisted in history
		{cmd: " ", err: cli.ErrBlankCommand},     // shouldn't be persisted in history
		{cmd: "     ", err: cli.ErrBlankCommand}, // shouldn't be persisted in history
	}

	// a blank command will return cli.ErrBlankCommand.
	for _, test := range tests {
		if err := c.ParseCommand(test.cmd); err != test.err {
			t.Errorf(`Got error %v for command %q, expected %v`, err, test.cmd, test.err)
		}
	}

	// buf shall not contain empty commands
	var buf bytes.Buffer
	c.Line.WriteHistory(&buf)
	scanner := bufio.NewScanner(&buf)
	for scanner.Scan() {
		if strings.TrimSpace(scanner.Text()) == "" {
			t.Fatal("Empty commands should not be persisted in history.")
		}
	}
}

func emptyTestServerWithPath(path string) *httptest.Server {
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Influxdb-Version", SERVER_VERSION)

		// Fake authorization entirely based on the username.
		authorized := false
		user, _, _ := r.BasicAuth()
		switch user {
		case "", "admin":
			authorized = true
		}

		queryPath := path + "/query"
		writePath := path + "/write"

		switch r.URL.Path {
		case queryPath:
			values := r.URL.Query()
			parser := influxql.NewParser(bytes.NewBufferString(values.Get("q")))
			q, err := parser.ParseQuery()
			if err != nil {
				return
			}
			stmt := q.Statements[0]

			switch stmt.(type) {
			case *influxql.ShowDatabasesStatement:
				if authorized {
					io.WriteString(w, `{"results":[{"series":[{"name":"databases","columns":["name"],"values":[["db", "db db"]]}]}]}`)
				} else {
					w.WriteHeader(http.StatusUnauthorized)
					io.WriteString(w, fmt.Sprintf(`{"error":"error authorizing query: %s not authorized to execute statement 'SHOW DATABASES', requires admin privilege"}`, user))
				}
			case *influxql.ShowDiagnosticsStatement:
				io.WriteString(w, `{"results":[{}]}`)
			}
		case writePath:
			w.WriteHeader(http.StatusOK)
		}
	}))
}

// helper methods

func emptyTestServer() *httptest.Server {
	return emptyTestServerWithPath("")
}
