package whois

import (
	// "errors"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/stretchr/testify/require"

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

// Make sure Whois implements telegraf.Input
var _ telegraf.Input = &Whois{}

func TestInit(t *testing.T) {
	// Setup the plugin
	plugin := &Whois{
		Domains: []string{"example.com", "google.com"},
		Server:  "whois.example.org",
		Timeout: config.Duration(5 * time.Second),
		Log:     testutil.Logger{},
	}

	// Test init
	require.NoError(t, plugin.Init())
}

func TestInitFail(t *testing.T) {
	tests := []struct {
		name     string
		domains  []string
		server   string
		timeout  config.Duration
		expected string
	}{
		{
			name:     "missing domains",
			timeout:  config.Duration(5 * time.Second),
			expected: "no domains configured",
		},
		{
			name:     "invalid timeout",
			domains:  []string{"example.com"},
			timeout:  config.Duration(0),
			expected: "timeout has to be greater than zero",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Setup the plugin
			plugin := &Whois{
				Domains: tt.domains,
				Server:  tt.server,
				Timeout: tt.timeout,
				Log:     testutil.Logger{},
			}
			// Test for the expected error message
			require.ErrorContains(t, plugin.Init(), tt.expected)
		})
	}
}

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

	// Prepare the influx parser for expectations
	parser := &influx.Parser{}
	require.NoError(t, parser.Init())

	for _, f := range folders {
		if !f.IsDir() {
			continue
		}

		testcasePath := filepath.Join("testcases", f.Name())
		configFilename := filepath.Join(testcasePath, "telegraf.conf")
		expectedFilename := filepath.Join(testcasePath, "expected.out")
		expectedErrorFilename := filepath.Join(testcasePath, "expected.err")

		// Compare options for metrics
		options := []cmp.Option{
			testutil.IgnoreTime(),
			testutil.SortMetrics(),
			// Ignore `expiry` due to possibility of fail on tests if CI is under high load
			testutil.IgnoreFields("expiry"),
		}

		t.Run(f.Name(), func(t *testing.T) {
			// Create and start a mock WHOIS server
			mockServer, err := createMockServer(testcasePath)
			require.NoError(t, err, "failed to create mock WHOIS server")

			mockServerAddr, err := mockServer.start()
			require.NoError(t, err, "failed to start mock WHOIS server")
			defer mockServer.stop() // Ensure cleanup

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

			// Read expected errors
			var expectedErrors []string
			if _, err := os.Stat(expectedErrorFilename); err == nil {
				expectedErrors, err = testutil.ParseLinesFromFile(expectedErrorFilename)
				require.NoError(t, err)
			}

			// Load Telegraf plugin config
			cfg := config.NewConfig()
			require.NoError(t, cfg.LoadConfig(configFilename))
			require.Len(t, cfg.Inputs, 1)

			// Get WHOIS plugin instance
			plugin := cfg.Inputs[0].Input.(*Whois)
			plugin.Server = mockServerAddr
			require.NoError(t, plugin.Init())

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

			actualErrorMsgs := make([]string, 0, len(acc.Errors))
			for _, err := range acc.Errors {
				actualErrorMsgs = append(actualErrorMsgs, err.Error())
			}
			require.ElementsMatch(t, actualErrorMsgs, expectedErrors)

			// Compare expected metrics
			actualMetrics := acc.GetTelegrafMetrics()
			testutil.RequireMetricsEqual(t, expectedMetrics, actualMetrics, options...)
		})
	}
}

type server struct {
	responses map[string][]byte
	listener  net.Listener
	wg        sync.WaitGroup

	errors []error
	sync.Mutex
}

func createMockServer(path string) (*server, error) {
	// Read the input data
	matches, err := filepath.Glob(filepath.Join(path, "input_*.txt"))
	if err != nil {
		return nil, fmt.Errorf("matching input files failed: %w", err)
	}

	responses := make(map[string][]byte, len(matches))
	for _, fn := range matches {
		buf, err := os.ReadFile(fn)
		if err != nil {
			return nil, fmt.Errorf("reading %q failed: %w", fn, err)
		}
		domain := strings.TrimPrefix(filepath.Base(fn), "input_")
		domain = strings.TrimSuffix(domain, ".txt")
		responses[domain] = buf
	}
	return &server{responses: responses}, nil
}

func (s *server) start() (string, error) {
	// Create the listener
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return "", fmt.Errorf("starting server failed: %w", err)
	}
	s.listener = listener

	addr := listener.Addr().String()
	s.wg.Add(1)
	go func() {
		defer s.wg.Done()
		for {
			conn, err := s.listener.Accept()
			if err != nil {
				return // Stop accepting new connections on shutdown
			}

			go func(c net.Conn) {
				defer c.Close()
				// Read the requested domain
				buf := make([]byte, 1024)
				n, err := c.Read(buf)
				if err != nil {
					return
				}
				domain := strings.TrimSpace(string(buf[:n]))

				// Write the response from the input data or an error if the domain cannot be found
				response, found := s.responses[domain]
				if !found {
					response = []byte("ERROR: No data available\n")
				}

				if _, err := c.Write(response); err != nil {
					s.Lock()
					s.errors = append(s.errors, fmt.Errorf("writing response %q failed: %w", domain, err))
					s.Unlock()
				}
			}(conn)
		}
	}()
	return addr, nil
}

func (s *server) stop() {
	if s.listener != nil {
		s.listener.Close()
		s.wg.Wait()
		s.listener = nil
	}
}
