package haproxy

import (
	"crypto/rand"
	"encoding/binary"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/influxdata/telegraf/testutil"
)

func serverSocket(l net.Listener) {
	for {
		conn, err := l.Accept()
		if err != nil {
			return
		}

		go func(c net.Conn) {
			defer c.Close()

			buf := make([]byte, 1024)
			n, err := c.Read(buf)
			if err != nil {
				return
			}

			data := buf[:n]
			if string(data) == "show stat\n" {
				c.Write(csvOutputSample) //nolint:errcheck // we return anyway
			}
		}(conn)
	}
}

func TestHaproxyGeneratesMetricsWithAuthentication(t *testing.T) {
	// We create a fake server to return test data
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		username, password, ok := r.BasicAuth()
		if !ok {
			w.WriteHeader(http.StatusNotFound)
			if _, err := fmt.Fprint(w, "Unauthorized"); err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				t.Error(err)
				return
			}
			return
		}

		if username == "user" && password == "password" {
			if _, err := fmt.Fprint(w, string(csvOutputSample)); err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				t.Error(err)
				return
			}
		} else {
			w.WriteHeader(http.StatusNotFound)
			if _, err := fmt.Fprint(w, "Unauthorized"); err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				t.Error(err)
				return
			}
		}
	}))
	defer ts.Close()

	// Now we tested again above server, with our authentication data
	r := &HAProxy{
		Servers: []string{strings.Replace(ts.URL, "http://", "http://user:password@", 1)},
	}

	var acc testutil.Accumulator

	err := r.Gather(&acc)
	require.NoError(t, err)

	tags := map[string]string{
		"server": ts.Listener.Addr().String(),
		"proxy":  "git",
		"sv":     "www",
		"type":   "server",
	}

	fields := haproxyGetFieldValues()
	acc.AssertContainsTaggedFields(t, "haproxy", fields, tags)

	// Here, we should get error because we don't pass authentication data
	r = &HAProxy{
		Servers: []string{ts.URL},
	}

	require.NoError(t, r.Gather(&acc))
	require.NotEmpty(t, acc.Errors)
}

func TestHaproxyGeneratesMetricsWithoutAuthentication(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if _, err := fmt.Fprint(w, string(csvOutputSample)); err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			t.Error(err)
			return
		}
	}))
	defer ts.Close()

	r := &HAProxy{
		Servers: []string{ts.URL},
	}

	var acc testutil.Accumulator

	require.NoError(t, r.Gather(&acc))

	tags := map[string]string{
		"server": ts.Listener.Addr().String(),
		"proxy":  "git",
		"sv":     "www",
		"type":   "server",
	}

	fields := haproxyGetFieldValues()
	acc.AssertContainsTaggedFields(t, "haproxy", fields, tags)
}

func TestHaproxyGeneratesMetricsUsingSocket(t *testing.T) {
	var randomNumber int64
	var sockets [5]net.Listener

	// The Maximum length of the socket path is 104/108 characters, path created with t.TempDir() is too long for some cases
	// (it combines test name with subtest name and some random numbers in the path). Therefore, in this case, it is safer to stick with `os.MkdirTemp()`.
	//nolint:usetesting // Ignore "os.TempDir() could be replaced by t.TempDir() in TestHaproxyGeneratesMetricsUsingSocket" finding.
	tempDir := os.TempDir()
	_globmask := filepath.Join(tempDir, "test-haproxy*.sock")
	_badmask := filepath.Join(tempDir, "test-fail-haproxy*.sock")

	for i := 0; i < 5; i++ {
		require.NoError(t, binary.Read(rand.Reader, binary.LittleEndian, &randomNumber))
		sockname := filepath.Join(tempDir, fmt.Sprintf("test-haproxy%d.sock", randomNumber))

		sock, err := net.Listen("unix", sockname)
		require.NoError(t, err, "Cannot initialize socket")

		sockets[i] = sock
		defer sock.Close() //nolint:revive,gocritic // done on purpose, closing will be executed properly

		go serverSocket(sock)
	}

	r := &HAProxy{
		Servers: []string{_globmask},
	}

	var acc testutil.Accumulator

	err := r.Gather(&acc)
	require.NoError(t, err)

	fields := haproxyGetFieldValues()

	for _, sock := range sockets {
		tags := map[string]string{
			"server": getSocketAddr(sock.Addr().String()),
			"proxy":  "git",
			"sv":     "www",
			"type":   "server",
		}

		acc.AssertContainsTaggedFields(t, "haproxy", fields, tags)
	}

	// This mask should not match any socket
	r.Servers = []string{_badmask}

	require.NoError(t, r.Gather(&acc))
	require.NotEmpty(t, acc.Errors)
}

