package test

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"strings"
	"testing"
	"time"
)

// TestTLSOff asserts that non-TLS-encrypted communication between the
// etcd server and an unauthenticated client works
func TestTLSOff(t *testing.T) {
	proc, err := startServer([]string{})
	if err != nil {
		t.Fatal(err.Error())
	}
	defer stopServer(proc)

	client := buildClient()
	err = assertServerFunctional(client, "http")
	if err != nil {
		t.Fatal(err.Error())
	}
}

// TestTLSAnonymousClient asserts that TLS-encrypted communication between the etcd
// server and an anonymous client works
func TestTLSAnonymousClient(t *testing.T) {
	proc, err := startServer([]string{
		"-cert-file=../../fixtures/ca/server.crt",
		"-key-file=../../fixtures/ca/server.key.insecure",
	})
	if err != nil {
		t.Fatal(err.Error())
	}
	defer stopServer(proc)

	cacertfile := "../../fixtures/ca/ca.crt"

	cp := x509.NewCertPool()
	bytes, err := ioutil.ReadFile(cacertfile)
	if err != nil {
		panic(err)
	}
	cp.AppendCertsFromPEM(bytes)

	cfg := tls.Config{}
	cfg.RootCAs = cp

	client := buildTLSClient(&cfg)
	err = assertServerFunctional(client, "https")
	if err != nil {
		t.Fatal(err)
	}
}

// TestTLSAuthenticatedClient asserts that TLS-encrypted communication
// between the etcd server and an authenticated client works
func TestTLSAuthenticatedClient(t *testing.T) {
	proc, err := startServer([]string{
		"-cert-file=../../fixtures/ca/server.crt",
		"-key-file=../../fixtures/ca/server.key.insecure",
		"-ca-file=../../fixtures/ca/ca.crt",
	})
	if err != nil {
		t.Fatal(err.Error())
	}
	defer stopServer(proc)

	cacertfile := "../../fixtures/ca/ca.crt"
	certfile := "../../fixtures/ca/server2.crt"
	keyfile := "../../fixtures/ca/server2.key.insecure"

	cert, err := tls.LoadX509KeyPair(certfile, keyfile)
	if err != nil {
		panic(err)
	}

	cp := x509.NewCertPool()
	bytes, err := ioutil.ReadFile(cacertfile)
	if err != nil {
		panic(err)
	}
	cp.AppendCertsFromPEM(bytes)

	cfg := tls.Config{}
	cfg.Certificates = []tls.Certificate{cert}
	cfg.RootCAs = cp

	time.Sleep(time.Second)

	client := buildTLSClient(&cfg)
	err = assertServerFunctional(client, "https")
	if err != nil {
		t.Fatal(err)
	}
}

// TestTLSUnathenticatedClient asserts that TLS-encrypted communication
// between the etcd server and an unauthenticated client fails
func TestTLSUnauthenticatedClient(t *testing.T) {
	proc, err := startServer([]string{
		"-cert-file=../../fixtures/ca/server.crt",
		"-key-file=../../fixtures/ca/server.key.insecure",
		"-ca-file=../../fixtures/ca/ca.crt",
	})
	if err != nil {
		t.Fatal(err.Error())
	}
	defer stopServer(proc)

	cacertfile := "../../fixtures/ca/ca.crt"
	certfile := "../../fixtures/ca/broken_server.crt"
	keyfile := "../../fixtures/ca/broken_server.key.insecure"

	cert, err := tls.LoadX509KeyPair(certfile, keyfile)
	if err != nil {
		panic(err)
	}

	cp := x509.NewCertPool()
	bytes, err := ioutil.ReadFile(cacertfile)
	if err != nil {
		panic(err)
	}
	cp.AppendCertsFromPEM(bytes)

	cfg := tls.Config{}
	cfg.Certificates = []tls.Certificate{cert}
	cfg.RootCAs = cp

	time.Sleep(time.Second)

	client := buildTLSClient(&cfg)
	err = assertServerNotFunctional(client, "https")
	if err != nil {
		t.Fatal(err)
	}
}

