package mongodb

import (
	"fmt"
	"path/filepath"
	"testing"
	"time"

	"github.com/docker/go-connections/nat"
	"github.com/stretchr/testify/require"
	"github.com/testcontainers/testcontainers-go/wait"

	"github.com/influxdata/telegraf/config"
	"github.com/influxdata/telegraf/plugins/common/tls"
	"github.com/influxdata/telegraf/testutil"
)

func TestConnectAndWriteIntegrationNoAuth(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	servicePort := "27017"
	container := testutil.Container{
		Image:        "mongo",
		ExposedPorts: []string{servicePort},
		WaitingFor: wait.ForAll(
			wait.NewHTTPStrategy("/").WithPort(nat.Port(servicePort)),
			wait.ForLog("Waiting for connections"),
		),
	}
	err := container.Start()
	require.NoError(t, err, "failed to start container")
	defer container.Terminate()

	// Run test
	plugin := &MongoDB{
		Dsn: fmt.Sprintf("mongodb://%s:%s",
			container.Address,
			container.Ports[servicePort],
		),
		AuthenticationType: "NONE",
		MetricDatabase:     "telegraf_test",
		MetricGranularity:  "seconds",
	}

	// validate config
	require.NoError(t, plugin.Init())
	require.NoError(t, plugin.Connect())
	require.NoError(t, plugin.Write(testutil.MockMetrics()))
	require.NoError(t, plugin.Close())
}

func TestConnectAndWriteIntegrationSCRAMAuth(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	initdb, err := filepath.Abs("testdata/auth_scram/setup.js")
	require.NoError(t, err)

	servicePort := "27017"
	container := testutil.Container{
		Image:        "mongo",
		ExposedPorts: []string{servicePort},
		Files: map[string]string{
			"/docker-entrypoint-initdb.d/setup.js": initdb,
		},
		WaitingFor: wait.ForAll(
			wait.NewHTTPStrategy("/").WithPort(nat.Port(servicePort)),
			wait.ForLog("Waiting for connections").WithOccurrence(2),
		),
	}
	err = container.Start()
	require.NoError(t, err, "failed to start container")
	defer container.Terminate()

	tests := []struct {
		name        string
		plugin      *MongoDB
		connErrFunc func(t *testing.T, err error)
	}{
		{
			name: "success with scram authentication",
			plugin: &MongoDB{
				Dsn: fmt.Sprintf("mongodb://%s:%s/admin",
					container.Address, container.Ports[servicePort]),
				AuthenticationType: "SCRAM",
				Username:           config.NewSecret([]byte("root")),
				Password:           config.NewSecret([]byte("changeme")),
				MetricDatabase:     "telegraf_test",
				MetricGranularity:  "seconds",
			},
			connErrFunc: func(t *testing.T, err error) {
				require.NoError(t, err)
			},
		},
		{
			name: "fail with scram authentication bad password",
			plugin: &MongoDB{
				Dsn: fmt.Sprintf("mongodb://%s:%s/admin",
					container.Address, container.Ports[servicePort]),
				AuthenticationType:  "SCRAM",
				Username:            config.NewSecret([]byte("root")),
				Password:            config.NewSecret([]byte("root")),
				MetricDatabase:      "telegraf_test",
				MetricGranularity:   "seconds",
				ServerSelectTimeout: config.Duration(time.Duration(5) * time.Second),
			},
			connErrFunc: func(t *testing.T, err error) {
				require.Error(t, err)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// validate config
			err := tt.plugin.Init()
			require.NoError(t, err)

			if err == nil {
				// connect
				err = tt.plugin.Connect()
				tt.connErrFunc(t, err)

				if err == nil {
					// insert mock metrics
					err = tt.plugin.Write(testutil.MockMetrics())
					require.NoError(t, err)

					// cleanup
					err = tt.plugin.Close()
					require.NoError(t, err)
				}
			}
		})
	}
}

