package utils

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"io/ioutil"
	"os"
	"testing"

	"github.com/docker/notary/cryptoservice"
	"github.com/docker/notary/trustmanager"
	"github.com/stretchr/testify/assert"
)

const (
	ServerCert = "../fixtures/notary-server.crt"
	ServerKey  = "../fixtures/notary-server.key"
	RootCA     = "../fixtures/root-ca.crt"
)

// generates a multiple-certificate file with both RSA and ECDSA certs and
// returns the filename so that cleanup can be deferred.
func generateMultiCert(t *testing.T) string {
	tempFile, err := ioutil.TempFile("/tmp", "cert-test")
	defer tempFile.Close()
	assert.NoError(t, err)

	rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
	assert.NoError(t, err)
	ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	assert.NoError(t, err)

	for _, key := range []crypto.Signer{rsaKey, ecKey} {
		cert, err := cryptoservice.GenerateTestingCertificate(key, "gun")
		assert.NoError(t, err)

		pemBytes := trustmanager.CertToPEM(cert)
		nBytes, err := tempFile.Write(pemBytes)
		assert.NoError(t, err)
		assert.Equal(t, nBytes, len(pemBytes))
	}
	return tempFile.Name()
}

// If the cert files and directory are provided but are invalid, an error is
// returned.
func TestConfigServerTLSFailsIfUnableToLoadCerts(t *testing.T) {
	for i := 0; i < 3; i++ {
		files := []string{ServerCert, ServerKey, RootCA}
		files[i] = "not-real-file"

		result, err := ConfigureServerTLS(&ServerTLSOpts{
			ServerCertFile: files[0],
			ServerKeyFile:  files[1],
			ClientCAFile:   files[2],
		})
		assert.Nil(t, result)
		assert.Error(t, err)
	}
}

// If server cert and key are provided, and client auth is disabled, then
// a valid tls.Config is returned with ClientAuth set to NoClientCert
func TestConfigServerTLSServerCertsOnly(t *testing.T) {
	keypair, err := tls.LoadX509KeyPair(ServerCert, ServerKey)
	assert.NoError(t, err)

	tlsConfig, err := ConfigureServerTLS(&ServerTLSOpts{
		ServerCertFile: ServerCert,
		ServerKeyFile:  ServerKey,
	})
	assert.NoError(t, err)
	assert.Equal(t, []tls.Certificate{keypair}, tlsConfig.Certificates)
	assert.True(t, tlsConfig.PreferServerCipherSuites)
	assert.Equal(t, tls.NoClientCert, tlsConfig.ClientAuth)
	assert.Nil(t, tlsConfig.ClientCAs)
}

// If a valid client cert file is provided, but it contains no client
// certs, an error is returned.
func TestConfigServerTLSWithEmptyCACertFile(t *testing.T) {
	tempFile, err := ioutil.TempFile("/tmp", "cert-test")
	assert.NoError(t, err)
	defer os.RemoveAll(tempFile.Name())
	tempFile.Close()

	tlsConfig, err := ConfigureServerTLS(&ServerTLSOpts{
		ServerCertFile: ServerCert,
		ServerKeyFile:  ServerKey,
		ClientCAFile:   tempFile.Name(),
	})
	assert.Nil(t, tlsConfig)
	assert.Error(t, err)
}

// If server cert and key are provided, and client cert file is provided with
// one cert, a valid tls.Config is returned with the clientCAs set to that
// cert.  ClientAuth is set to RequireAndVerifyClientCert.
func TestConfigServerTLSWithOneCACert(t *testing.T) {
	keypair, err := tls.LoadX509KeyPair(ServerCert, ServerKey)
	assert.NoError(t, err)

	tlsConfig, err := ConfigureServerTLS(&ServerTLSOpts{
		ServerCertFile: ServerCert,
		ServerKeyFile:  ServerKey,
		ClientCAFile:   RootCA,
	})
	assert.NoError(t, err)
	assert.Equal(t, []tls.Certificate{keypair}, tlsConfig.Certificates)
	assert.True(t, tlsConfig.PreferServerCipherSuites)
	assert.Equal(t, tls.RequireAndVerifyClientCert, tlsConfig.ClientAuth)
	assert.Len(t, tlsConfig.ClientCAs.Subjects(), 1)
}

