// SPDX-License-Identifier: GPL-3.0-or-later

package redis

import (
	"context"
	"errors"
	"os"
	"strings"
	"testing"

	"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
	"github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/tlscfg"

	"github.com/go-redis/redis/v8"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var (
	dataConfigJSON, _ = os.ReadFile("testdata/config.json")
	dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")

	dataPikaInfoAll, _   = os.ReadFile("testdata/pika/info_all.txt")
	dataVer609InfoAll, _ = os.ReadFile("testdata/v6.0.9/info_all.txt")
)

func Test_testDataIsValid(t *testing.T) {
	for name, data := range map[string][]byte{
		"dataConfigJSON":    dataConfigJSON,
		"dataConfigYAML":    dataConfigYAML,
		"dataPikaInfoAll":   dataPikaInfoAll,
		"dataVer609InfoAll": dataVer609InfoAll,
	} {
		require.NotNil(t, data, name)
	}
}

func TestRedis_ConfigurationSerialize(t *testing.T) {
	module.TestConfigurationSerialize(t, &Redis{}, dataConfigJSON, dataConfigYAML)
}

func TestRedis_Init(t *testing.T) {
	tests := map[string]struct {
		config   Config
		wantFail bool
	}{
		"success on default config": {
			config: New().Config,
		},
		"fails on unset 'address'": {
			wantFail: true,
			config:   Config{Address: ""},
		},
		"fails on invalid 'address' format": {
			wantFail: true,
			config:   Config{Address: "127.0.0.1:6379"},
		},
		"fails on invalid TLSCA": {
			wantFail: true,
			config: Config{
				Address:   "redis://127.0.0.1:6379",
				TLSConfig: tlscfg.TLSConfig{TLSCA: "testdata/tls"},
			},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			rdb := New()
			rdb.Config = test.config

			if test.wantFail {
				assert.Error(t, rdb.Init())
			} else {
				assert.NoError(t, rdb.Init())
			}
		})
	}
}

func TestRedis_Check(t *testing.T) {
	tests := map[string]struct {
		prepare  func(t *testing.T) *Redis
		wantFail bool
	}{
		"success on valid response v6.0.9": {
			prepare: prepareRedisV609,
		},
		"fails on error on Info": {
			wantFail: true,
			prepare:  prepareRedisErrorOnInfo,
		},
		"fails on response from not Redis instance": {
			wantFail: true,
			prepare:  prepareRedisWithPikaMetrics,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			rdb := test.prepare(t)

			if test.wantFail {
				assert.Error(t, rdb.Check())
			} else {
				assert.NoError(t, rdb.Check())
			}
		})
	}
}

func TestRedis_Charts(t *testing.T) {
	rdb := New()
	require.NoError(t, rdb.Init())

	assert.NotNil(t, rdb.Charts())
}

func TestRedis_Cleanup(t *testing.T) {
	rdb := New()
	assert.NotPanics(t, rdb.Cleanup)

	require.NoError(t, rdb.Init())
	m := &mockRedisClient{}
	rdb.rdb = m

	rdb.Cleanup()

	assert.True(t, m.calledClose)
}