func TestHaproxyGeneratesMetricsUsingTcp(t *testing.T) {
	l, err := net.Listen("tcp", "localhost:8192")
	if err != nil {
		t.Fatal(err)
	}
	defer l.Close()

	go serverSocket(l)

	r := &HAProxy{
		Servers: []string{"tcp://" + l.Addr().String()},
	}

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

	fields := haproxyGetFieldValues()

	tags := map[string]string{
		"server": l.Addr().String(),
		"proxy":  "git",
		"sv":     "www",
		"type":   "server",
	}

	acc.AssertContainsTaggedFields(t, "haproxy", fields, tags)

	require.NoError(t, r.Gather(&acc))
}

// When not passing server config, we default to localhost
// We just want to make sure we did request stat from localhost
func TestHaproxyDefaultGetFromLocalhost(t *testing.T) {
	r := &HAProxy{}

	var acc testutil.Accumulator

	err := r.Gather(&acc)
	require.Error(t, err)
	require.Contains(t, err.Error(), "127.0.0.1:1936/haproxy?stats/;csv")
}

func TestHaproxyKeepFieldNames(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if _, err := fmt.Fprint(w, string(csvOutputSample)); err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			t.Error(err)
			return
		}
	}))
	defer ts.Close()

	r := &HAProxy{
		Servers:        []string{ts.URL},
		KeepFieldNames: true,
	}

	var acc testutil.Accumulator

	require.NoError(t, r.Gather(&acc))

	tags := map[string]string{
		"server": ts.Listener.Addr().String(),
		"pxname": "git",
		"svname": "www",
		"type":   "server",
	}

	fields := haproxyGetFieldValues()
	fields["act"] = fields["active_servers"]
	delete(fields, "active_servers")
	fields["bck"] = fields["backup_servers"]
	delete(fields, "backup_servers")
	fields["cli_abrt"] = fields["cli_abort"]
	delete(fields, "cli_abort")
	fields["srv_abrt"] = fields["srv_abort"]
	delete(fields, "srv_abort")
	fields["hrsp_1xx"] = fields["http_response.1xx"]
	delete(fields, "http_response.1xx")
	fields["hrsp_2xx"] = fields["http_response.2xx"]
	delete(fields, "http_response.2xx")
	fields["hrsp_3xx"] = fields["http_response.3xx"]
	delete(fields, "http_response.3xx")
	fields["hrsp_4xx"] = fields["http_response.4xx"]
	delete(fields, "http_response.4xx")
	fields["hrsp_5xx"] = fields["http_response.5xx"]
	delete(fields, "http_response.5xx")
	fields["hrsp_other"] = fields["http_response.other"]
	delete(fields, "http_response.other")

	acc.AssertContainsTaggedFields(t, "haproxy", fields, tags)
}

func mustReadSampleOutput() []byte {
	filePath := "testdata/sample_output.csv"
	data, err := os.ReadFile(filePath)
	if err != nil {
		panic(fmt.Errorf("could not read from file %s: %w", filePath, err))
	}

	return data
}

func haproxyGetFieldValues() map[string]interface{} {
	fields := map[string]interface{}{
		"active_servers":      uint64(1),
		"backup_servers":      uint64(0),
		"bin":                 uint64(5228218),
		"bout":                uint64(303747244),
		"check_code":          uint64(200),
		"check_duration":      uint64(3),
		"check_fall":          uint64(3),
		"check_health":        uint64(4),
		"check_rise":          uint64(2),
		"check_status":        "L7OK",
		"chkdown":             uint64(84),
		"chkfail":             uint64(559),
		"cli_abort":           uint64(690),
		"ctime":               uint64(1),
		"downtime":            uint64(3352),
		"dresp":               uint64(0),
		"econ":                uint64(0),
		"eresp":               uint64(21),
		"http_response.1xx":   uint64(0),
		"http_response.2xx":   uint64(5668),
		"http_response.3xx":   uint64(8710),
		"http_response.4xx":   uint64(140),
		"http_response.5xx":   uint64(0),
		"http_response.other": uint64(0),
		"iid":                 uint64(4),
		"last_chk":            "OK",
		"lastchg":             uint64(1036557),
		"lastsess":            int64(1342),
		"lbtot":               uint64(9481),
		"mode":                "http",
		"pid":                 uint64(1),
		"qcur":                uint64(0),
		"qmax":                uint64(0),
		"qtime":               uint64(1268),
		"rate":                uint64(0),
		"rate_max":            uint64(2),
		"rtime":               uint64(2908),
		"sid":                 uint64(1),
		"scur":                uint64(0),
		"slim":                uint64(2),
		"smax":                uint64(2),
		"srv_abort":           uint64(0),
		"status":              "UP",
		"stot":                uint64(14539),
		"ttime":               uint64(4500),
		"weight":              uint64(1),
		"wredis":              uint64(0),
		"wretr":               uint64(0),
	}
	return fields
}

// Can obtain from official haproxy demo: 'http://demo.haproxy.org/;csv'
var csvOutputSample = mustReadSampleOutput()
