package opcua_listener

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"testing"
	"time"

	gopcua "github.com/gopcua/opcua"
	"github.com/gopcua/opcua/ua"
	"github.com/stretchr/testify/require"
	"github.com/testcontainers/testcontainers-go/wait"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/config"
	"github.com/influxdata/telegraf/metric"
	"github.com/influxdata/telegraf/plugins/common/opcua"
	"github.com/influxdata/telegraf/plugins/common/opcua/input"
	"github.com/influxdata/telegraf/testutil"
)

const servicePort = "4840"

type opcTags struct {
	name           string
	namespace      string
	identifierType string
	identifier     string
	want           interface{}
}

func mapOPCTag(tags opcTags) (out input.NodeSettings) {
	out.FieldName = tags.name
	out.Namespace = tags.namespace
	out.IdentifierType = tags.identifierType
	out.Identifier = tags.identifier
	return out
}

func TestInitPluginWithBadConnectFailBehaviorValue(t *testing.T) {
	plugin := OpcUaListener{
		subscribeClientConfig: subscribeClientConfig{
			InputClientConfig: input.InputClientConfig{
				OpcUAClientConfig: opcua.OpcUAClientConfig{
					Endpoint:       "opc.tcp://notarealserver:4840",
					SecurityPolicy: "None",
					SecurityMode:   "None",
					ConnectTimeout: config.Duration(5 * time.Second),
					RequestTimeout: config.Duration(10 * time.Second),
				},
				MetricName: "opcua",
				Timestamp:  input.TimestampSourceTelegraf,
				RootNodes:  make([]input.NodeSettings, 0),
			},
			ConnectFailBehavior:  "notanoption",
			SubscriptionInterval: config.Duration(100 * time.Millisecond),
		},
		Log: testutil.Logger{},
	}
	err := plugin.Init()
	require.ErrorContains(t, err, "unknown setting \"notanoption\" for 'connect_fail_behavior'")
}

func TestStartPlugin(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	acc := &testutil.Accumulator{}

	plugin := OpcUaListener{
		subscribeClientConfig: subscribeClientConfig{
			InputClientConfig: input.InputClientConfig{
				OpcUAClientConfig: opcua.OpcUAClientConfig{
					Endpoint:       "opc.tcp://notarealserver:4840",
					SecurityPolicy: "None",
					SecurityMode:   "None",
					ConnectTimeout: config.Duration(5 * time.Second),
					RequestTimeout: config.Duration(10 * time.Second),
				},
				MetricName: "opcua",
				Timestamp:  input.TimestampSourceTelegraf,
				RootNodes:  make([]input.NodeSettings, 0),
			},
			SubscriptionInterval: config.Duration(100 * time.Millisecond),
		},
		Log: testutil.Logger{},
	}
	testopctags := []opcTags{
		{"ProductName", "0", "i", "2261", "open62541 OPC UA Server"},
	}
	for _, tags := range testopctags {
		plugin.subscribeClientConfig.RootNodes = append(plugin.subscribeClientConfig.RootNodes, mapOPCTag(tags))
	}
	require.NoError(t, plugin.Init())
	err := plugin.Start(acc)
	require.ErrorContains(t, err, "could not resolve address")

	plugin.subscribeClientConfig.ConnectFailBehavior = "ignore"
	require.NoError(t, plugin.Init())
	require.NoError(t, plugin.Start(acc))
	require.Equal(t, opcua.Disconnected, plugin.client.OpcUAClient.State())
	plugin.Stop()

	container := testutil.Container{
		Image:        "open62541/open62541",
		ExposedPorts: []string{servicePort},
		WaitingFor: wait.ForAll(
			wait.ForListeningPort(servicePort),
			wait.ForLog("TCP network layer listening on opc.tcp://"),
		),
	}
	plugin.subscribeClientConfig.ConnectFailBehavior = "retry"
	require.NoError(t, plugin.Init())
	require.NoError(t, plugin.Start(acc))
	require.Equal(t, opcua.Disconnected, plugin.client.OpcUAClient.State())

	err = container.Start()
	require.NoError(t, err, "failed to start container")

	defer container.Terminate()
	newEndpoint := fmt.Sprintf("opc.tcp://%s:%s", container.Address, container.Ports[servicePort])
	plugin.client.Config.Endpoint = newEndpoint
	plugin.client.OpcUAClient.Config.Endpoint = newEndpoint
	err = plugin.Gather(acc)
	require.NoError(t, err)
	require.Equal(t, opcua.Connected, plugin.client.OpcUAClient.State())
}

