package postgresql

import (
	"fmt"
	"strings"
	"testing"

	"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/testutil"
)

const servicePort = "5432"

func launchTestContainer(t *testing.T) *testutil.Container {
	container := testutil.Container{
		Image:        "postgres:alpine",
		ExposedPorts: []string{servicePort},
		Env: map[string]string{
			"POSTGRES_HOST_AUTH_METHOD": "trust",
		},
		WaitingFor: wait.ForAll(
			// the database comes up twice, once right away, then again a second
			// time after the docker entrypoint starts configuration
			wait.ForLog("database system is ready to accept connections").WithOccurrence(2),
			wait.ForListeningPort(nat.Port(servicePort)),
		),
	}

	err := container.Start()
	require.NoError(t, err, "failed to start container")

	return &container
}

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

	container := launchTestContainer(t)
	defer container.Terminate()

	addr := fmt.Sprintf(
		"host=%s port=%s user=postgres sslmode=disable",
		container.Address,
		container.Ports[servicePort],
	)

	p := &Postgresql{
		Service: Service{
			Address:     config.NewSecret([]byte(addr)),
			IsPgBouncer: false,
		},
		Databases: []string{"postgres"},
	}

	var acc testutil.Accumulator
	require.NoError(t, p.Start(&acc))
	require.NoError(t, p.Gather(&acc))

	intMetrics := []string{
		"xact_commit",
		"xact_rollback",
		"blks_read",
		"blks_hit",
		"tup_returned",
		"tup_fetched",
		"tup_inserted",
		"tup_updated",
		"tup_deleted",
		"conflicts",
		"temp_files",
		"temp_bytes",
		"deadlocks",
		"buffers_alloc",
		"buffers_backend",
		"buffers_backend_fsync",
		"buffers_checkpoint",
		"buffers_clean",
		"checkpoints_req",
		"checkpoints_timed",
		"maxwritten_clean",
		"datid",
		"numbackends",
	}

	int32Metrics := []string{}

	floatMetrics := []string{
		"blk_read_time",
		"blk_write_time",
		"checkpoint_write_time",
		"checkpoint_sync_time",
	}

	stringMetrics := []string{
		"datname",
	}

	metricsCounted := 0

	for _, metric := range intMetrics {
		require.True(t, acc.HasInt64Field("postgresql", metric))
		metricsCounted++
	}

	for _, metric := range int32Metrics {
		require.True(t, acc.HasInt32Field("postgresql", metric))
		metricsCounted++
	}

	for _, metric := range floatMetrics {
		require.True(t, acc.HasFloatField("postgresql", metric))
		metricsCounted++
	}

	for _, metric := range stringMetrics {
		require.True(t, acc.HasStringField("postgresql", metric))
		metricsCounted++
	}

	require.Greater(t, metricsCounted, 0)
	require.Equal(t, len(floatMetrics)+len(intMetrics)+len(int32Metrics)+len(stringMetrics), metricsCounted)
}

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

	container := launchTestContainer(t)
	defer container.Terminate()

	addr := fmt.Sprintf(
		"host=%s port=%s user=postgres sslmode=disable",
		container.Address,
		container.Ports[servicePort],
	)

	p := &Postgresql{
		Service: Service{
			Address: config.NewSecret([]byte(addr)),
		},
		Databases: []string{"postgres"},
	}

	var acc testutil.Accumulator

	require.NoError(t, p.Start(&acc))
	require.NoError(t, p.Gather(&acc))

	point, ok := acc.Get("postgresql")
	require.True(t, ok)

	require.Equal(t, "postgres", point.Tags["db"])
}

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

	container := launchTestContainer(t)
	defer container.Terminate()

	addr := fmt.Sprintf(
		"host=%s port=%s user=postgres sslmode=disable",
		container.Address,
		container.Ports[servicePort],
	)

	p := &Postgresql{
		Service: Service{
			Address: config.NewSecret([]byte(addr)),
		},
	}

	var acc testutil.Accumulator

	require.NoError(t, p.Start(&acc))
	require.NoError(t, p.Gather(&acc))

	var found bool

	for _, pnt := range acc.Metrics {
		if pnt.Measurement == "postgresql" {
			if pnt.Tags["db"] == "postgres" {
				found = true
				break
			}
		}
	}

	require.True(t, found)
}

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

	container := launchTestContainer(t)
	defer container.Terminate()

	addr := fmt.Sprintf(
		"host=%s port=%s user=postgres sslmode=disable",
		container.Address,
		container.Ports[servicePort],
	)

	p := &Postgresql{
		Service: Service{
			Address: config.NewSecret([]byte(addr)),
		},
	}

	var acc testutil.Accumulator
	require.NoError(t, p.Start(&acc))
	require.NoError(t, p.Gather(&acc))

	for col := range p.IgnoredColumns() {
		require.False(t, acc.HasMeasurement(col))
	}
}

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

	container := launchTestContainer(t)
	defer container.Terminate()

	addr := fmt.Sprintf(
		"host=%s port=%s user=postgres sslmode=disable",
		container.Address,
		container.Ports[servicePort],
	)

	p := &Postgresql{
		Service: Service{
			Address: config.NewSecret([]byte(addr)),
		},
		Databases: []string{"template0"},
	}

	var acc testutil.Accumulator

	require.NoError(t, p.Start(&acc))
	require.NoError(t, p.Gather(&acc))

	var foundTemplate0 = false
	var foundTemplate1 = false

	for _, pnt := range acc.Metrics {
		if pnt.Measurement == "postgresql" {
			if pnt.Tags["db"] == "template0" {
				foundTemplate0 = true
			}
		}
		if pnt.Measurement == "postgresql" {
			if pnt.Tags["db"] == "template1" {
				foundTemplate1 = true
			}
		}
	}

	require.True(t, foundTemplate0)
	require.False(t, foundTemplate1)
}

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

	container := launchTestContainer(t)
	defer container.Terminate()

	addr := fmt.Sprintf(
		"host=%s port=%s user=postgres sslmode=disable",
		container.Address,
		container.Ports[servicePort],
	)

	p := &Postgresql{
		Service: Service{
			Address: config.NewSecret([]byte(addr)),
		},
		IgnoredDatabases: []string{"template0"},
	}

	var acc testutil.Accumulator
	require.NoError(t, p.Start(&acc))
	require.NoError(t, p.Gather(&acc))

	var foundTemplate0 = false
	var foundTemplate1 = false

	for _, pnt := range acc.Metrics {
		if pnt.Measurement == "postgresql" {
			if pnt.Tags["db"] == "template0" {
				foundTemplate0 = true
			}
		}
		if pnt.Measurement == "postgresql" {
			if pnt.Tags["db"] == "template1" {
				foundTemplate1 = true
			}
		}
	}

	require.False(t, foundTemplate0)
	require.True(t, foundTemplate1)
}