func TestConnectAndWriteIntegrationX509Auth(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	pki := testutil.NewPKI("../../../testutil/pki")

	// bind mount files
	initdb, err := filepath.Abs("testdata/auth_x509/setup.js")
	require.NoError(t, err)
	cacert, err := filepath.Abs(pki.CACertPath())
	require.NoError(t, err)
	serverpem, err := filepath.Abs(pki.ServerCertAndKeyPath())
	require.NoError(t, err)

	servicePort := "27017"
	container := testutil.Container{
		Image:        "mongo",
		ExposedPorts: []string{servicePort},
		Files: map[string]string{
			"/docker-entrypoint-initdb.d/setup.js": initdb,
			"/cacert.pem":                          cacert,
			"/server.pem":                          serverpem,
		},
		Entrypoint: []string{
			"docker-entrypoint.sh",
			"--auth", "--setParameter", "authenticationMechanisms=MONGODB-X509",
			"--tlsMode", "preferTLS",
			"--tlsCAFile", "/cacert.pem",
			"--tlsCertificateKeyFile", "/server.pem",
		},
		WaitingFor: wait.ForAll(
			wait.NewHTTPStrategy("/").WithPort(nat.Port(servicePort)),
			wait.ForLog("Waiting for connections").WithOccurrence(2),
		),
	}
	err = container.Start()
	require.NoError(t, err, "failed to start container")
	defer container.Terminate()

	tests := []struct {
		name        string
		plugin      *MongoDB
		connErrFunc func(t *testing.T, err error)
	}{
		{
			name: "success with x509 authentication",
			plugin: &MongoDB{
				Dsn: fmt.Sprintf("mongodb://%s:%s",
					container.Address, container.Ports[servicePort]),
				AuthenticationType:  "X509",
				MetricDatabase:      "telegraf_test",
				MetricGranularity:   "seconds",
				ServerSelectTimeout: config.Duration(time.Duration(5) * time.Second),
				TTL:                 config.Duration(time.Duration(5) * time.Minute),
				ClientConfig: tls.ClientConfig{
					TLSCA:              pki.CACertPath(),
					TLSKey:             pki.ClientCertAndKeyPath(),
					InsecureSkipVerify: false,
				},
			},
			connErrFunc: func(t *testing.T, err error) {
				require.NoError(t, err)
			},
		},
		{
			name: "success with x509 authentication using encrypted key file",
			plugin: &MongoDB{
				Dsn: fmt.Sprintf("mongodb://%s:%s",
					container.Address, container.Ports[servicePort]),
				AuthenticationType:  "X509",
				MetricDatabase:      "telegraf_test",
				MetricGranularity:   "seconds",
				ServerSelectTimeout: config.Duration(time.Duration(5) * time.Second),
				TTL:                 config.Duration(time.Duration(5) * time.Minute),
				ClientConfig: tls.ClientConfig{
					TLSCA:              pki.CACertPath(),
					TLSKey:             pki.ClientCertAndEncKeyPath(),
					TLSKeyPwd:          "changeme",
					InsecureSkipVerify: false,
				},
			},
			connErrFunc: func(t *testing.T, err error) {
				require.NoError(t, err)
			},
		},
		{
			name: "success with x509 authentication missing ca and using insceure tls",
			plugin: &MongoDB{
				Dsn: fmt.Sprintf("mongodb://%s:%s",
					container.Address, container.Ports[servicePort]),
				AuthenticationType:  "X509",
				MetricDatabase:      "telegraf_test",
				MetricGranularity:   "seconds",
				ServerSelectTimeout: config.Duration(time.Duration(5) * time.Second),
				TTL:                 config.Duration(time.Duration(5) * time.Minute),
				ClientConfig: tls.ClientConfig{
					TLSKey:             pki.ClientCertAndKeyPath(),
					InsecureSkipVerify: true,
				},
			},
			connErrFunc: func(t *testing.T, err error) {
				require.NoError(t, err)
			},
		},
		{
			name: "fail with x509 authentication missing ca",
			plugin: &MongoDB{
				Dsn: fmt.Sprintf("mongodb://%s:%s",
					container.Address, container.Ports[servicePort]),
				AuthenticationType:  "X509",
				MetricDatabase:      "telegraf_test",
				MetricGranularity:   "seconds",
				ServerSelectTimeout: config.Duration(time.Duration(5) * time.Second),
				TTL:                 config.Duration(time.Duration(5) * time.Minute),
				ClientConfig: tls.ClientConfig{
					TLSKey:             pki.ClientCertAndKeyPath(),
					InsecureSkipVerify: false,
				},
			},
			connErrFunc: func(t *testing.T, err error) {
				require.Error(t, err)
			},
		},
		{
			name: "fail with x509 authentication using encrypted key file",
			plugin: &MongoDB{
				Dsn: fmt.Sprintf("mongodb://%s:%s",
					container.Address, container.Ports[servicePort]),
				AuthenticationType:  "X509",
				MetricDatabase:      "telegraf_test",
				MetricGranularity:   "seconds",
				ServerSelectTimeout: config.Duration(time.Duration(5) * time.Second),
				TTL:                 config.Duration(time.Duration(5) * time.Minute),
				ClientConfig: tls.ClientConfig{
					TLSCA:              pki.CACertPath(),
					TLSKey:             pki.ClientCertAndEncKeyPath(),
					TLSKeyPwd:          "badpassword",
					InsecureSkipVerify: false,
				},
			},
			connErrFunc: func(t *testing.T, err error) {
				require.Error(t, err)
			},
		},
		{
			name: "fail with x509 authentication using invalid ca",
			plugin: &MongoDB{
				Dsn: fmt.Sprintf("mongodb://%s:%s",
					container.Address, container.Ports[servicePort]),
				AuthenticationType:  "X509",
				MetricDatabase:      "telegraf_test",
				MetricGranularity:   "seconds",
				ServerSelectTimeout: config.Duration(time.Duration(5) * time.Second),
				TTL:                 config.Duration(time.Duration(5) * time.Minute),
				ClientConfig: tls.ClientConfig{
					TLSCA:              pki.ClientCertAndKeyPath(),
					TLSKey:             pki.ClientCertAndKeyPath(),
					InsecureSkipVerify: false,
				},
			},
			connErrFunc: func(t *testing.T, err error) {
				require.Error(t, err)
			},
		},
		{
			name: "fail with x509 authentication using invalid key",
			plugin: &MongoDB{
				Dsn: fmt.Sprintf("mongodb://%s:%s",
					container.Address, container.Ports[servicePort]),
				AuthenticationType:  "X509",
				MetricDatabase:      "telegraf_test",
				MetricGranularity:   "seconds",
				ServerSelectTimeout: config.Duration(time.Duration(5) * time.Second),
				TTL:                 config.Duration(time.Duration(5) * time.Minute),
				ClientConfig: tls.ClientConfig{
					TLSCA:              pki.CACertPath(),
					TLSKey:             pki.CACertPath(),
					InsecureSkipVerify: false,
				},
			},
			connErrFunc: func(t *testing.T, err error) {
				require.Error(t, err)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// validate config
			err := tt.plugin.Init()
			require.NoError(t, err)

			if err == nil {
				// connect
				err = tt.plugin.Connect()
				tt.connErrFunc(t, err)

				if err == nil {
					// insert mock metrics
					err = tt.plugin.Write(testutil.MockMetrics())
					require.NoError(t, err)

					// cleanup
					err = tt.plugin.Close()
					require.NoError(t, err)
				}
			}
		})
	}
}