// If server cert and key are provided, and client cert file is provided with
// multiple certs, a valid tls.Config is returned with the clientCAs set to
// the valid cert.  ClientAuth is set to RequireAndVerifyClientCert.
func TestConfigServerTLSWithMultipleCACerts(t *testing.T) {
	tempFilename := generateMultiCert(t)
	defer os.RemoveAll(tempFilename)

	keypair, err := tls.LoadX509KeyPair(ServerCert, ServerKey)
	assert.NoError(t, err)

	tlsConfig, err := ConfigureServerTLS(&ServerTLSOpts{
		ServerCertFile: ServerCert,
		ServerKeyFile:  ServerKey,
		ClientCAFile:   tempFilename,
	})
	assert.NoError(t, err)
	assert.Equal(t, []tls.Certificate{keypair}, tlsConfig.Certificates)
	assert.True(t, tlsConfig.PreferServerCipherSuites)
	assert.Equal(t, tls.RequireAndVerifyClientCert, tlsConfig.ClientAuth)
	assert.Len(t, tlsConfig.ClientCAs.Subjects(), 2)
}

// The skipVerify boolean gets set on the tls.Config's InsecureSkipBoolean
func TestConfigClientTLSNoVerify(t *testing.T) {
	for _, skip := range []bool{true, false} {
		tlsConfig, err := ConfigureClientTLS(
			&ClientTLSOpts{InsecureSkipVerify: skip})
		assert.NoError(t, err)
		assert.Nil(t, tlsConfig.Certificates)
		assert.Equal(t, skip, tlsConfig.InsecureSkipVerify)
		assert.Equal(t, "", tlsConfig.ServerName)
		assert.Nil(t, tlsConfig.RootCAs)
	}
}

// The skipVerify boolean gets set on the tls.Config's InsecureSkipBoolean
func TestConfigClientServerName(t *testing.T) {
	for _, name := range []string{"", "myname"} {
		tlsConfig, err := ConfigureClientTLS(&ClientTLSOpts{ServerName: name})
		assert.NoError(t, err)
		assert.Nil(t, tlsConfig.Certificates)
		assert.Equal(t, false, tlsConfig.InsecureSkipVerify)
		assert.Equal(t, name, tlsConfig.ServerName)
		assert.Nil(t, tlsConfig.RootCAs)
	}
}

// The RootCA is set if the file provided has a single CA cert.
func TestConfigClientTLSRootCAFileWithOneCert(t *testing.T) {
	tlsConfig, err := ConfigureClientTLS(&ClientTLSOpts{RootCAFile: RootCA})
	assert.NoError(t, err)
	assert.Nil(t, tlsConfig.Certificates)
	assert.Equal(t, false, tlsConfig.InsecureSkipVerify)
	assert.Equal(t, "", tlsConfig.ServerName)
	assert.Len(t, tlsConfig.RootCAs.Subjects(), 1)
}

// If the root CA file provided has multiple CA certs, only the valid certs
// are read.
func TestConfigClientTLSRootCAFileMultipleCerts(t *testing.T) {
	tempFilename := generateMultiCert(t)
	defer os.RemoveAll(tempFilename)

	tlsConfig, err := ConfigureClientTLS(
		&ClientTLSOpts{RootCAFile: tempFilename})
	assert.NoError(t, err)
	assert.Nil(t, tlsConfig.Certificates)
	assert.Equal(t, false, tlsConfig.InsecureSkipVerify)
	assert.Equal(t, "", tlsConfig.ServerName)
	assert.Len(t, tlsConfig.RootCAs.Subjects(), 2)
}

// An error is returned if a root CA is provided but the file doesn't exist.
func TestConfigClientTLSNonexistentRootCAFile(t *testing.T) {
	tlsConfig, err := ConfigureClientTLS(
		&ClientTLSOpts{RootCAFile: "not-a-file"})
	assert.Error(t, err)
	assert.Nil(t, tlsConfig)
}

// An error is returned if either the client cert or the key are provided
// but invalid or blank.
func TestConfigClientTLSClientCertOrKeyInvalid(t *testing.T) {
	for i := 0; i < 2; i++ {
		for _, invalid := range []string{"not-a-file", ""} {
			files := []string{ServerCert, ServerKey}
			files[i] = invalid
			tlsConfig, err := ConfigureClientTLS(&ClientTLSOpts{
				ClientCertFile: files[0], ClientKeyFile: files[1]})
			assert.Error(t, err)
			assert.Nil(t, tlsConfig)
		}
	}
}

// The certificate is set if the client cert and client key are provided and
// valid.
func TestConfigClientTLSValidClientCertAndKey(t *testing.T) {
	keypair, err := tls.LoadX509KeyPair(ServerCert, ServerKey)
	assert.NoError(t, err)

	tlsConfig, err := ConfigureClientTLS(&ClientTLSOpts{
		ClientCertFile: ServerCert, ClientKeyFile: ServerKey})
	assert.NoError(t, err)
	assert.Equal(t, []tls.Certificate{keypair}, tlsConfig.Certificates)
	assert.Equal(t, false, tlsConfig.InsecureSkipVerify)
	assert.Equal(t, "", tlsConfig.ServerName)
	assert.Nil(t, tlsConfig.RootCAs)
}