package ca

import (
	"fmt"
	"io/ioutil"
	"net"
	"sync"
	"testing"
	"time"

	"github.com/hashicorp/consul/agent/connect"
	"github.com/hashicorp/consul/agent/structs"
	vaultapi "github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/builtin/logical/pki"
	vaulthttp "github.com/hashicorp/vault/http"
	"github.com/hashicorp/vault/vault"
	"github.com/stretchr/testify/require"
)

var vaultLock sync.Mutex

func testVaultCluster(t *testing.T) (*VaultProvider, *vault.Core, net.Listener) {
	return testVaultClusterWithConfig(t, true, nil)
}

func testVaultClusterWithConfig(t *testing.T, isRoot bool, rawConf map[string]interface{}) (*VaultProvider, *vault.Core, net.Listener) {
	vaultLock.Lock()
	defer vaultLock.Unlock()

	if err := vault.AddTestLogicalBackend("pki", pki.Factory); err != nil {
		t.Fatal(err)
	}
	core, _, token := vault.TestCoreUnsealedRaw(t)

	ln, addr := vaulthttp.TestServer(t, core)

	conf := map[string]interface{}{
		"Address":             addr,
		"Token":               token,
		"RootPKIPath":         "pki-root/",
		"IntermediatePKIPath": "pki-intermediate/",
		// Tests duration parsing after msgpack type mangling during raft apply.
		"LeafCertTTL": []uint8("72h"),
	}
	for k, v := range rawConf {
		conf[k] = v
	}

	require := require.New(t)
	provider := &VaultProvider{}
	require.NoError(provider.Configure("asdf", isRoot, conf))
	if isRoot {
		require.NoError(provider.GenerateRoot())
		_, err := provider.GenerateIntermediate()
		require.NoError(err)
	}

	return provider, core, ln
}

func TestVaultCAProvider_VaultTLSConfig(t *testing.T) {
	config := &structs.VaultCAProviderConfig{
		CAFile:        "/capath/ca.pem",
		CAPath:        "/capath/",
		CertFile:      "/certpath/cert.pem",
		KeyFile:       "/certpath/key.pem",
		TLSServerName: "server.name",
		TLSSkipVerify: true,
	}
	tlsConfig := vaultTLSConfig(config)
	require := require.New(t)
	require.Equal(config.CAFile, tlsConfig.CACert)
	require.Equal(config.CAPath, tlsConfig.CAPath)
	require.Equal(config.CertFile, tlsConfig.ClientCert)
	require.Equal(config.KeyFile, tlsConfig.ClientKey)
	require.Equal(config.TLSServerName, tlsConfig.TLSServerName)
	require.Equal(config.TLSSkipVerify, tlsConfig.Insecure)
}

func TestVaultCAProvider_Bootstrap(t *testing.T) {
	t.Parallel()

	require := require.New(t)
	provider, core, listener := testVaultCluster(t)
	defer core.Shutdown()
	defer listener.Close()
	client, err := vaultapi.NewClient(&vaultapi.Config{
		Address: "http://" + listener.Addr().String(),
	})
	require.NoError(err)
	client.SetToken(provider.config.Token)

	cases := []struct {
		certFunc    func() (string, error)
		backendPath string
	}{
		{
			certFunc:    provider.ActiveRoot,
			backendPath: "pki-root/",
		},
		{
			certFunc:    provider.ActiveIntermediate,
			backendPath: "pki-intermediate/",
		},
	}

	// Verify the root and intermediate certs match the ones in the vault backends
	for _, tc := range cases {
		cert, err := tc.certFunc()
		require.NoError(err)
		req := client.NewRequest("GET", "/v1/"+tc.backendPath+"ca/pem")
		resp, err := client.RawRequest(req)
		require.NoError(err)
		bytes, err := ioutil.ReadAll(resp.Body)
		require.NoError(err)
		require.Equal(cert, string(bytes))

		// Should be a valid CA cert
		parsed, err := connect.ParseCert(cert)
		require.NoError(err)
		require.True(parsed.IsCA)
		require.Len(parsed.URIs, 1)
		require.Equal(parsed.URIs[0].String(), fmt.Sprintf("spiffe://%s.consul", provider.clusterId))
	}
}