func TestConfiguration(t *testing.T) {
	tests := []struct {
		name    string
		plugin  *MongoDB
		errFunc func(t *testing.T, err error)
	}{
		{
			name: "fail with invalid connection string",
			plugin: &MongoDB{
				Dsn:                "asdf1234",
				AuthenticationType: "NONE",
				MetricDatabase:     "telegraf_test",
				MetricGranularity:  "seconds",
				TTL:                config.Duration(time.Duration(5) * time.Minute),
			},
		},
		{
			name: "fail with invalid metric granularity",
			plugin: &MongoDB{
				Dsn:                "mongodb://localhost:27017",
				AuthenticationType: "NONE",
				MetricDatabase:     "telegraf_test",
				MetricGranularity:  "somerandomgranularitythatdoesntwork",
			},
		},
		{
			name: "fail with scram authentication missing username field",
			plugin: &MongoDB{
				Dsn:                "mongodb://localhost:27017",
				AuthenticationType: "SCRAM",
				Password:           config.NewSecret([]byte("somerandompasswordthatwontwork")),
				MetricDatabase:     "telegraf_test",
				MetricGranularity:  "seconds",
			},
		},
		{
			name: "fail with scram authentication missing password field",
			plugin: &MongoDB{
				Dsn:                "mongodb://localhost:27017",
				AuthenticationType: "SCRAM",
				Username:           config.NewSecret([]byte("somerandomusernamethatwontwork")),
				MetricDatabase:     "telegraf_test",
				MetricGranularity:  "seconds",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// validate config
			err := tt.plugin.Init()
			require.Error(t, err)
		})
	}

	tests = []struct {
		name    string
		plugin  *MongoDB
		errFunc func(t *testing.T, err error)
	}{
		{
			name: "success init with missing metric database",
			plugin: &MongoDB{
				Dsn:                "mongodb://localhost:27017",
				AuthenticationType: "NONE",
				MetricGranularity:  "seconds",
			},
		},
		{
			name: "success init missing metric granularity",
			plugin: &MongoDB{
				Dsn:                "mongodb://localhost:27017",
				AuthenticationType: "NONE",
				MetricDatabase:     "telegraf_test",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// validate config
			err := tt.plugin.Init()
			require.NoError(t, err)
		})
	}
}