func TestURIParsing(t *testing.T) {
	tests := []struct {
		name     string
		uri      string
		expected string
	}{
		{
			name:     "short",
			uri:      `postgres://localhost`,
			expected: "host=localhost",
		},
		{
			name:     "with port",
			uri:      `postgres://localhost:5432`,
			expected: "host=localhost port=5432",
		},
		{
			name:     "with database",
			uri:      `postgres://localhost/mydb`,
			expected: "dbname=mydb host=localhost",
		},
		{
			name:     "with additional parameters",
			uri:      `postgres://localhost/mydb?application_name=pgxtest&search_path=myschema&connect_timeout=5`,
			expected: "application_name=pgxtest connect_timeout=5 dbname=mydb host=localhost search_path=myschema",
		},
		{
			name:     "with database setting in params",
			uri:      `postgres://localhost:5432/?database=mydb`,
			expected: "database=mydb host=localhost port=5432",
		},
		{
			name:     "with authentication",
			uri:      `postgres://jack:secret@localhost:5432/mydb?sslmode=prefer`,
			expected: "dbname=mydb host=localhost password=secret port=5432 sslmode=prefer user=jack",
		},
		{
			name:     "with spaces",
			uri:      `postgres://jack%20hunter:secret@localhost/mydb?application_name=pgx%20test`,
			expected: "application_name='pgx test' dbname=mydb host=localhost password=secret user='jack hunter'",
		},
		{
			name:     "with equal signs",
			uri:      `postgres://jack%20hunter:secret@localhost/mydb?application_name=pgx%3Dtest`,
			expected: "application_name='pgx=test' dbname=mydb host=localhost password=secret user='jack hunter'",
		},
		{
			name:     "multiple hosts",
			uri:      `postgres://jack:secret@foo:1,bar:2,baz:3/mydb?sslmode=disable`,
			expected: "dbname=mydb host=foo,bar,baz password=secret port=1,2,3 sslmode=disable user=jack",
		},
		{
			name:     "multiple hosts without ports",
			uri:      `postgres://jack:secret@foo,bar,baz/mydb?sslmode=disable`,
			expected: "dbname=mydb host=foo,bar,baz password=secret sslmode=disable user=jack",
		},
	}

	for _, tt := range tests {
		// Key value without spaces around equal sign
		t.Run(tt.name, func(t *testing.T) {
			actual, err := toKeyValue(tt.uri)
			require.NoError(t, err)
			require.Equalf(t, tt.expected, actual, "initial: %s", tt.uri)
		})
	}
}