func TestSubscribeClientIntegration(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	container := testutil.Container{
		Image:        "open62541/open62541",
		ExposedPorts: []string{servicePort},
		WaitingFor: wait.ForAll(
			wait.ForListeningPort(servicePort),
			wait.ForLog("TCP network layer listening on opc.tcp://"),
		),
	}
	err := container.Start()
	require.NoError(t, err, "failed to start container")
	defer container.Terminate()

	testopctags := []opcTags{
		{"ProductName", "0", "i", "2261", "open62541 OPC UA Server"},
		{"ProductUri", "0", "i", "2262", "http://open62541.org"},
		{"ManufacturerName", "0", "i", "2263", "open62541"},
		{"badnode", "1", "i", "1337", nil},
		{"goodnode", "1", "s", "the.answer", int32(42)},
		{"DateTime", "1", "i", "51037", "0001-01-01T00:00:00Z"},
	}
	tagsRemaining := make([]string, 0, len(testopctags))
	for i, tag := range testopctags {
		if tag.want != nil {
			tagsRemaining = append(tagsRemaining, testopctags[i].name)
		}
	}

	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       fmt.Sprintf("opc.tcp://%s:%s", container.Address, container.Ports[servicePort]),
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName: "testing",
			RootNodes:  make([]input.NodeSettings, 0),
			Groups:     make([]input.NodeGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	for _, tags := range testopctags {
		subscribeConfig.RootNodes = append(subscribeConfig.RootNodes, mapOPCTag(tags))
	}
	o, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.NoError(t, err)

	// give initial setup a couple extra attempts, as on CircleCI this can be
	// attempted to soon
	require.Eventually(t, func() bool {
		return o.SetupOptions() == nil
	}, 5*time.Second, 10*time.Millisecond)

	err = o.connect()
	require.NoError(t, err, "Connection failed")

	ctx, cancel := context.WithTimeout(t.Context(), time.Second*10)
	defer cancel()
	res, err := o.startMonitoring(ctx)
	require.Equal(t, opcua.Connected, o.State())
	require.NoError(t, err)

	for {
		select {
		case m := <-res:
			for fieldName, fieldValue := range m.Fields() {
				for _, tag := range testopctags {
					if fieldName != tag.name {
						continue
					}

					if tag.want == nil {
						t.Errorf("Tag: %s has value: %v", tag.name, fieldValue)
						return
					}

					require.Equal(t, tag.want, fieldValue)

					newRemaining := make([]string, 0, len(tagsRemaining))
					for _, remainingTag := range tagsRemaining {
						if fieldName != remainingTag {
							newRemaining = append(newRemaining, remainingTag)
							break
						}
					}

					if len(newRemaining) == 0 {
						return
					}

					tagsRemaining = newRemaining
				}
			}

		case <-ctx.Done():
			var sb strings.Builder
			for _, tag := range tagsRemaining {
				sb.WriteString(tag)
				sb.WriteString(", ")
			}
			t.Errorf("Tags %s are remaining without a received value", sb.String())
			return
		}
	}
}

func TestSubscribeClientIntegrationAdditionalFields(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	container := testutil.Container{
		Image:        "open62541/open62541",
		ExposedPorts: []string{servicePort},
		WaitingFor: wait.ForAll(
			wait.ForListeningPort(servicePort),
			wait.ForLog("TCP network layer listening on opc.tcp://"),
		),
	}
	require.NoError(t, container.Start(), "failed to start container")
	defer container.Terminate()

	testopctags := []opcTags{
		{"ProductName", "0", "i", "2261", "open62541 OPC UA Server"},
		{"ProductUri", "0", "i", "2262", "http://open62541.org"},
		{"ManufacturerName", "0", "i", "2263", "open62541"},
		{"badnode", "1", "i", "1337", nil},
		{"goodnode", "1", "s", "the.answer", int32(42)},
		{"DateTime", "1", "i", "51037", "0001-01-01T00:00:00Z"},
	}
	testopctypes := []string{
		"String",
		"String",
		"String",
		"Null",
		"Int32",
		"DateTime",
	}
	testopcquality := []string{
		"The operation succeeded. StatusGood (0x0)",
		"The operation succeeded. StatusGood (0x0)",
		"The operation succeeded. StatusGood (0x0)",
		"User does not have permission to perform the requested operation. StatusBadUserAccessDenied (0x801F0000)",
		"The operation succeeded. StatusGood (0x0)",
		"The operation succeeded. StatusGood (0x0)",
	}
	expectedopcmetrics := make([]telegraf.Metric, 0, len(testopctags))
	for i, x := range testopctags {
		now := time.Now()
		tags := map[string]string{
			"id": fmt.Sprintf("ns=%s;%s=%s", x.namespace, x.identifierType, x.identifier),
		}
		fields := map[string]interface{}{
			x.name:     x.want,
			"Quality":  testopcquality[i],
			"DataType": testopctypes[i],
		}
		expectedopcmetrics = append(expectedopcmetrics, metric.New("testing", tags, fields, now))
	}

	tagsRemaining := make([]string, 0, len(testopctags))
	for i, tag := range testopctags {
		if tag.want != nil {
			tagsRemaining = append(tagsRemaining, testopctags[i].name)
		}
	}

	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       fmt.Sprintf("opc.tcp://%s:%s", container.Address, container.Ports[servicePort]),
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
				OptionalFields: []string{"DataType"},
			},
			MetricName: "testing",
			RootNodes:  make([]input.NodeSettings, 0),
			Groups:     make([]input.NodeGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	for _, tags := range testopctags {
		subscribeConfig.RootNodes = append(subscribeConfig.RootNodes, mapOPCTag(tags))
	}
	o, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.NoError(t, err)

	// give initial setup a couple extra attempts, as on CircleCI this can be
	// attempted to soon
	require.Eventually(t, func() bool {
		return o.SetupOptions() == nil
	}, 5*time.Second, 10*time.Millisecond)

	require.NoError(t, o.connect(), "Connection failed")

	ctx, cancel := context.WithTimeout(t.Context(), time.Second*10)
	defer cancel()
	res, err := o.startMonitoring(ctx)
	require.NoError(t, err)

	for {
		select {
		case m := <-res:
			for fieldName, fieldValue := range m.Fields() {
				for _, tag := range testopctags {
					if fieldName != tag.name {
						continue
					}
					// nil-value tags should not be sent from server, error if one does
					if tag.want == nil {
						t.Errorf("Tag: %s has value: %v", tag.name, fieldValue)
						return
					}

					newRemaining := make([]string, 0, len(tagsRemaining))
					for _, remainingTag := range tagsRemaining {
						if fieldName != remainingTag {
							newRemaining = append(newRemaining, remainingTag)
							break
						}
					}

					if len(newRemaining) == 0 {
						return
					}
					// Test if the received metric matches one of the expected
					testutil.RequireMetricsSubset(t, []telegraf.Metric{m}, expectedopcmetrics, testutil.IgnoreTime())
					tagsRemaining = newRemaining
				}
			}

		case <-ctx.Done():
			var sb strings.Builder
			for _, tag := range tagsRemaining {
				sb.WriteString(tag)
				sb.WriteString(", ")
			}
			t.Errorf("Tags %s are remaining without a received value", sb.String())
			return
		}
	}
}

func TestSkipFailedMonitoredItemsIntegration(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	container := testutil.Container{
		Image:        "open62541/open62541",
		ExposedPorts: []string{servicePort},
		WaitingFor: wait.ForAll(
			wait.ForListeningPort(servicePort),
			wait.ForLog("TCP network layer listening on opc.tcp://"),
		),
	}
	require.NoError(t, container.Start(), "failed to start container")
	defer container.Terminate()

	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       fmt.Sprintf("opc.tcp://%s:%s", container.Address, container.Ports[servicePort]),
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
			},
			MetricName: "testing",
			RootNodes: []input.NodeSettings{
				{FieldName: "ProductName", Namespace: "0", IdentifierType: "i", Identifier: "2261"},
				{FieldName: "NonExistent", Namespace: "99", IdentifierType: "i", Identifier: "99999"},
			},
		},
	}

	o, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.NoError(t, err)

	require.Eventually(t, func() bool {
		return o.SetupOptions() == nil
	}, 5*time.Second, 10*time.Millisecond)

	require.NoError(t, o.connect())

	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
	defer cancel()
	res, err := o.startMonitoring(ctx)
	require.NoError(t, err)

	select {
	case m := <-res:
		require.Contains(t, m.Fields(), "ProductName")
	case <-ctx.Done():
		t.Fatal("Timed out waiting for metric from valid node")
	}
}

