package upsd

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/config"
	"github.com/influxdata/telegraf/plugins/inputs"
	"github.com/influxdata/telegraf/plugins/parsers/influx"
	"github.com/influxdata/telegraf/testutil"
)

func TestStringifyIDsWarning(t *testing.T) {
	tr := true
	fa := false
	tests := []struct {
		name        string
		value       *bool
		wantWarning bool
	}{
		{name: "unset emits warning", value: nil, wantWarning: true},
		{name: "explicit true is silent", value: &tr, wantWarning: false},
		{name: "explicit false is silent", value: &fa, wantWarning: false},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			logger := &testutil.CaptureLogger{}
			plugin := &Upsd{
				Server:       defaultAddress,
				Port:         defaultPort,
				StringifyIDs: tc.value,
				Log:          logger,
			}
			require.NoError(t, plugin.Init())

			warnings := logger.Warnings()
			if tc.wantWarning {
				require.Len(t, warnings, 1)
				require.Contains(t, warnings[0], "stringify_ids")
			} else {
				require.Empty(t, warnings)
			}
		})
	}
}

func TestBadServer(t *testing.T) {
	// Create and start a server without interactions
	server := &mockServer{}
	ctx, cancel := context.WithCancel(t.Context())
	addr, err := server.listen(ctx)
	require.NoError(t, err)
	defer cancel()

	// Setup the plugin
	plugin := &Upsd{
		Server: addr.IP.String(),
		Port:   addr.Port,
		Log:    testutil.Logger{},
	}
	require.NoError(t, plugin.Init())

	// Do the query
	var acc testutil.Accumulator
	require.Error(t, plugin.Gather(&acc))
}

func TestCases(t *testing.T) {
	// Get all directories in testdata
	folders, err := os.ReadDir("testcases")
	require.NoError(t, err)

	// Register the plugin
	inputs.Add("upsd", func() telegraf.Input {
		return &Upsd{}
	})

	for _, f := range folders {
		// Only handle folders
		if !f.IsDir() {
			continue
		}
		testcasePath := filepath.Join("testcases", f.Name())
		configFilename := filepath.Join(testcasePath, "telegraf.conf")
		expectedFilename := filepath.Join(testcasePath, "expected.out")

		t.Run(f.Name(), func(t *testing.T) {
			// Prepare the influx parser for expectations
			parser := &influx.Parser{}
			require.NoError(t, parser.Init())

			// Read the expected output if any
			var expected []telegraf.Metric
			if _, err := os.Stat(expectedFilename); err == nil {
				var err error
				expected, err = testutil.ParseMetricsFromFile(expectedFilename, parser)
				require.NoError(t, err)
			}

			// Setup a server from the input data
			server, err := setupServer(testcasePath)
			require.NoError(t, err)

			// Start the server
			ctx, cancel := context.WithCancel(t.Context())
			addr, err := server.listen(ctx)
			require.NoError(t, err)
			defer cancel()

			// Configure the plugin
			cfg := config.NewConfig()
			require.NoError(t, cfg.LoadConfig(configFilename))
			require.Len(t, cfg.Inputs, 1)
			plugin := cfg.Inputs[0].Input.(*Upsd)
			plugin.Server = addr.IP.String()
			plugin.Port = addr.Port
			require.NoError(t, plugin.Init())

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

			// Check the metric nevertheless as we might get some metrics despite errors.
			actual := acc.GetTelegrafMetrics()
			testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime())
			acc.Lock()
			defer acc.Unlock()
			require.Empty(t, acc.Errors)
		})
	}
}

type interaction struct {
	expected string
	response string
}

type variable struct {
	name  string
	value string
}

type mockServer struct {
	protocol []interaction
}

func (s *mockServer) init() {
	s.protocol = []interaction{
		{
			expected: "VER\n",
			response: "1\n",
		},
		{
			expected: "NETVER\n",
			response: "1\n",
		},
		{
			expected: "LIST UPS\n",
			response: "BEGIN LIST UPS\nUPS fake \"fake UPS\"\nEND LIST UPS\n",
		},
		{
			expected: "LIST CLIENT fake\n",
			response: "BEGIN LIST CLIENT fake\nCLIENT fake 127.0.0.1\nEND LIST CLIENT fake\n",
		},
		{
			expected: "LIST CMD fake\n",
			response: "BEGIN LIST CMD fake\nEND LIST CMD fake\n",
		},
		{
			expected: "GET UPSDESC fake\n",
			response: "UPSDESC fake \"stub-ups-description\"\n",
		},
		{
			expected: "GET NUMLOGINS fake\n",
			response: "NUMLOGINS fake 1\n",
		},
	}
}

