package ldap

import (
	"testing"
	"time"

	"github.com/go-ldap/ldap/v3"
	"github.com/stretchr/testify/require"
	"github.com/testcontainers/testcontainers-go/wait"

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

const (
	servicePortOpenLDAP       = "1389"
	servicePortOpenLDAPSecure = "1636"

	servicePort389DS = "3389"
)

func TestMockResult(t *testing.T) {
	// mock a query result
	mockSearchResult := &ldap.SearchResult{
		Entries: []*ldap.Entry{
			{
				DN:         "cn=Total,cn=Connections,cn=Monitor",
				Attributes: []*ldap.EntryAttribute{{Name: "monitorCounter", Values: []string{"1"}}},
			},
		},
	}

	// Setup the plugin
	plugin := &LDAP{}
	require.NoError(t, plugin.Init())

	// Setup the expectations
	expected := []telegraf.Metric{
		metric.New(
			"openldap",
			map[string]string{
				"server": "localhost",
				"port":   "389",
			},
			map[string]interface{}{
				"total_connections": int64(1),
			},
			time.Unix(0, 0),
		),
	}

	// Retrieve the converter
	requests := plugin.newOpenLDAPConfig()
	require.Len(t, requests, 1)
	converter := requests[0].convert
	require.NotNil(t, converter)

	// Test metric conversion
	actual := converter(mockSearchResult, time.Unix(0, 0))
	testutil.RequireMetricsEqual(t, expected, actual)
}

func TestMockLDAPI(t *testing.T) {
	// mock a query result
	mockSearchResult := &ldap.SearchResult{
		Entries: []*ldap.Entry{
			{
				DN:         "cn=Total,cn=Connections,cn=Monitor",
				Attributes: []*ldap.EntryAttribute{{Name: "monitorCounter", Values: []string{"1"}}},
			},
		},
	}

	// Setup the plugin
	plugin := &LDAP{
		Server: "ldapi://%2Ftmp%2Fsocket%3F",
	}
	require.NoError(t, plugin.Init())

	// Setup the expectations
	expected := []telegraf.Metric{
		metric.New(
			"openldap",
			map[string]string{
				"path": "/tmp/socket?",
			},
			map[string]interface{}{
				"total_connections": int64(1),
			},
			time.Unix(0, 0),
		),
	}

	// Retrieve the converter
	requests := plugin.newOpenLDAPConfig()
	require.Len(t, requests, 1)
	converter := requests[0].convert
	require.NotNil(t, converter)

	// Test metric conversion
	actual := converter(mockSearchResult, time.Unix(0, 0))
	testutil.RequireMetricsEqual(t, expected, actual)
}

func TestLdapiURLHandling(t *testing.T) {
	// Setup the plugin
	plugin := &LDAP{
		Server: "ldapi://%2Ftmp%2Fsocket%3F",
	}
	require.NoError(t, plugin.Init())

	// Test the resulting setting
	require.Equal(t, "/tmp/socket%3F", plugin.host)
	require.Empty(t, plugin.port)
}

func TestInvalidTLSMode(t *testing.T) {
	plugin := &LDAP{
		Server: "foo://localhost",
	}
	require.ErrorContains(t, plugin.Init(), "invalid scheme")
}

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

	// Setup the plugin
	plugin := &LDAP{Server: "ldap://nosuchhost"}
	require.NoError(t, plugin.Init())

	// Collect the metrics and compare
	var acc testutil.Accumulator
	require.ErrorContains(t, plugin.Gather(&acc), "connection failed")
	require.Empty(t, acc.GetTelegrafMetrics())
}

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

	// Start the docker container
	container := testutil.Container{
		Image:        "bitnamilegacy/openldap",
		ExposedPorts: []string{servicePortOpenLDAP},
		Env: map[string]string{
			"LDAP_ADMIN_USERNAME": "manager",
			"LDAP_ADMIN_PASSWORD": "secret",
		},
		WaitingFor: wait.ForAll(
			wait.ForLog("slapd starting"),
			wait.ForListeningPort(servicePortOpenLDAP),
		),
	}
	require.NoError(t, container.Start(), "failed to start container")
	defer container.Terminate()

	// Setup the plugin
	port := container.Ports[servicePortOpenLDAP]
	plugin := &LDAP{
		Server:       "ldap://" + container.Address + ":" + port,
		BindDn:       "CN=manager,DC=example,DC=org",
		BindPassword: config.NewSecret([]byte("secret")),
	}
	require.NoError(t, plugin.Init())

	// Setup the expectations
	expected := []telegraf.Metric{
		metric.New(
			"openldap",
			map[string]string{
				"server": container.Address,
				"port":   port,
			},
			map[string]interface{}{
				"abandon_operations_completed":     int64(0),
				"abandon_operations_initiated":     int64(0),
				"active_threads":                   int64(0),
				"add_operations_completed":         int64(0),
				"add_operations_initiated":         int64(0),
				"backload_threads":                 int64(0),
				"bind_operations_completed":        int64(0),
				"bind_operations_initiated":        int64(0),
				"bytes_statistics":                 int64(0),
				"compare_operations_completed":     int64(0),
				"compare_operations_initiated":     int64(0),
				"current_connections":              int64(0),
				"delete_operations_completed":      int64(0),
				"delete_operations_initiated":      int64(0),
				"entries_statistics":               int64(0),
				"extended_operations_completed":    int64(0),
				"extended_operations_initiated":    int64(0),
				"max_file_descriptors_connections": int64(0),
				"max_pending_threads":              int64(0),
				"max_threads":                      int64(0),
				"modify_operations_completed":      int64(0),
				"modify_operations_initiated":      int64(0),
				"modrdn_operations_completed":      int64(0),
				"modrdn_operations_initiated":      int64(0),
				"open_threads":                     int64(0),
				"operations_completed":             int64(0),
				"operations_initiated":             int64(0),
				"pdu_statistics":                   int64(0),
				"pending_threads":                  int64(0),
				"read_waiters":                     int64(0),
				"referrals_statistics":             int64(0),
				"search_operations_completed":      int64(0),
				"search_operations_initiated":      int64(0),
				"starting_threads":                 int64(0),
				"total_connections":                int64(0),
				"unbind_operations_completed":      int64(0),
				"unbind_operations_initiated":      int64(0),
				"uptime_time":                      int64(0),
				"write_waiters":                    int64(0),
			},
			time.Unix(0, 0),
		),
	}

	// Collect the metrics and compare
	var acc testutil.Accumulator
	require.NoError(t, plugin.Gather(&acc))

	actual := acc.GetTelegrafMetrics()
	testutil.RequireMetricsStructureEqual(t, expected, actual, testutil.IgnoreTime())
}

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

	// Start the docker container
	container := testutil.Container{
		Image:        "bitnamilegacy/openldap",
		ExposedPorts: []string{servicePortOpenLDAP},
		Env: map[string]string{
			"LDAP_ADMIN_USERNAME": "manager",
			"LDAP_ADMIN_PASSWORD": "secret",
		},
		WaitingFor: wait.ForAll(
			wait.ForLog("slapd starting"),
			wait.ForListeningPort(servicePortOpenLDAP),
		),
	}
	require.NoError(t, container.Start(), "failed to start container")
	defer container.Terminate()

	// Setup the plugin
	port := container.Ports[servicePortOpenLDAP]
	plugin := &LDAP{
		Server:            "ldap://" + container.Address + ":" + port,
		BindDn:            "CN=manager,DC=example,DC=org",
		BindPassword:      config.NewSecret([]byte("secret")),
		ReverseFieldNames: true,
	}
	require.NoError(t, plugin.Init())

	// Setup the expectations
	expected := []telegraf.Metric{
		metric.New(
			"openldap",
			map[string]string{
				"server": container.Address,
				"port":   port,
			},
			map[string]interface{}{
				"connections_max_file_descriptors": int64(0),
				"connections_total":                int64(0),
				"connections_current":              int64(0),
				"operations_bind_initiated":        int64(0),
				"operations_bind_completed":        int64(0),
				"operations_completed":             int64(0),
				"operations_initiated":             int64(0),
				"operations_unbind_initiated":      int64(0),
				"operations_unbind_completed":      int64(0),
				"operations_search_initiated":      int64(0),
				"operations_search_completed":      int64(0),
				"operations_compare_initiated":     int64(0),
				"operations_compare_completed":     int64(0),
				"operations_modify_initiated":      int64(0),
				"operations_modify_completed":      int64(0),
				"operations_modrdn_initiated":      int64(0),
				"operations_modrdn_completed":      int64(0),
				"operations_add_initiated":         int64(0),
				"operations_add_completed":         int64(0),
				"operations_delete_initiated":      int64(0),
				"operations_delete_completed":      int64(0),
				"operations_abandon_initiated":     int64(0),
				"operations_abandon_completed":     int64(0),
				"operations_extended_initiated":    int64(0),
				"operations_extended_completed":    int64(0),
				"statistics_bytes":                 int64(0),
				"statistics_pdu":                   int64(0),
				"statistics_entries":               int64(0),
				"statistics_referrals":             int64(0),
				"threads_max":                      int64(0),
				"threads_max_pending":              int64(0),
				"threads_open":                     int64(0),
				"threads_starting":                 int64(0),
				"threads_active":                   int64(0),
				"threads_pending":                  int64(0),
				"threads_backload":                 int64(0),
				"time_uptime":                      int64(0),
				"waiters_read":                     int64(0),
				"waiters_write":                    int64(0),
			},
			time.Unix(0, 0),
		),
	}

	// Collect the metrics and compare
	var acc testutil.Accumulator
	require.NoError(t, plugin.Gather(&acc))

	actual := acc.GetTelegrafMetrics()
	testutil.RequireMetricsStructureEqual(t, expected, actual, testutil.IgnoreTime())
}

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

	// Setup PKI for TLS testing
	pkiPaths, err := testutil.NewPKI("../../../testutil/pki").AbsolutePaths()
	require.NoError(t, err)

	// Start the docker container
	container := testutil.Container{
		Image:        "bitnamilegacy/openldap",
		ExposedPorts: []string{servicePortOpenLDAP},
		Env: map[string]string{
			"LDAP_ADMIN_USERNAME": "manager",
			"LDAP_ADMIN_PASSWORD": "secret",
			"LDAP_ENABLE_TLS":     "yes",
			"LDAP_TLS_CA_FILE":    "server.pem",
			"LDAP_TLS_CERT_FILE":  "server.crt",
			"LDAP_TLS_KEY_FILE":   "server.key",
		},
		Files: map[string]string{
			"/server.pem": pkiPaths.ServerPem,
			"/server.crt": pkiPaths.ServerCert,
			"/server.key": pkiPaths.ServerKey,
		},
		WaitingFor: wait.ForAll(
			wait.ForLog("slapd starting"),
			wait.ForListeningPort(servicePortOpenLDAP),
		),
	}
	require.NoError(t, container.Start(), "failed to start container")
	defer container.Terminate()

	// Setup the plugin
	port := container.Ports[servicePortOpenLDAP]
	plugin := &LDAP{
		Server:       "starttls://" + container.Address + ":" + port,
		BindDn:       "CN=manager,DC=example,DC=org",
		BindPassword: config.NewSecret([]byte("secret")),
		ClientConfig: common_tls.ClientConfig{
			TLSCA:              pkiPaths.ClientCert,
			InsecureSkipVerify: true,
		},
	}
	require.NoError(t, plugin.Init())

	// Setup the expectations
	expected := []telegraf.Metric{
		metric.New(
			"openldap",
			map[string]string{
				"server": container.Address,
				"port":   port,
			},
			map[string]interface{}{
				"abandon_operations_completed":     int64(0),
				"abandon_operations_initiated":     int64(0),
				"active_threads":                   int64(0),
				"add_operations_completed":         int64(0),
				"add_operations_initiated":         int64(0),
				"backload_threads":                 int64(0),
				"bind_operations_completed":        int64(0),
				"bind_operations_initiated":        int64(0),
				"bytes_statistics":                 int64(0),
				"compare_operations_completed":     int64(0),
				"compare_operations_initiated":     int64(0),
				"current_connections":              int64(0),
				"delete_operations_completed":      int64(0),
				"delete_operations_initiated":      int64(0),
				"entries_statistics":               int64(0),
				"extended_operations_completed":    int64(0),
				"extended_operations_initiated":    int64(0),
				"max_file_descriptors_connections": int64(0),
				"max_pending_threads":              int64(0),
				"max_threads":                      int64(0),
				"modify_operations_completed":      int64(0),
				"modify_operations_initiated":      int64(0),
				"modrdn_operations_completed":      int64(0),
				"modrdn_operations_initiated":      int64(0),
				"open_threads":                     int64(0),
				"operations_completed":             int64(0),
				"operations_initiated":             int64(0),
				"pdu_statistics":                   int64(0),
				"pending_threads":                  int64(0),
				"read_waiters":                     int64(0),
				"referrals_statistics":             int64(0),
				"search_operations_completed":      int64(0),
				"search_operations_initiated":      int64(0),
				"starting_threads":                 int64(0),
				"total_connections":                int64(0),
				"unbind_operations_completed":      int64(0),
				"unbind_operations_initiated":      int64(0),
				"uptime_time":                      int64(0),
				"write_waiters":                    int64(0),
			},
			time.Unix(0, 0),
		),
	}

	// Collect the metrics and compare
	var acc testutil.Accumulator
	require.NoError(t, plugin.Gather(&acc))

	actual := acc.GetTelegrafMetrics()
	testutil.RequireMetricsStructureEqual(t, expected, actual, testutil.IgnoreTime())
}

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

	// Setup PKI for TLS testing
	pkiPaths, err := testutil.NewPKI("../../../testutil/pki").AbsolutePaths()
	require.NoError(t, err)

	// Start the docker container
	container := testutil.Container{
		Image:        "bitnamilegacy/openldap",
		ExposedPorts: []string{servicePortOpenLDAPSecure},
		Env: map[string]string{
			"LDAP_ADMIN_USERNAME": "manager",
			"LDAP_ADMIN_PASSWORD": "secret",
			"LDAP_ENABLE_TLS":     "yes",
			"LDAP_TLS_CA_FILE":    "server.pem",
			"LDAP_TLS_CERT_FILE":  "server.crt",
			"LDAP_TLS_KEY_FILE":   "server.key",
		},
		Files: map[string]string{
			"/server.pem": pkiPaths.ServerPem,
			"/server.crt": pkiPaths.ServerCert,
			"/server.key": pkiPaths.ServerKey,
		},
		WaitingFor: wait.ForAll(
			wait.ForLog("slapd starting"),
			wait.ForListeningPort(servicePortOpenLDAPSecure),
		),
	}
	require.NoError(t, container.Start(), "failed to start container")
	defer container.Terminate()

	// Setup the plugin
	port := container.Ports[servicePortOpenLDAPSecure]
	plugin := &LDAP{
		Server:       "ldaps://" + container.Address + ":" + port,
		BindDn:       "CN=manager,DC=example,DC=org",
		BindPassword: config.NewSecret([]byte("secret")),
		ClientConfig: common_tls.ClientConfig{
			InsecureSkipVerify: true,
		},
	}
	require.NoError(t, plugin.Init())

	// Setup the expectations
	expected := []telegraf.Metric{
		metric.New(
			"openldap",
			map[string]string{
				"server": container.Address,
				"port":   port,
			},
			map[string]interface{}{
				"abandon_operations_completed":     int64(0),
				"abandon_operations_initiated":     int64(0),
				"active_threads":                   int64(0),
				"add_operations_completed":         int64(0),
				"add_operations_initiated":         int64(0),
				"backload_threads":                 int64(0),
				"bind_operations_completed":        int64(0),
				"bind_operations_initiated":        int64(0),
				"bytes_statistics":                 int64(0),
				"compare_operations_completed":     int64(0),
				"compare_operations_initiated":     int64(0),
				"current_connections":              int64(0),
				"delete_operations_completed":      int64(0),
				"delete_operations_initiated":      int64(0),
				"entries_statistics":               int64(0),
				"extended_operations_completed":    int64(0),
				"extended_operations_initiated":    int64(0),
				"max_file_descriptors_connections": int64(0),
				"max_pending_threads":              int64(0),
				"max_threads":                      int64(0),
				"modify_operations_completed":      int64(0),
				"modify_operations_initiated":      int64(0),
				"modrdn_operations_completed":      int64(0),
				"modrdn_operations_initiated":      int64(0),
				"open_threads":                     int64(0),
				"operations_completed":             int64(0),
				"operations_initiated":             int64(0),
				"pdu_statistics":                   int64(0),
				"pending_threads":                  int64(0),
				"read_waiters":                     int64(0),
				"referrals_statistics":             int64(0),
				"search_operations_completed":      int64(0),
				"search_operations_initiated":      int64(0),
				"starting_threads":                 int64(0),
				"total_connections":                int64(0),
				"unbind_operations_completed":      int64(0),
				"unbind_operations_initiated":      int64(0),
				"uptime_time":                      int64(0),
				"write_waiters":                    int64(0),
			},
			time.Unix(0, 0),
		),
	}

	// Collect the metrics and compare
	var acc testutil.Accumulator
	require.NoError(t, plugin.Gather(&acc))

	actual := acc.GetTelegrafMetrics()
	testutil.RequireMetricsStructureEqual(t, expected, actual, testutil.IgnoreTime())
}

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

	// Start the docker container
	container := testutil.Container{
		Image:        "389ds/dirsrv",
		ExposedPorts: []string{servicePort389DS},
		Env: map[string]string{
			"DS_DM_PASSWORD": "secret",
		},
		WaitingFor: wait.ForAll(
			wait.ForLog("389-ds-container started"),
			wait.ForListeningPort(servicePort389DS),
		),
	}
	require.NoError(t, container.Start(), "failed to start container")
	defer container.Terminate()

	// Setup the plugin
	port := container.Ports[servicePort389DS]
	plugin := &LDAP{
		Server:       "ldap://" + container.Address + ":" + port,
		Dialect:      "389ds",
		BindDn:       "cn=Directory Manager",
		BindPassword: config.NewSecret([]byte("secret")),
	}
	require.NoError(t, plugin.Init())

	// Setup the expectations
	expected := []telegraf.Metric{
		metric.New(
			"389ds",
			map[string]string{
				"server": container.Address,
				"port":   port,
			},
			map[string]interface{}{
				"add_operations":                     int64(0),
				"anonymous_binds":                    int64(0),
				"backends":                           int64(0),
				"bind_security_errors":               int64(0),
				"bytes_received":                     int64(0),
				"bytes_sent":                         int64(0),
				"cache_entries":                      int64(0),
				"cache_hits":                         int64(0),
				"chainings":                          int64(0),
				"compare_operations":                 int64(0),
				"connections":                        int64(0),
				"connections_in_max_threads":         int64(0),
				"connections_max_threads":            int64(0),
				"copy_entries":                       int64(0),
				"current_connections":                int64(0),
				"current_connections_at_max_threads": int64(0),
				"delete_operations":                  int64(0),
				"dtablesize":                         int64(0),
				"entries_returned":                   int64(0),
				"entries_sent":                       int64(0),
				"errors":                             int64(0),
				"in_operations":                      int64(0),
				"list_operations":                    int64(0),
				"maxthreads_per_conn_hits":           int64(0),
				"modify_operations":                  int64(0),
				"modrdn_operations":                  int64(0),
				"onelevel_search_operations":         int64(0),
				"operations_completed":               int64(0),
				"operations_initiated":               int64(0),
				"read_operations":                    int64(0),
				"read_waiters":                       int64(0),
				"referrals":                          int64(0),
				"referrals_returned":                 int64(0),
				"search_operations":                  int64(0),
				"security_errors":                    int64(0),
				"simpleauth_binds":                   int64(0),
				"strongauth_binds":                   int64(0),
				"threads":                            int64(0),
				"total_connections":                  int64(0),
				"unauth_binds":                       int64(0),
				"wholesubtree_search_operations":     int64(0),
			},
			time.Unix(0, 0),
		),
	}

	// Collect the metrics and compare
	var acc testutil.Accumulator
	require.NoError(t, plugin.Gather(&acc))

	actual := acc.GetTelegrafMetrics()
	testutil.RequireMetricsStructureEqual(t, expected, actual, testutil.IgnoreTime())
}