func TestSubscribeClientConfig(t *testing.T) {
	toml := `
[[inputs.opcua_listener]]
name = "localhost"
endpoint = "opc.tcp://localhost:4840"
connect_timeout = "10s"
request_timeout = "5s"
subscription_interval = "200ms"
connect_fail_behavior = "error"
security_policy = "auto"
security_mode = "auto"
certificate = "/etc/telegraf/cert.pem"
private_key = "/etc/telegraf/key.pem"
auth_method = "Anonymous"
timestamp_format = "2006-01-02T15:04:05Z07:00"
username = ""
password = ""

optional_fields = ["DataType"]

nodes = [
  {name="name",  namespace="1", identifier_type="s", identifier="one"},
  {name="name2", namespace="2", identifier_type="s", identifier="two"},
]

[[inputs.opcua_listener.group]]
name = "foo"
namespace = "3"
identifier_type = "i"
default_tags = {tag1="val1", tag2="val2"}
[[inputs.opcua_listener.group.nodes]]
  name = "name3"
  identifier = "3000"
  default_tags = {tag3="val3"}

[[inputs.opcua_listener.group]]
name = "bar"
namespace = "0"
identifier_type = "i"
default_tags = {tag1="val1", tag2="val2"}
[[inputs.opcua_listener.group.nodes]]
  name = "name4"
  identifier = "4000"
  default_tags = {tag1="override"}

[inputs.opcua_listener.workarounds]
additional_valid_status_codes = ["0xC0"]
`

	c := config.NewConfig()
	err := c.LoadConfigData([]byte(toml), config.EmptySourcePath)
	require.NoError(t, err)

	require.Len(t, c.Inputs, 1)

	o, ok := c.Inputs[0].Input.(*OpcUaListener)
	require.True(t, ok)

	require.Equal(t, "localhost", o.subscribeClientConfig.MetricName)
	require.Equal(t, "opc.tcp://localhost:4840", o.subscribeClientConfig.Endpoint)
	require.Equal(t, config.Duration(10*time.Second), o.subscribeClientConfig.ConnectTimeout)
	require.Equal(t, config.Duration(5*time.Second), o.subscribeClientConfig.RequestTimeout)
	require.Equal(t, config.Duration(200*time.Millisecond), o.subscribeClientConfig.SubscriptionInterval)
	require.Equal(t, "error", o.subscribeClientConfig.ConnectFailBehavior)
	require.Equal(t, "auto", o.subscribeClientConfig.SecurityPolicy)
	require.Equal(t, "auto", o.subscribeClientConfig.SecurityMode)
	require.Equal(t, "/etc/telegraf/cert.pem", o.subscribeClientConfig.Certificate)
	require.Equal(t, "/etc/telegraf/key.pem", o.subscribeClientConfig.PrivateKey)
	require.Equal(t, "Anonymous", o.subscribeClientConfig.AuthMethod)
	require.True(t, o.subscribeClientConfig.Username.Empty())
	require.True(t, o.subscribeClientConfig.Password.Empty())
	require.Equal(t, []input.NodeSettings{
		{
			FieldName:      "name",
			Namespace:      "1",
			IdentifierType: "s",
			Identifier:     "one",
		},
		{
			FieldName:      "name2",
			Namespace:      "2",
			IdentifierType: "s",
			Identifier:     "two",
		},
	}, o.subscribeClientConfig.RootNodes)
	require.Equal(t, []input.NodeGroupSettings{
		{
			MetricName:     "foo",
			Namespace:      "3",
			IdentifierType: "i",
			DefaultTags:    map[string]string{"tag1": "val1", "tag2": "val2"},
			Nodes: []input.NodeSettings{{
				FieldName:   "name3",
				Identifier:  "3000",
				DefaultTags: map[string]string{"tag3": "val3"},
			}},
		},
		{
			MetricName:     "bar",
			Namespace:      "0",
			IdentifierType: "i",
			DefaultTags:    map[string]string{"tag1": "val1", "tag2": "val2"},
			Nodes: []input.NodeSettings{{
				FieldName:   "name4",
				Identifier:  "4000",
				DefaultTags: map[string]string{"tag1": "override"},
			}},
		},
	}, o.subscribeClientConfig.Groups)
	require.Equal(t, opcua.OpcUAWorkarounds{AdditionalValidStatusCodes: []string{"0xC0"}}, o.subscribeClientConfig.Workarounds)
	require.Equal(t, []string{"DataType"}, o.subscribeClientConfig.OptionalFields)
}