func (s *mockServer) addVariables(variables []variable, types map[string]string) error {
	// Add a VAR entries for the variables
	values := make([]string, 0, len(variables))
	for _, v := range variables {
		values = append(values, fmt.Sprintf("VAR fake %s %q", v.name, v.value))
	}

	s.protocol = append(s.protocol, interaction{
		expected: "LIST VAR fake\n",
		response: "BEGIN LIST VAR fake\n" + strings.Join(values, "\n") + "\nEND LIST VAR fake\n",
	})

	// Add a description and type interaction for the variable
	for _, v := range variables {
		variableType, found := types[v.name]
		if !found {
			return fmt.Errorf("type for variable %q not found", v.name)
		}

		s.protocol = append(s.protocol,
			interaction{
				expected: "GET DESC fake " + v.name + "\n",
				response: "DESC fake" + v.name + " \"No description here\"\n",
			},
			interaction{
				expected: "GET TYPE fake " + v.name + "\n",
				response: "TYPE fake " + v.name + " " + variableType + "\n",
			},
		)
	}

	return nil
}

func (s *mockServer) listen(ctx context.Context) (*net.TCPAddr, error) {
	lc := net.ListenConfig{}
	ln, err := lc.Listen(ctx, "tcp4", "127.0.0.1:0")
	if err != nil {
		return nil, err
	}

	go func() {
		defer ln.Close()

		for ctx.Err() == nil {
			func() {
				conn, err := ln.Accept()
				if err != nil {
					return
				}
				defer conn.Close()
				err = conn.SetReadDeadline(time.Now().Add(time.Minute))
				if err != nil {
					return
				}

				in := make([]byte, 128)
				for _, interaction := range s.protocol {
					n, err := conn.Read(in)
					if err != nil {
						fmt.Printf("Failed to read from connection: %v\n", err)
						return
					}

					request := in[:n]
					if !bytes.Equal([]byte(interaction.expected), request) {
						fmt.Printf("Unexpected request %q, expected %q\n", string(request), interaction.expected)
						return
					}

					if _, err := conn.Write([]byte(interaction.response)); err != nil {
						fmt.Printf("Cannot write answer for request %q: %v\n", string(request), err)
						return
					}
				}

				// Append EOF to end of output bytes
				if _, err := conn.Write([]byte{0, 0}); err != nil {
					fmt.Printf("Cannot write EOF: %v\n", err)
					return
				}
			}()
		}
	}()

	return ln.Addr().(*net.TCPAddr), nil
}

func setupServer(path string) (*mockServer, error) {
	// Read the variables
	varbuf, err := os.ReadFile(filepath.Join(path, "variables.dev"))
	if err != nil {
		return nil, fmt.Errorf("reading variables failed: %w", err)
	}

	// Parse the information into variable names and values (upsc format)
	variables := make([]variable, 0)
	scanner := bufio.NewScanner(bytes.NewBuffer(varbuf))
	for scanner.Scan() {
		line := scanner.Text()
		parts := strings.SplitN(line, ":", 2)
		if len(parts) != 2 {
			return nil, fmt.Errorf("cannot parse line %s", line)
		}
		name := strings.TrimSpace(parts[0])
		value := strings.TrimSpace(parts[1])
		variables = append(variables, variable{name, value})
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("processing variables failed: %w", err)
	}

	// Read the variable-type mapping
	typebuf, err := os.ReadFile(filepath.Join(path, "types.dev"))
	if err != nil {
		return nil, fmt.Errorf("reading variables failed: %w", err)
	}

	// Parse the information into variable names and values (upsc format)
	types := make(map[string]string, 0)
	scanner = bufio.NewScanner(bytes.NewBuffer(typebuf))
	for scanner.Scan() {
		line := scanner.Text()
		parts := strings.SplitN(line, ":", 2)
		if len(parts) != 2 {
			return nil, fmt.Errorf("cannot parse line %s", line)
		}
		name := strings.TrimSpace(parts[0])
		vartype := strings.TrimSpace(parts[1])
		types[name] = vartype
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("processing variables failed: %w", err)
	}

	// Setup the server and add the device information
	server := &mockServer{}
	server.init()
	err = server.addVariables(variables, types)
	return server, err
}
