package clarify

import (
	"context"
	"encoding/json"
	"errors"
	"math"
	"slices"
	"testing"
	"time"

	"github.com/clarify/clarify-go"
	"github.com/clarify/clarify-go/jsonrpc"
	"github.com/clarify/clarify-go/views"
	"github.com/stretchr/testify/require"

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

var errTimeout = errors.New("timeout: operation timed out")

const validResponse = `{
	"signalsByInput" : {
		"test1.value" : {
			"id": "c8bvu9fqfsjctpv7b6fg",
			"created" : true
		}
	}
}`

type MockHandler struct {
	jsonResult string
	sleep      time.Duration
}

func (m *MockHandler) Do(ctx context.Context, _ jsonrpc.Request, result any) error {
	err := json.Unmarshal([]byte(m.jsonResult), result)
	if m.sleep > 0 {
		timer := time.NewTimer(m.sleep)
		select {
		case <-ctx.Done():
			timer.Stop()
			return errTimeout
		case <-timer.C:
			timer.Stop()
			return nil
		}
	}
	return err
}

func TestGenerateID(t *testing.T) {
	clfy := &Clarify{
		Log:          testutil.Logger{},
		IDTags:       []string{"tag1", "tag2"},
		ClarifyIDTag: "clarify_input_id",
	}
	var idTests = []struct {
		inMetric telegraf.Metric
		outID    []string
		err      error
	}{
		{
			metric.New(
				"cpu+='''..2!@#$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
				map[string]string{
					"tag1": "78sx",
				},
				map[string]interface{}{
					"time_idle": math.NaN(),
				},
				time.Now()),
			[]string{"cpu.time_idle.78sx"},
			errIDTooLong,
		},
		{
			metric.New(
				"cpu@@",
				map[string]string{
					"tag1": "78sx",
					"tag2": "33t2",
				},
				map[string]interface{}{
					"time_idle": math.NaN(),
				},
				time.Now()),
			[]string{"cpu__.time_idle.78sx.33t2"},
			nil,
		},
		{
			metric.New(
				"temperature",
				map[string]string{},
				map[string]interface{}{
					"cpu1": 12,
					"cpu2": 13,
				},
				time.Now()),
			[]string{"temperature.cpu1", "temperature.cpu2"},
			nil,
		},
		{
			metric.New(
				"legacy_measurement",
				map[string]string{
					"clarify_input_id": "e5e82f63-3700-4997-835d-eb366b7294a2",
					"xid":              "78sx",
				},
				map[string]interface{}{
					"value": 1337,
				},
				time.Now()),
			[]string{"e5e82f63-3700-4997-835d-eb366b7294a2"},
			nil,
		},
	}
	for _, tt := range idTests {
		for n, f := range tt.inMetric.FieldList() {
			id, err := clfy.generateID(tt.inMetric, f)
			if tt.err != nil {
				require.ErrorIs(t, err, tt.err)
			} else {
				require.NoError(t, err)
				require.True(t, slices.Contains(tt.outID, id), "\nexpected %+v\ngot %+v\n", tt.outID[n], id)
			}
		}
	}
}

func TestProcessMetrics(t *testing.T) {
	clfy := &Clarify{
		Log:          testutil.Logger{},
		IDTags:       []string{"tag1", "tag2", "node_id"},
		ClarifyIDTag: "clarify_input_id",
	}
	var idTests = []struct {
		inMetric   telegraf.Metric
		outFrame   views.DataFrame
		outSignals map[string]views.SignalSave
	}{
		{
			metric.New(
				"cpu1",
				map[string]string{
					"tag1": "78sx",
				},
				map[string]interface{}{
					"time_idle": 1337.3,
				},
				time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)),
			views.DataFrame{
				"cpu1.time_idle.78sx": views.DataSeries{
					1257894000000000: 1337.3,
				},
			},
			map[string]views.SignalSave{
				"cpu1.time_idle.78sx": {
					SignalSaveAttributes: views.SignalSaveAttributes{
						Name: "cpu1.time_idle",
						Labels: map[string][]string{
							"tag1": {"78sx"},
						},
					},
				},
			},
		},
		{
			metric.New(
				"cpu2",
				map[string]string{
					"tag1": "78sx",
					"tag2": "33t2",
				},
				map[string]interface{}{
					"time_idle": 200,
				},
				time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)),
			views.DataFrame{
				"cpu2.time_idle.78sx.33t2": views.DataSeries{
					1257894000000000: 200,
				},
			},
			map[string]views.SignalSave{
				"cpu2.time_idle.78sx.33t2": {
					SignalSaveAttributes: views.SignalSaveAttributes{
						Name: "cpu2.time_idle",
						Labels: map[string][]string{
							"tag1": {"78sx"},
							"tag2": {"33t2"},
						},
					},
				},
			},
		},
		{
			metric.New(
				"temperature",
				map[string]string{},
				map[string]interface{}{
					"cpu1": 12,
					"cpu2": 13,
				},
				time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)),
			views.DataFrame{
				"temperature.cpu1": views.DataSeries{
					1257894000000000: 12,
				},
				"temperature.cpu2": views.DataSeries{
					1257894000000000: 13,
				},
			},
			map[string]views.SignalSave{
				"temperature.cpu1": {
					SignalSaveAttributes: views.SignalSaveAttributes{
						Name: "temperature.cpu1",
					},
				},
				"temperature.cpu2": {
					SignalSaveAttributes: views.SignalSaveAttributes{
						Name: "temperature.cpu2",
					},
				},
			},
		},
		{
			metric.New(
				"legacy_measurement",
				map[string]string{
					"clarify_input_id": "e5e82f63-3700-4997-835d-eb366b7294a2",
					"xid":              "78sx",
				},
				map[string]interface{}{
					"value": 123.333,
				},
				time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)),
			views.DataFrame{
				"e5e82f63-3700-4997-835d-eb366b7294a2": views.DataSeries{
					1257894000000000: 123.333,
				},
			},
			map[string]views.SignalSave{
				"e5e82f63-3700-4997-835d-eb366b7294a2": {
					SignalSaveAttributes: views.SignalSaveAttributes{
						Name: "legacy_measurement.value",
						Labels: map[string][]string{
							"clarify-input-id": {"e5e82f63-3700-4997-835d-eb366b7294a2"},
							"xid":              {"78sx"},
						},
					},
				},
			},
		},
		{
			metric.New(
				"opc_metric",
				map[string]string{
					"node_id": "ns=1;s=Omron PLC.Objects.new_Controller_0.GlobalVars.counter1",
				},
				map[string]interface{}{
					"value":   12345.6789,
					"quality": "GOOD",
				},
				time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)),
			views.DataFrame{
				"opc_metric.value.ns_1_s_Omron_PLC.Objects.new_Controller_0.GlobalVars.counter1": views.DataSeries{
					1257894000000000: 12345.6789,
				},
			},
			map[string]views.SignalSave{
				"opc_metric.value.ns_1_s_Omron_PLC.Objects.new_Controller_0.GlobalVars.counter1": {
					SignalSaveAttributes: views.SignalSaveAttributes{
						Name: "opc_metric.value",
						Labels: map[string][]string{
							"node-id": {"ns=1;s=Omron PLC.Objects.new_Controller_0.GlobalVars.counter1"},
						},
					},
				},
			},
		},
	}
	for _, tt := range idTests {
		of, os := clfy.processMetrics([]telegraf.Metric{tt.inMetric})
		require.EqualValues(t, tt.outFrame, of)
		require.EqualValues(t, tt.outSignals, os)
	}
}

func TestTimeout(t *testing.T) {
	clfy := &Clarify{
		Log:     testutil.Logger{},
		Timeout: config.Duration(1 * time.Millisecond),
		client: clarify.NewClient("c8bvu9fqfsjctpv7b6fg", &MockHandler{
			sleep:      6 * time.Millisecond,
			jsonResult: validResponse,
		}),
	}

	err := clfy.Write(nil)
	require.ErrorIs(t, err, errTimeout)
}

func TestInit(t *testing.T) {
	username := config.NewSecret([]byte("user"))

	clfy := &Clarify{
		Log:     testutil.Logger{},
		Timeout: config.Duration(1 * time.Millisecond),
		client: clarify.NewClient("c8bvu9fqfsjctpv7b6fg", &MockHandler{
			sleep:      6 * time.Millisecond,
			jsonResult: validResponse,
		}),
		Username:        username,
		CredentialsFile: "file",
	}
	require.ErrorIs(t, clfy.Init(), errCredentials)
}