func TestRedis_Collect(t *testing.T) {
	tests := map[string]struct {
		prepare       func(t *testing.T) *Redis
		wantCollected map[string]int64
	}{
		"success on valid response v6.0.9": {
			prepare: prepareRedisV609,
			wantCollected: map[string]int64{
				"active_defrag_hits":              0,
				"active_defrag_key_hits":          0,
				"active_defrag_key_misses":        0,
				"active_defrag_misses":            0,
				"active_defrag_running":           0,
				"allocator_active":                1208320,
				"allocator_allocated":             903408,
				"allocator_frag_bytes":            304912,
				"allocator_frag_ratio":            1340,
				"allocator_resident":              3723264,
				"allocator_rss_bytes":             2514944,
				"allocator_rss_ratio":             3080,
				"aof_base_size":                   116,
				"aof_buffer_length":               0,
				"aof_current_rewrite_time_sec":    -1,
				"aof_current_size":                294,
				"aof_delayed_fsync":               0,
				"aof_enabled":                     0,
				"aof_last_cow_size":               0,
				"aof_last_rewrite_time_sec":       -1,
				"aof_pending_bio_fsync":           0,
				"aof_pending_rewrite":             0,
				"aof_rewrite_buffer_length":       0,
				"aof_rewrite_in_progress":         0,
				"aof_rewrite_scheduled":           0,
				"arch_bits":                       64,
				"blocked_clients":                 0,
				"client_recent_max_input_buffer":  8,
				"client_recent_max_output_buffer": 0,
				"clients_in_timeout_table":        0,
				"cluster_enabled":                 0,
				"cmd_command_calls":               2,
				"cmd_command_usec":                2182,
				"cmd_command_usec_per_call":       1091000,
				"cmd_get_calls":                   2,
				"cmd_get_usec":                    29,
				"cmd_get_usec_per_call":           14500,
				"cmd_hello_calls":                 1,
				"cmd_hello_usec":                  15,
				"cmd_hello_usec_per_call":         15000,
				"cmd_hmset_calls":                 2,
				"cmd_hmset_usec":                  408,
				"cmd_hmset_usec_per_call":         204000,
				"cmd_info_calls":                  132,
				"cmd_info_usec":                   37296,
				"cmd_info_usec_per_call":          282550,
				"cmd_ping_calls":                  19,
				"cmd_ping_usec":                   286,
				"cmd_ping_usec_per_call":          15050,
				"cmd_set_calls":                   3,
				"cmd_set_usec":                    140,
				"cmd_set_usec_per_call":           46670,
				"configured_hz":                   10,
				"connected_clients":               1,
				"connected_slaves":                0,
				"db0_expires_keys":                0,
				"db0_keys":                        4,
				"evicted_keys":                    0,
				"expire_cycle_cpu_milliseconds":   28362,
				"expired_keys":                    0,
				"expired_stale_perc":              0,
				"expired_time_cap_reached_count":  0,
				"hz":                              10,
				"instantaneous_input_kbps":        0,
				"instantaneous_ops_per_sec":       0,
				"instantaneous_output_kbps":       0,
				"io_threaded_reads_processed":     0,
				"io_threaded_writes_processed":    0,
				"io_threads_active":               0,
				"keyspace_hit_rate":               100000,
				"keyspace_hits":                   2,
				"keyspace_misses":                 0,
				"latest_fork_usec":                810,
				"lazyfree_pending_objects":        0,
				"loading":                         0,
				"lru_clock":                       13181377,
				"master_repl_offset":              0,
				"master_replid2":                  0,
				"maxmemory":                       0,
				"mem_aof_buffer":                  0,
				"mem_clients_normal":              0,
				"mem_clients_slaves":              0,
				"mem_fragmentation_bytes":         3185848,
				"mem_fragmentation_ratio":         4960,
				"mem_not_counted_for_evict":       0,
				"mem_replication_backlog":         0,
				"migrate_cached_sockets":          0,
				"module_fork_in_progress":         0,
				"module_fork_last_cow_size":       0,
				"number_of_cached_scripts":        0,
				"ping_latency_avg":                0,
				"ping_latency_count":              5,
				"ping_latency_max":                0,
				"ping_latency_min":                0,
				"ping_latency_sum":                0,
				"process_id":                      1,
				"pubsub_channels":                 0,
				"pubsub_patterns":                 0,
				"rdb_bgsave_in_progress":          0,
				"rdb_changes_since_last_save":     0,
				"rdb_current_bgsave_time_sec":     0,
				"rdb_last_bgsave_status":          0,
				"rdb_last_bgsave_time_sec":        0,
				"rdb_last_cow_size":               290816,
				"rdb_last_save_time":              56978305,
				"redis_git_dirty":                 0,
				"redis_git_sha1":                  0,
				"rejected_connections":            0,
				"repl_backlog_active":             0,
				"repl_backlog_first_byte_offset":  0,
				"repl_backlog_histlen":            0,
				"repl_backlog_size":               1048576,
				"rss_overhead_bytes":              266240,
				"rss_overhead_ratio":              1070,
				"second_repl_offset":              -1,
				"slave_expires_tracked_keys":      0,
				"sync_full":                       0,
				"sync_partial_err":                0,
				"sync_partial_ok":                 0,
				"tcp_port":                        6379,
				"total_commands_processed":        161,
				"total_connections_received":      87,
				"total_net_input_bytes":           2301,
				"total_net_output_bytes":          507187,
				"total_reads_processed":           250,
				"total_system_memory":             2084032512,
				"total_writes_processed":          163,
				"tracking_clients":                0,
				"tracking_total_items":            0,
				"tracking_total_keys":             0,
				"tracking_total_prefixes":         0,
				"unexpected_error_replies":        0,
				"uptime_in_days":                  2,
				"uptime_in_seconds":               252812,
				"used_cpu_sys":                    630829,
				"used_cpu_sys_children":           20,
				"used_cpu_user":                   188394,
				"used_cpu_user_children":          2,
				"used_memory":                     867160,
				"used_memory_dataset":             63816,
				"used_memory_lua":                 37888,
				"used_memory_overhead":            803344,
				"used_memory_peak":                923360,
				"used_memory_rss":                 3989504,
				"used_memory_scripts":             0,
				"used_memory_startup":             803152,
			},
		},
		"fails on error on Info": {
			prepare: prepareRedisErrorOnInfo,
		},
		"fails on response from not Redis instance": {
			prepare: prepareRedisWithPikaMetrics,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			rdb := test.prepare(t)

			ms := rdb.Collect()

			copyTimeRelatedMetrics(ms, test.wantCollected)

			assert.Equal(t, test.wantCollected, ms)
			if len(test.wantCollected) > 0 {
				ensureCollectedHasAllChartsDimsVarsIDs(t, rdb, ms)
				ensureCollectedCommandsAddedToCharts(t, rdb)
				ensureCollectedDbsAddedToCharts(t, rdb)
			}
		})
	}
}