func buildClient() http.Client {
	return http.Client{}
}

func buildTLSClient(tlsConf *tls.Config) http.Client {
	tr := http.Transport{TLSClientConfig: tlsConf}
	return http.Client{Transport: &tr}
}

func startServer(extra []string) (*os.Process, error) {
	procAttr := new(os.ProcAttr)
	procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}

	cmd := []string{"etcd", "-f", "-data-dir=/tmp/node1", "-name=node1"}
	cmd = append(cmd, extra...)

	println(strings.Join(cmd, " "))

	return os.StartProcess(EtcdBinPath, cmd, procAttr)
}

// TODO(yichengq): refactor these helper functions in #645
func startServer2(extra []string) (*os.Process, error) {
	procAttr := new(os.ProcAttr)
	procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}

	cmd := []string{"etcd", "-f", "-data-dir=/tmp/node2", "-name=node2"}
	cmd = append(cmd, extra...)

	fmt.Println(strings.Join(cmd, " "))

	return os.StartProcess(EtcdBinPath, cmd, procAttr)
}

func startServerWithDataDir(extra []string) (*os.Process, error) {
	procAttr := new(os.ProcAttr)
	procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}

	cmd := []string{"etcd", "-data-dir=/tmp/node1", "-name=node1"}
	cmd = append(cmd, extra...)

	fmt.Println(strings.Join(cmd, " "))

	return os.StartProcess(EtcdBinPath, cmd, procAttr)
}

func startServer2WithDataDir(extra []string) (*os.Process, error) {
	procAttr := new(os.ProcAttr)
	procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}

	cmd := []string{"etcd", "-data-dir=/tmp/node2", "-name=node2"}
	cmd = append(cmd, extra...)

	println(strings.Join(cmd, " "))

	return os.StartProcess(EtcdBinPath, cmd, procAttr)
}

func stopServer(proc *os.Process) {
	err := proc.Kill()
	if err != nil {
		panic(err.Error())
	}
	proc.Release()
}

func assertServerFunctional(client http.Client, scheme string) error {
	path := fmt.Sprintf("%s://127.0.0.1:4001/v2/keys/foo", scheme)
	fields := url.Values(map[string][]string{"value": {"bar"}})

	for i := 0; i < 10; i++ {
		time.Sleep(1 * time.Second)

		resp, err := client.PostForm(path, fields)
		// If the status is Temporary Redirect, we should follow the
		// new location, because the request did not go to the leader yet.
		// TODO(yichengq): the difference between Temporary Redirect(307)
		// and Created(201) could distinguish between leader and followers
		for err == nil && resp.StatusCode == http.StatusTemporaryRedirect {
			loc, _ := resp.Location()
			newPath := loc.String()
			resp, err = client.PostForm(newPath, fields)
		}

		if err == nil {
			// Internal error may mean that servers are in leader election
			if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusInternalServerError {
				return errors.New(fmt.Sprintf("resp.StatusCode == %s", resp.Status))
			} else {
				return nil
			}
		}
	}

	return errors.New("etcd server was not reachable in time / had internal error")
}

func assertServerNotFunctional(client http.Client, scheme string) error {
	path := fmt.Sprintf("%s://127.0.0.1:4001/v2/keys/foo", scheme)
	fields := url.Values(map[string][]string{"value": {"bar"}})

	for i := 0; i < 10; i++ {
		time.Sleep(1 * time.Second)

		_, err := client.PostForm(path, fields)
		if err == nil {
			return errors.New("Expected error during POST, got nil")
		} else {
			errString := err.Error()
			if strings.Contains(errString, "connection refused") {
				continue
			} else if strings.Contains(errString, "bad certificate") {
				return nil
			} else {
				return err
			}
		}
	}

	return errors.New("Expected server to fail with 'bad certificate'")
}