func TestVaultCAProvider_SignLeaf(t *testing.T) {
	t.Parallel()

	require := require.New(t)
	provider, core, listener := testVaultClusterWithConfig(t, true, map[string]interface{}{
		"LeafCertTTL": "1h",
	})
	defer core.Shutdown()
	defer listener.Close()
	client, err := vaultapi.NewClient(&vaultapi.Config{
		Address: "http://" + listener.Addr().String(),
	})
	require.NoError(err)
	client.SetToken(provider.config.Token)

	spiffeService := &connect.SpiffeIDService{
		Host:       "node1",
		Namespace:  "default",
		Datacenter: "dc1",
		Service:    "foo",
	}

	// Generate a leaf cert for the service.
	var firstSerial uint64
	{
		raw, _ := connect.TestCSR(t, spiffeService)

		csr, err := connect.ParseCSR(raw)
		require.NoError(err)

		cert, err := provider.Sign(csr)
		require.NoError(err)

		parsed, err := connect.ParseCert(cert)
		require.NoError(err)
		require.Equal(parsed.URIs[0], spiffeService.URI())
		firstSerial = parsed.SerialNumber.Uint64()

		// Ensure the cert is valid now and expires within the correct limit.
		now := time.Now()
		require.True(parsed.NotAfter.Sub(now) < time.Hour)
		require.True(parsed.NotBefore.Before(now))
	}

	// Generate a new cert for another service and make sure
	// the serial number is unique.
	spiffeService.Service = "bar"
	{
		raw, _ := connect.TestCSR(t, spiffeService)

		csr, err := connect.ParseCSR(raw)
		require.NoError(err)

		cert, err := provider.Sign(csr)
		require.NoError(err)

		parsed, err := connect.ParseCert(cert)
		require.NoError(err)
		require.Equal(parsed.URIs[0], spiffeService.URI())
		require.NotEqual(firstSerial, parsed.SerialNumber.Uint64())

		// Ensure the cert is valid now and expires within the correct limit.
		require.True(parsed.NotAfter.Sub(time.Now()) < time.Hour)
		require.True(parsed.NotBefore.Before(time.Now()))
	}
}

func TestVaultCAProvider_CrossSignCA(t *testing.T) {
	t.Parallel()

	provider1, core1, listener1 := testVaultCluster(t)
	defer core1.Shutdown()
	defer listener1.Close()

	provider2, core2, listener2 := testVaultCluster(t)
	defer core2.Shutdown()
	defer listener2.Close()

	testCrossSignProviders(t, provider1, provider2)
}

func TestVaultProvider_SignIntermediate(t *testing.T) {
	t.Parallel()

	provider1, core1, listener1 := testVaultCluster(t)
	defer core1.Shutdown()
	defer listener1.Close()

	provider2, core2, listener2 := testVaultClusterWithConfig(t, false, nil)
	defer core2.Shutdown()
	defer listener2.Close()

	testSignIntermediateCrossDC(t, provider1, provider2)
}

func TestVaultProvider_SignIntermediateConsul(t *testing.T) {
	t.Parallel()

	require := require.New(t)

	// primary = Vault, secondary = Consul
	{
		provider1, core, listener := testVaultCluster(t)
		defer core.Shutdown()
		defer listener.Close()

		conf := testConsulCAConfig()
		delegate := newMockDelegate(t, conf)
		provider2 := &ConsulProvider{Delegate: delegate}
		require.NoError(provider2.Configure(conf.ClusterID, false, conf.Config))

		testSignIntermediateCrossDC(t, provider1, provider2)
	}

	// primary = Consul, secondary = Vault
	{
		conf := testConsulCAConfig()
		delegate := newMockDelegate(t, conf)
		provider1 := &ConsulProvider{Delegate: delegate}
		require.NoError(provider1.Configure(conf.ClusterID, true, conf.Config))
		require.NoError(provider1.GenerateRoot())

		provider2, core, listener := testVaultClusterWithConfig(t, false, nil)
		defer core.Shutdown()
		defer listener.Close()

		testSignIntermediateCrossDC(t, provider1, provider2)
	}
}