func TestSubscribeClientConfigWithMonitoringParams(t *testing.T) {
	toml := `
[[inputs.opcua_listener]]
name = "localhost"
endpoint = "opc.tcp://localhost:4840"
subscription_interval = "200ms"

[[inputs.opcua_listener.group]]
name = "foo"
namespace = "3"
identifier_type = "i"
default_tags = {tag1="val1", tag2="val2"}
[[inputs.opcua_listener.group.nodes]]
  name = "name3"
  identifier = "3000"
  default_tags = {tag3="val3"}

[inputs.opcua_listener.group.nodes.monitoring_params]
sampling_interval = "50ms"
queue_size = 10
discard_oldest = true

[inputs.opcua_listener.group.nodes.monitoring_params.data_change_filter]
trigger = "StatusValue"
deadband_type = "Absolute"
deadband_value = 100.0
`

	c := config.NewConfig()
	err := c.LoadConfigData([]byte(toml), config.EmptySourcePath)
	require.NoError(t, err)

	require.Len(t, c.Inputs, 1)

	o, ok := c.Inputs[0].Input.(*OpcUaListener)
	require.True(t, ok)

	queueSize := uint32(10)
	discardOldest := true
	deadbandValue := 100.0
	require.Equal(t, []input.NodeGroupSettings{
		{
			MetricName:     "foo",
			Namespace:      "3",
			IdentifierType: "i",
			DefaultTags:    map[string]string{"tag1": "val1", "tag2": "val2"},
			Nodes: []input.NodeSettings{{
				FieldName:   "name3",
				Identifier:  "3000",
				DefaultTags: map[string]string{"tag3": "val3"},
				MonitoringParams: input.MonitoringParameters{
					SamplingInterval: 50000000,
					QueueSize:        &queueSize,
					DiscardOldest:    &discardOldest,
					DataChangeFilter: &input.DataChangeFilter{
						Trigger:       "StatusValue",
						DeadbandType:  "Absolute",
						DeadbandValue: &deadbandValue,
					},
				},
			}},
		},
	}, o.subscribeClientConfig.Groups)
}

func TestSubscribeClientBrowseDiscoveryIntegration(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	container := testutil.Container{
		Image:        "open62541/open62541",
		ExposedPorts: []string{servicePort},
		WaitingFor: wait.ForAll(
			wait.ForListeningPort(servicePort),
			wait.ForLog("TCP network layer listening on opc.tcp://"),
		),
	}
	require.NoError(t, container.Start(), "failed to start container")
	defer container.Terminate()

	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       fmt.Sprintf("opc.tcp://%s:%s", container.Address, container.Ports[servicePort]),
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
			},
			MetricName: "browse_listener",
			Browse: input.BrowseConfig{
				Depth: 5,
				Paths: []input.BrowsePathSettings{
					{Pattern: "Server/**", MetricName: "server_vars"},
				},
			},
		},
		SubscriptionInterval: config.Duration(100 * time.Millisecond),
	}

	client, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.NoError(t, err)

	require.NoError(t, client.connect())
	require.NotEmpty(t, client.NodeMetricMapping, "browse should discover at least one variable under Server/**")

	// Reconnect re-runs discovery against an unchanged server; mapping size
	// must stay bounded rather than grow with each reconnect.
	mappingSize := len(client.NodeMetricMapping)
	require.NoError(t, client.Disconnect(t.Context()))
	require.NoError(t, client.connect())
	require.Len(t, client.NodeMetricMapping, mappingSize, "rediscovery must not grow the mapping")
}