func TestSanitizeAddressKeyValue(t *testing.T) {
	keys := []string{"password", "sslcert", "sslkey", "sslmode", "sslrootcert"}
	tests := []struct {
		name  string
		value string
	}{
		{
			name:  "simple text",
			value: `foo`,
		},
		{
			name:  "empty values",
			value: `''`,
		},
		{
			name:  "space in value",
			value: `'foo bar'`,
		},
		{
			name:  "equal sign in value",
			value: `'foo=bar'`,
		},
		{
			name:  "escaped quote",
			value: `'foo\'s bar'`,
		},
		{
			name:  "escaped quote no space",
			value: `\'foobar\'s\'`,
		},
		{
			name:  "escaped backslash",
			value: `'foo bar\\'`,
		},
		{
			name:  "escaped quote and backslash",
			value: `'foo\\\'s bar'`,
		},
		{
			name:  "two escaped backslashes",
			value: `'foo bar\\\\'`,
		},
		{
			name:  "multiple inline spaces",
			value: "'foo     \t bar'",
		},
		{
			name:  "leading space",
			value: `' foo bar'`,
		},
		{
			name:  "trailing space",
			value: `'foo bar '`,
		},
		{
			name:  "multiple equal signs",
			value: `'foo===bar'`,
		},
		{
			name:  "leading equal sign",
			value: `'=foo bar'`,
		},
		{
			name:  "trailing equal sign",
			value: `'foo bar='`,
		},
		{
			name:  "mix of equal signs and spaces",
			value: "'foo = a\t===\tbar'",
		},
	}

	for _, tt := range tests {
		// Key value without spaces around equal sign
		t.Run(tt.name, func(t *testing.T) {
			// Generate the DSN from the given keys and value
			parts := make([]string, 0, len(keys))
			for _, k := range keys {
				parts = append(parts, k+"="+tt.value)
			}
			dsn := strings.Join(parts, " canary=ok ")

			plugin := &Postgresql{
				Service: Service{
					Address: config.NewSecret([]byte(dsn)),
				},
			}

			expected := strings.Join(make([]string, len(keys)), "canary=ok ")
			expected = strings.TrimSpace(expected)
			actual, err := plugin.SanitizedAddress()
			require.NoError(t, err)
			require.Equalf(t, expected, actual, "initial: %s", dsn)
		})

		// Key value with spaces around equal sign
		t.Run("spaced "+tt.name, func(t *testing.T) {
			// Generate the DSN from the given keys and value
			parts := make([]string, 0, len(keys))
			for _, k := range keys {
				parts = append(parts, k+" = "+tt.value)
			}
			dsn := strings.Join(parts, " canary=ok ")

			plugin := &Postgresql{
				Service: Service{
					Address: config.NewSecret([]byte(dsn)),
				},
			}

			expected := strings.Join(make([]string, len(keys)), "canary=ok ")
			expected = strings.TrimSpace(expected)
			actual, err := plugin.SanitizedAddress()
			require.NoError(t, err)
			require.Equalf(t, expected, actual, "initial: %s", dsn)
		})
	}
}

func TestSanitizeAddressURI(t *testing.T) {
	keys := []string{"password", "sslcert", "sslkey", "sslmode", "sslrootcert"}
	tests := []struct {
		name  string
		value string
	}{
		{
			name:  "simple text",
			value: `foo`,
		},
		{
			name:  "empty values",
			value: ``,
		},
		{
			name:  "space in value",
			value: `foo bar`,
		},
		{
			name:  "equal sign in value",
			value: `foo=bar`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Generate the DSN from the given keys and value
			value := strings.ReplaceAll(tt.value, "=", "%3D")
			value = strings.ReplaceAll(value, " ", "%20")
			parts := make([]string, 0, len(keys))
			for _, k := range keys {
				parts = append(parts, k+"="+value)
			}
			dsn := "postgresql://user:passwd@localhost:5432/db?" + strings.Join(parts, "&")

			plugin := &Postgresql{
				Service: Service{
					Address: config.NewSecret([]byte(dsn)),
				},
			}

			expected := "dbname=db host=localhost port=5432 user=user"
			actual, err := plugin.SanitizedAddress()
			require.NoError(t, err)
			require.Equalf(t, expected, actual, "initial: %s", dsn)
		})
	}
}