func prepareRedisV609(t *testing.T) *Redis {
	rdb := New()
	require.NoError(t, rdb.Init())
	rdb.rdb = &mockRedisClient{
		result: dataVer609InfoAll,
	}
	return rdb
}

func prepareRedisErrorOnInfo(t *testing.T) *Redis {
	rdb := New()
	require.NoError(t, rdb.Init())
	rdb.rdb = &mockRedisClient{
		errOnInfo: true,
	}
	return rdb
}

func prepareRedisWithPikaMetrics(t *testing.T) *Redis {
	rdb := New()
	require.NoError(t, rdb.Init())
	rdb.rdb = &mockRedisClient{
		result: dataPikaInfoAll,
	}
	return rdb
}

func ensureCollectedHasAllChartsDimsVarsIDs(t *testing.T, rdb *Redis, ms map[string]int64) {
	for _, chart := range *rdb.Charts() {
		if chart.Obsolete {
			continue
		}
		for _, dim := range chart.Dims {
			_, ok := ms[dim.ID]
			assert.Truef(t, ok, "chart '%s' dim '%s': no dim in collected", dim.ID, chart.ID)
		}
		for _, v := range chart.Vars {
			_, ok := ms[v.ID]
			assert.Truef(t, ok, "chart '%s' dim '%s': no dim in collected", v.ID, chart.ID)
		}
	}
}

func ensureCollectedCommandsAddedToCharts(t *testing.T, rdb *Redis) {
	for _, id := range []string{
		chartCommandsCalls.ID,
		chartCommandsUsec.ID,
		chartCommandsUsecPerSec.ID,
	} {
		chart := rdb.Charts().Get(id)
		require.NotNilf(t, chart, "'%s' chart is not in charts", id)
		assert.Lenf(t, chart.Dims, len(rdb.collectedCommands),
			"'%s' chart unexpected number of dimensions", id)
	}
}

func ensureCollectedDbsAddedToCharts(t *testing.T, rdb *Redis) {
	for _, id := range []string{
		chartKeys.ID,
		chartExpiresKeys.ID,
	} {
		chart := rdb.Charts().Get(id)
		require.NotNilf(t, chart, "'%s' chart is not in charts", id)
		assert.Lenf(t, chart.Dims, len(rdb.collectedDbs),
			"'%s' chart unexpected number of dimensions", id)
	}
}

func copyTimeRelatedMetrics(dst, src map[string]int64) {
	for k, v := range src {
		switch {
		case k == "rdb_last_save_time",
			strings.HasPrefix(k, "ping_latency"):

			if _, ok := dst[k]; ok {
				dst[k] = v
			}
		}
	}
}

type mockRedisClient struct {
	errOnInfo   bool
	result      []byte
	calledClose bool
}

func (m *mockRedisClient) Info(_ context.Context, _ ...string) (cmd *redis.StringCmd) {
	if m.errOnInfo {
		cmd = redis.NewStringResult("", errors.New("error on Info"))
	} else {
		cmd = redis.NewStringResult(string(m.result), nil)
	}
	return cmd
}

func (m *mockRedisClient) Ping(_ context.Context) (cmd *redis.StatusCmd) {
	return redis.NewStatusResult("PONG", nil)
}

func (m *mockRedisClient) Close() error {
	m.calledClose = true
	return nil
}