func TestSubscribeClientConfigInvalidTrigger(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://localhost:4840",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName: "testing",
			RootNodes:  make([]input.NodeSettings, 0),
			Groups:     make([]input.NodeGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.RootNodes = append(subscribeConfig.RootNodes, input.NodeSettings{
		FieldName:      "foo",
		Namespace:      "3",
		Identifier:     "1",
		IdentifierType: "i",
		MonitoringParams: input.MonitoringParameters{
			DataChangeFilter: &input.DataChangeFilter{
				Trigger: "not_valid",
			},
		},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "node 'ns=3;i=1': trigger 'not_valid' not supported")
}

func TestSubscribeClientConfigMissingTrigger(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://localhost:4840",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName: "testing",
			RootNodes:  make([]input.NodeSettings, 0),
			Groups:     make([]input.NodeGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.RootNodes = append(subscribeConfig.RootNodes, input.NodeSettings{
		FieldName:      "foo",
		Namespace:      "3",
		Identifier:     "1",
		IdentifierType: "i",
		MonitoringParams: input.MonitoringParameters{
			DataChangeFilter: &input.DataChangeFilter{
				DeadbandType: "Absolute",
			},
		},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "node 'ns=3;i=1': trigger '' not supported")
}

func TestSubscribeClientConfigInvalidDeadbandType(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://localhost:4840",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName: "testing",
			RootNodes:  make([]input.NodeSettings, 0),
			Groups:     make([]input.NodeGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.RootNodes = append(subscribeConfig.RootNodes, input.NodeSettings{
		FieldName:      "foo",
		Namespace:      "3",
		Identifier:     "1",
		IdentifierType: "i",
		MonitoringParams: input.MonitoringParameters{
			DataChangeFilter: &input.DataChangeFilter{
				Trigger:      "Status",
				DeadbandType: "not_valid",
			},
		},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "node 'ns=3;i=1': deadband_type 'not_valid' not supported")
}

func TestSubscribeClientConfigMissingDeadbandType(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://localhost:4840",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName: "testing",
			RootNodes:  make([]input.NodeSettings, 0),
			Groups:     make([]input.NodeGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.RootNodes = append(subscribeConfig.RootNodes, input.NodeSettings{
		FieldName:      "foo",
		Namespace:      "3",
		Identifier:     "1",
		IdentifierType: "i",
		MonitoringParams: input.MonitoringParameters{
			DataChangeFilter: &input.DataChangeFilter{
				Trigger: "Status",
			},
		},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "node 'ns=3;i=1': deadband_type '' not supported")
}

func TestSubscribeClientConfigInvalidDeadbandValue(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://localhost:4840",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName: "testing",
			RootNodes:  make([]input.NodeSettings, 0),
			Groups:     make([]input.NodeGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	deadbandValue := -1.0
	subscribeConfig.RootNodes = append(subscribeConfig.RootNodes, input.NodeSettings{
		FieldName:      "foo",
		Namespace:      "3",
		Identifier:     "1",
		IdentifierType: "i",
		MonitoringParams: input.MonitoringParameters{
			DataChangeFilter: &input.DataChangeFilter{
				Trigger:       "Status",
				DeadbandType:  "Absolute",
				DeadbandValue: &deadbandValue,
			},
		},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "node 'ns=3;i=1': negative deadband_value not supported")
}

func TestSubscribeClientConfigMissingDeadbandValue(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://localhost:4840",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName: "testing",
			RootNodes:  make([]input.NodeSettings, 0),
			Groups:     make([]input.NodeGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.RootNodes = append(subscribeConfig.RootNodes, input.NodeSettings{
		FieldName:      "foo",
		Namespace:      "3",
		Identifier:     "1",
		IdentifierType: "i",
		MonitoringParams: input.MonitoringParameters{
			DataChangeFilter: &input.DataChangeFilter{
				Trigger:      "Status",
				DeadbandType: "Absolute",
			},
		},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "node 'ns=3;i=1': deadband_value was not set")
}

func TestSubscribeClientConfigValidMonitoringParams(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://localhost:4840",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName: "testing",
			RootNodes:  make([]input.NodeSettings, 0),
			Groups:     make([]input.NodeGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}

	var queueSize uint32 = 10
	discardOldest := true
	deadbandValue := 10.0
	subscribeConfig.RootNodes = append(subscribeConfig.RootNodes, input.NodeSettings{
		FieldName:      "foo",
		Namespace:      "3",
		Identifier:     "1",
		IdentifierType: "i",
		MonitoringParams: input.MonitoringParameters{
			SamplingInterval: 50000000,
			QueueSize:        &queueSize,
			DiscardOldest:    &discardOldest,
			DataChangeFilter: &input.DataChangeFilter{
				Trigger:       "Status",
				DeadbandType:  "Absolute",
				DeadbandValue: &deadbandValue,
			},
		},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.NoError(t, err)

	// Verify assignConfigValuesToRequest correctly translates monitoring params
	nodeID, err := ua.ParseNodeID("ns=3;i=1")
	require.NoError(t, err)
	req := gopcua.NewMonitoredItemCreateRequestWithDefaults(nodeID, ua.AttributeIDValue, 0)
	require.NoError(t, assignConfigValuesToRequest(req, &subscribeConfig.RootNodes[0].MonitoringParams))
	require.Equal(t, &ua.MonitoringParameters{
		SamplingInterval: 50,
		QueueSize:        queueSize,
		DiscardOldest:    discardOldest,
		Filter: ua.NewExtensionObject(
			&ua.DataChangeFilter{
				Trigger:       ua.DataChangeTriggerStatus,
				DeadbandType:  uint32(ua.DeadbandTypeAbsolute),
				DeadbandValue: deadbandValue,
			},
		),
	}, req.RequestedParameters)
}

func TestSubscribeClientConfigValidMonitoringParamsNoDeadband(t *testing.T) {
	var queueSize uint32 = 10
	discardOldest := true
	monParams := input.MonitoringParameters{
		SamplingInterval: 50000000,
		QueueSize:        &queueSize,
		DiscardOldest:    &discardOldest,
		DataChangeFilter: &input.DataChangeFilter{
			Trigger:      "Status",
			DeadbandType: "None",
		},
	}

	nodeID, err := ua.ParseNodeID("ns=3;i=1")
	require.NoError(t, err)
	req := gopcua.NewMonitoredItemCreateRequestWithDefaults(nodeID, ua.AttributeIDValue, 0)
	require.NoError(t, assignConfigValuesToRequest(req, &monParams))
	require.Equal(t, &ua.MonitoringParameters{
		SamplingInterval: 50,
		QueueSize:        queueSize,
		DiscardOldest:    discardOldest,
		Filter: ua.NewExtensionObject(
			&ua.DataChangeFilter{
				Trigger:       ua.DataChangeTriggerStatus,
				DeadbandType:  uint32(ua.DeadbandTypeNone),
				DeadbandValue: 0,
			},
		),
	}, req.RequestedParameters)
}

func TestSubscribeClientConfigValidMonitoringAndEventParams(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://localhost:4840",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName:  "testing",
			RootNodes:   make([]input.NodeSettings, 0),
			Groups:      make([]input.NodeGroupSettings, 0),
			EventGroups: make([]input.EventGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}

	var queueSize uint32 = 10
	discardOldest := true
	deadbandValue := 10.0
	subscribeConfig.RootNodes = append(subscribeConfig.RootNodes, input.NodeSettings{
		FieldName:      "foo",
		Namespace:      "3",
		Identifier:     "1",
		IdentifierType: "i",
		MonitoringParams: input.MonitoringParameters{
			SamplingInterval: 50000000,
			QueueSize:        &queueSize,
			DiscardOldest:    &discardOldest,
			DataChangeFilter: &input.DataChangeFilter{
				Trigger:       "Status",
				DeadbandType:  "Absolute",
				DeadbandValue: &deadbandValue,
			},
		},
	})
	subscribeConfig.EventGroups = append(subscribeConfig.EventGroups, input.EventGroupSettings{
		SamplingInterval: 1.0,
		EventTypeNode: input.EventNodeSettings{
			Namespace:      "3",
			IdentifierType: "i",
			Identifier:     "1234",
		},
		Namespace:      "3",
		IdentifierType: "i",
		NodeIDSettings: []input.EventNodeSettings{
			{
				Namespace:      "3",
				IdentifierType: "i",
				Identifier:     "12",
			},
			{
				Namespace:      "3",
				IdentifierType: "i",
				Identifier:     "13",
			},
		},
		SourceNames: []string{"SensorXYZ"},
		Fields:      []string{"PressureValue"},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.NoError(t, err)

	// Verify assignConfigValuesToRequest correctly translates monitoring params
	nodeID, err := ua.ParseNodeID("ns=3;i=1")
	require.NoError(t, err)
	req := gopcua.NewMonitoredItemCreateRequestWithDefaults(nodeID, ua.AttributeIDValue, 0)
	require.NoError(t, assignConfigValuesToRequest(req, &subscribeConfig.RootNodes[0].MonitoringParams))
	require.Equal(t, &ua.MonitoringParameters{
		SamplingInterval: 50,
		QueueSize:        queueSize,
		DiscardOldest:    discardOldest,
		Filter: ua.NewExtensionObject(
			&ua.DataChangeFilter{
				Trigger:       ua.DataChangeTriggerStatus,
				DeadbandType:  uint32(ua.DeadbandTypeAbsolute),
				DeadbandValue: deadbandValue,
			},
		),
	}, req.RequestedParameters)
}

func TestSubscribeClientConfigValidEventStreamingParams(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName:  "testing",
			EventGroups: make([]input.EventGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.EventGroups = append(subscribeConfig.EventGroups, input.EventGroupSettings{
		SamplingInterval: 1.0,
		EventTypeNode: input.EventNodeSettings{
			Namespace:      "3",
			IdentifierType: "i",
			Identifier:     "1234",
		},
		Namespace:      "3",
		IdentifierType: "i",
		NodeIDSettings: []input.EventNodeSettings{
			{
				Namespace:      "3",
				IdentifierType: "i",
				Identifier:     "12",
			},
			{
				Namespace:      "3",
				IdentifierType: "i",
				Identifier:     "13",
			},
		},
		SourceNames: []string{"SensorXYZ"},
		Fields:      []string{"PressureValue"},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.NoError(t, err)
}

func TestSubscribeClientConfigEventInputMissingSamplingInterval(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName:  "testing",
			EventGroups: make([]input.EventGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.EventGroups = append(subscribeConfig.EventGroups, input.EventGroupSettings{
		EventTypeNode: input.EventNodeSettings{
			Namespace:      "3",
			IdentifierType: "i",
			Identifier:     "1234",
		},
		Namespace:      "3",
		IdentifierType: "i",
		NodeIDSettings: []input.EventNodeSettings{
			{
				Namespace:      "3",
				IdentifierType: "i",
				Identifier:     "12",
			},
		},
		SourceNames: []string{"SensorXYZ"},
		Fields:      []string{"PressureValue"},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.NoError(t, err)
}

func TestSubscribeClientConfigEventInputMissingEventType(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName:  "testing",
			EventGroups: make([]input.EventGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.EventGroups = append(subscribeConfig.EventGroups, input.EventGroupSettings{
		SamplingInterval: 1.0,
		Namespace:        "3",
		IdentifierType:   "i",
		NodeIDSettings: []input.EventNodeSettings{
			{
				Namespace:      "3",
				IdentifierType: "i",
				Identifier:     "12",
			},
		},
		SourceNames: []string{"SensorXYZ"},
		Fields:      []string{"PressureValue"},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "invalid event_type_node_settings")
}

func TestSubscribeClientConfigEventMissingEventTypeNamespace(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName:  "testing",
			EventGroups: make([]input.EventGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.EventGroups = append(subscribeConfig.EventGroups, input.EventGroupSettings{
		SamplingInterval: 1.0,
		EventTypeNode: input.EventNodeSettings{
			IdentifierType: "i",
			Identifier:     "1234",
		},
		Namespace:      "3",
		IdentifierType: "i",
		NodeIDSettings: []input.EventNodeSettings{
			{
				Namespace:      "3",
				IdentifierType: "i",
				Identifier:     "12",
			},
		},
		SourceNames: []string{"SensorXYZ"},
		Fields:      []string{"PressureValue"},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "must specify either 'namespace' or 'namespace_uri'")
}

func TestSubscribeClientConfigEventMissingEventTypeIdentifierType(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName:  "testing",
			EventGroups: make([]input.EventGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.EventGroups = append(subscribeConfig.EventGroups, input.EventGroupSettings{
		SamplingInterval: 1.0,
		EventTypeNode: input.EventNodeSettings{
			Namespace:  "3",
			Identifier: "1234",
		},
		Namespace:      "3",
		IdentifierType: "i",
		NodeIDSettings: []input.EventNodeSettings{
			{
				Namespace:      "3",
				IdentifierType: "i",
				Identifier:     "12",
			},
		},
		SourceNames: []string{"SensorXYZ"},
		Fields:      []string{"PressureValue"},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "identifier_type must be set")
}

func TestSubscribeClientConfigEventMissingEventTypeIdentifier(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName:  "testing",
			EventGroups: make([]input.EventGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.EventGroups = append(subscribeConfig.EventGroups, input.EventGroupSettings{
		SamplingInterval: 1.0,
		EventTypeNode: input.EventNodeSettings{
			Namespace:      "3",
			IdentifierType: "i",
		},
		Namespace:      "3",
		IdentifierType: "i",
		NodeIDSettings: []input.EventNodeSettings{
			{
				Namespace:      "3",
				IdentifierType: "i",
				Identifier:     "12",
			},
		},
		SourceNames: []string{"SensorXYZ"},
		Fields:      []string{"PressureValue"},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "identifier must be set")
}

func TestSubscribeClientConfigEventInputMissingNodeIDs(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName:  "testing",
			EventGroups: make([]input.EventGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.EventGroups = append(subscribeConfig.EventGroups, input.EventGroupSettings{
		EventTypeNode: input.EventNodeSettings{
			Namespace:      "3",
			IdentifierType: "i",
			Identifier:     "1234",
		},
		Namespace:      "3",
		IdentifierType: "i",
		SourceNames:    []string{"SensorXYZ"},
		Fields:         []string{"PressureValue"},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "at least one node_id must be specified")
}

func TestSubscribeClientConfigEventInputMissingFields(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName:  "testing",
			EventGroups: make([]input.EventGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.EventGroups = append(subscribeConfig.EventGroups, input.EventGroupSettings{
		SamplingInterval: 1.0,
		EventTypeNode: input.EventNodeSettings{
			Namespace:      "3",
			IdentifierType: "i",
			Identifier:     "1234",
		},
		Namespace:      "3",
		IdentifierType: "i",
		NodeIDSettings: []input.EventNodeSettings{
			{
				Namespace:      "3",
				IdentifierType: "i",
				Identifier:     "12",
			},
		},
		SourceNames: []string{"SensorXYZ"},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "at least one Field must be specified")
}

func TestSubscribeClientConfigEventInputInvalidFields(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName:  "testing",
			EventGroups: make([]input.EventGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.EventGroups = append(subscribeConfig.EventGroups, input.EventGroupSettings{
		SamplingInterval: 1.0,
		EventTypeNode: input.EventNodeSettings{
			Namespace:      "3",
			IdentifierType: "i",
			Identifier:     "1234",
		},
		Namespace:      "3",
		IdentifierType: "i",
		NodeIDSettings: []input.EventNodeSettings{
			{
				Namespace:      "3",
				IdentifierType: "i",
				Identifier:     "12",
			},
		},
		SourceNames: []string{"SensorXYZ"},
		Fields:      []string{"Fieldname", ""},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.ErrorContains(t, err, "empty field name in fields stanza")
}

func TestSubscribeClientConfigValidEventStreamingDefaultNodeParams(t *testing.T) {
	subscribeConfig := subscribeClientConfig{
		InputClientConfig: input.InputClientConfig{
			OpcUAClientConfig: opcua.OpcUAClientConfig{
				Endpoint:       "opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer",
				SecurityPolicy: "None",
				SecurityMode:   "None",
				AuthMethod:     "Anonymous",
				ConnectTimeout: config.Duration(10 * time.Second),
				RequestTimeout: config.Duration(1 * time.Second),
				Workarounds:    opcua.OpcUAWorkarounds{},
			},
			MetricName:  "testing",
			EventGroups: make([]input.EventGroupSettings, 0),
		},
		SubscriptionInterval: 0,
	}
	subscribeConfig.EventGroups = append(subscribeConfig.EventGroups, input.EventGroupSettings{
		SamplingInterval: 1.0,
		EventTypeNode: input.EventNodeSettings{
			Namespace:      "3",
			IdentifierType: "i",
			Identifier:     "1234",
		},
		Namespace:      "3",
		IdentifierType: "i",
		NodeIDSettings: []input.EventNodeSettings{
			{
				Identifier: "12",
			},
		},
		SourceNames: []string{"SensorXYZ"},
		Fields:      []string{"PressureValue"},
	})

	_, err := subscribeConfig.createSubscribeClient(testutil.Logger{})
	require.NoError(t, err)

	o := subscribeConfig.InputClientConfig.EventGroups[0].NodeIDSettings[0]
	require.Equal(t, "i", o.IdentifierType)
	require.Equal(t, "3", o.Namespace)
}

func TestProcessNotificationsSurvivesNilValueAndError(t *testing.T) {
	// This test verifies that processReceivedNotifications continues processing
	// after receiving nil values and errors on the notification channel, which
	// occur during gopcua's automatic session reconnection cycle.
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	nodeID, err := ua.ParseNodeID("ns=0;i=2258")
	require.NoError(t, err)

	nodeSettings := input.NodeSettings{FieldName: "TestValue"}
	nmm, err := input.NewNodeMetricMapping("opcua", nodeSettings, nil)
	require.NoError(t, err)

	opcuaConfig := &opcua.OpcUAClientConfig{
		Endpoint:       "opc.tcp://localhost:4840",
		SecurityPolicy: "None",
		SecurityMode:   "None",
		AuthMethod:     "Anonymous",
		ConnectTimeout: config.Duration(5 * time.Second),
		RequestTimeout: config.Duration(10 * time.Second),
	}
	opcuaClient, err := opcuaConfig.CreateClient(testutil.Logger{})
	require.NoError(t, err)

	client := &subscribeClient{
		OpcUAInputClient: &input.OpcUAInputClient{
			OpcUAClient: opcuaClient,
			Log:         testutil.Logger{},
			Config: input.InputClientConfig{
				MetricName: "opcua",
				Timestamp:  input.TimestampSourceTelegraf,
			},
			NodeIDs:           []*ua.NodeID{nodeID},
			NodeMetricMapping: []input.NodeMetricMapping{*nmm},
			LastReceivedData: []input.NodeValue{
				{TagName: "TestValue"},
			},
		},
		dataNotifications: make(chan *gopcua.PublishNotificationData, 10),
		metrics:           make(chan telegraf.Metric, 10),
		ctx:               ctx,
		cancel:            cancel,
	}

	go client.processReceivedNotifications()

	// Simulate a reconnection error (gopcua sends errors through the channel during recovery)
	client.dataNotifications <- &gopcua.PublishNotificationData{
		Error: errors.New("session closed"),
	}

	// Simulate a nil value notification (can occur as a transient state during reconnection)
	client.dataNotifications <- &gopcua.PublishNotificationData{}

	// Send a valid data change notification after the error and nil value
	client.dataNotifications <- &gopcua.PublishNotificationData{
		Value: &ua.DataChangeNotification{
			MonitoredItems: []*ua.MonitoredItemNotification{
				{
					ClientHandle: 0,
					Value: &ua.DataValue{
						Value:           ua.MustVariant(float64(42.0)),
						Status:          ua.StatusOK,
						ServerTimestamp: time.Now(),
						SourceTimestamp: time.Now(),
					},
				},
			},
		},
	}

	// The goroutine must survive the error and nil value, then produce a metric
	require.Eventually(t, func() bool {
		return len(client.metrics) >= 1
	}, time.Second, 10*time.Millisecond)

	m := <-client.metrics
	require.Equal(t, "opcua", m.Name())
	v, ok := m.GetField("TestValue")
	require.True(t, ok)
	require.InDelta(t, 42.0, v, 0.001)
}
