/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package testutils

import (
	"context"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"os"
	"runtime/debug"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
	"github.com/subchen/go-xmldom"

	"github.com/apache/plc4x/plc4go/pkg/api"
	"github.com/apache/plc4x/plc4go/pkg/api/config"
	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
	"github.com/apache/plc4x/plc4go/spi"
	"github.com/apache/plc4x/plc4go/spi/options"
	"github.com/apache/plc4x/plc4go/spi/options/converter"
	"github.com/apache/plc4x/plc4go/spi/transports"
	"github.com/apache/plc4x/plc4go/spi/transports/test"
	"github.com/apache/plc4x/plc4go/spi/utils"
)

var DriverTestsuiteConnectTimeout = 30 * time.Second

type DriverTestsuite struct {
	name             string
	protocolName     string
	outputFlavor     string
	driverName       string
	driverParameters map[string]string
	byteOrder        binary.ByteOrder
	parser           XmlParser
	rootTypeParser   func(utils.ReadBufferByteBased) (any, error)
	setupSteps       []DriverTestStep
	teardownSteps    []DriverTestStep
	testcases        []DriverTestcase
}

type XmlParser interface {
	Parse(typeName string, xmlString string, parserArguments ...string) (any, error)
}

// WithRootTypeParser can be used to output the root type of protocol for better debugging
func WithRootTypeParser(rootTypeParser func(utils.ReadBufferByteBased) (any, error)) config.WithOption {
	return withRootTypeParser{rootTypeParser: rootTypeParser}
}

type withRootTypeParser struct {
	options.Option
	rootTypeParser func(utils.ReadBufferByteBased) (any, error)
}

// WithSkippedTestCases can be used to skip test cases
func WithSkippedTestCases(skippedTestCases ...string) config.WithOption {
	return withSkippedTestCases{skippedTestCases: skippedTestCases}
}

type withSkippedTestCases struct {
	options.Option
	skippedTestCases []string
}

type TestTransportInstance interface {
	transports.TransportInstance
	FillReadBuffer(data []byte)
	GetNumDrainableBytes() uint32
	DrainWriteBuffer(numBytes uint32) []byte
}

func (m DriverTestsuite) Run(t *testing.T, driverManager plc4go.PlcDriverManager, testcase DriverTestcase) error {
	var driverParameters []string
	for key, value := range m.driverParameters {
		driverParameters = append(driverParameters, fmt.Sprintf("%s=%s", key, value))
	}
	optionsString := ""
	if len(driverParameters) > 0 {
		optionsString = "?" + strings.Join(driverParameters, "&")
	}
	// Get a connection
	t.Log("getting a connection")
	connectionChan := driverManager.GetConnection(m.driverName + ":test://hurz" + optionsString)
	timer := time.NewTimer(DriverTestsuiteConnectTimeout)
	var connectionResult plc4go.PlcConnectionConnectResult
	select {
	case connectionResult = <-connectionChan:
	case <-timer.C:
		t.Fatalf("timeout")
	}

	if connectionResult.GetErr() != nil {
		return errors.Wrap(connectionResult.GetErr(), "error getting a connection")
	}
	connection := connectionResult.GetConnection()
	t.Cleanup(func() {
		timeout := time.NewTimer(30 * time.Second)
		select {
		case result := <-connection.Close():
			assert.NoError(t, result.GetErr())
		case <-timeout.C:
			t.Error("timeout closing connection")
		}
	})
	utils.NewAsciiBoxWriter()
	m.LogDelimiterSection(t, "=", "Executing testcase: %s", testcase.name)

	if len(m.setupSteps) > 0 {
		// Run the setup steps
		m.LogDelimiterSection(t, "-", "Performing setup for: %s", testcase.name)
		for _, testStep := range m.setupSteps {
			err := m.ExecuteStep(t, connection, &testcase, testStep)
			if err != nil {
				return errors.Wrap(err, "error in setup step "+testStep.name)
			}
			// We sleep a bit to not run too fast into the post setup steps and give connections a bit time to settle built up
			time.Sleep(100 * time.Millisecond) // TODO: this is really bad as on CI sometimes those sleeps are not enough...
		}
		m.LogDelimiterSection(t, "-", "setup done for: %s", testcase.name)
	} else {
		t.Log("no setup steps")
	}

	if len(testcase.steps) > 0 {
		// Run the actual scenario steps
		m.LogDelimiterSection(t, "-", "Running testcases for: %s", testcase.name)
		for _, testStep := range testcase.steps {
			err := m.ExecuteStep(t, connection, &testcase, testStep)
			if err != nil {
				return errors.Wrap(err, "error in step "+testStep.name)
			}
			time.Sleep(100 * time.Millisecond) // TODO: this is really bad as on CI sometimes those sleeps are not enough...
		}
		m.LogDelimiterSection(t, "-", "testcases done for: %s", testcase.name)
	} else {
		t.Log("no testcase steps")
	}

	if len(m.teardownSteps) > 0 {
		// Run the teardown steps
		m.LogDelimiterSection(t, "-", "Performing teardown for: %s", testcase.name)
		for _, testStep := range m.teardownSteps {
			err := m.ExecuteStep(t, connection, &testcase, testStep)
			if err != nil {
				return errors.Wrap(err, "error in teardown step "+testStep.name)
			}
			time.Sleep(100 * time.Millisecond) // TODO: this is really bad as on CI sometimes those sleeps are not enough...
		}
		m.LogDelimiterSection(t, "-", "teardown done for: %s", testcase.name)
	} else {
		t.Log("no teardown steps")
	}

	m.LogDelimiterSection(t, "=", "done testcase: %s", testcase.name)
	return nil
}

func (m DriverTestsuite) ExecuteStep(t *testing.T, connection plc4go.PlcConnection, testcase *DriverTestcase, step DriverTestStep) error {
	mc, ok := connection.(spi.TransportInstanceExposer)
	if !ok {
		return errors.New("couldn't access connections transport instance")
	}
	testTransportInstance, ok := mc.GetTransportInstance().(TestTransportInstance)
	if !ok {
		return errors.New("transport must be of type TestTransport")
	}

	start := time.Now()
	m.LogDelimiterSection(t, "┄", " - Executing step (%s): %s", step.stepType, step.name)
	switch step.stepType {
	case StepTypeApiRequest:
		switch step.payload.Name {
		case "TestReadRequest":
			// Assemble a read-request according to the information in the test xml
			t.Log("Assemble read request")
			rrb := connection.ReadRequestBuilder()
			for _, tagNode := range step.payload.GetChild("tags").GetChildren("tag") {
				tagName := tagNode.GetChild("name").Text
				tagAddress := tagNode.GetChild("address").Text
				rrb.AddTagAddress(tagName, tagAddress)
			}
			readRequest, err := rrb.Build()
			if err != nil {
				return errors.Wrap(err, "Error creating read-request")
			}

			// Execute the read-request and store the response-channel in the testcase.
			t.Logf("Execute read request (%T)\n%[1]s", readRequest)
			if testcase.readRequestResultChannel != nil {
				return errors.New("testcase read-request result channel already occupied")
			}
			testcase.readRequestResultChannel = readRequest.Execute()
			t.Log("request executed")
		case "TestWriteRequest":
			t.Log("Assemble write request")
			wrb := connection.WriteRequestBuilder()
			for _, tagNode := range step.payload.GetChild("tags").GetChildren("tag") {
				tagName := tagNode.GetChild("name").Text
				tagAddress := tagNode.GetChild("address").Text

				he, ok := connection.(spi.HandlerExposer)
				if !ok {
					return errors.New("connection is not a HandlerExposer")
				}
				tag, err := he.GetPlcTagHandler().ParseTag(tagAddress)
				if err != nil {
					return errors.Wrapf(err, "error parsing address: %s", tagAddress)
				}
				if len(tag.GetArrayInfo()) > 0 {
					var tagValue []string
					for _, valueChild := range tagNode.GetChildren("value") {
						tagValue = append(tagValue, valueChild.Text)
					}
					wrb.AddTagAddress(tagName, tagAddress, tagValue)
				} else {
					tagValue := tagNode.GetChild("value").Text
					wrb.AddTagAddress(tagName, tagAddress, tagValue)
				}
			}
			writeRequest, err := wrb.Build()
			if err != nil {
				return errors.Wrap(err, "Error creating write-request")
			}
			t.Logf("Execute write request (%T)\n%[1]s", writeRequest)
			if testcase.writeRequestResultChannel != nil {
				return errors.New("testcase write-request result channel already occupied")
			}
			testcase.writeRequestResultChannel = writeRequest.Execute()
			t.Log("request executed")
		}
	case StepTypeApiResponse:
		switch step.payload.Name {
		case "PlcReadResponse":
			if testcase.readRequestResultChannel == nil {
				return errors.New("no read response expected")
			}
			t.Log("Waiting for read request result")
			readRequestResult := <-testcase.readRequestResultChannel
			if readRequestResult.GetErr() != nil {
				return errors.Wrap(readRequestResult.GetErr(), "error sending response")
			}
			// Serialize the response to XML
			xmlWriteBuffer := utils.NewXmlWriteBuffer()
			response := readRequestResult.GetResponse()
			t.Logf("Got response (%T)\n%[1]s", response)
			err := response.(utils.Serializable).SerializeWithWriteBuffer(context.Background(), xmlWriteBuffer)
			if err != nil {
				return errors.Wrap(err, "error serializing response")
			}
			actualResponse := xmlWriteBuffer.GetXmlString()
			// Get the reference XML
			referenceSerialized := step.payload.XMLPretty()
			// Compare the results
			err = CompareResults(t, []byte(actualResponse), []byte(referenceSerialized))
			if err != nil {
				return errors.Wrap(err, "Error comparing the results")
			}
			// Reset read channel
			testcase.readRequestResultChannel = nil
		case "PlcWriteResponse":
			if testcase.writeRequestResultChannel == nil {
				return errors.New("no write response expected")
			}
			t.Log("Waiting for write request result")
			writeResponseResult := <-testcase.writeRequestResultChannel
			if writeResponseResult.GetErr() != nil {
				return errors.Wrap(writeResponseResult.GetErr(), "error sending response")
			}
			// Serialize the response to XML
			xmlWriteBuffer := utils.NewXmlWriteBuffer()
			response := writeResponseResult.GetResponse()
			t.Logf("Got response (%T)\n%[1]s", response)
			err := response.(utils.Serializable).SerializeWithWriteBuffer(context.Background(), xmlWriteBuffer)
			if err != nil {
				return errors.Wrap(err, "error serializing response")
			}
			actualResponse := xmlWriteBuffer.GetXmlString()
			// Get the reference XML
			referenceSerialized := step.payload.XMLPretty()
			// Compare the results
			err = CompareResults(t, []byte(actualResponse), []byte(referenceSerialized))
			if err != nil {
				return errors.Wrap(err, "Error comparing the results")
			}
			// Reset write channel
			testcase.writeRequestResultChannel = nil
		}
	case StepTypeOutgoingPlcMessage:
		typeName := step.payload.Name
		payloadString := step.payload.XMLPretty()

		// Parse the xml into a real model
		t.Log("parsing xml")
		expectedMessage, err := m.parseMessage(typeName, payloadString, step)
		if err != nil {
			return errors.Wrap(err, "Error parsing message")
		}
		t.Logf("Parsed message (%T) from xml\n%[1]s", expectedMessage)

		// Serialize the model into bytes
		t.Log("Serialize the model into bytes and compare output")
		expectedSerializable, ok := expectedMessage.(utils.Serializable)
		if !ok {
			return errors.Errorf("error converting type %t into Serializable type", expectedMessage)
		}
		var expectedWriteBuffer utils.WriteBufferByteBased
		if m.byteOrder == binary.BigEndian {
			expectedWriteBuffer = utils.NewWriteBufferByteBased()
		} else {
			t.Log("using little endian")
			expectedWriteBuffer = utils.NewWriteBufferByteBased(utils.WithByteOrderForByteBasedBuffer(binary.LittleEndian))
		}
		err = expectedSerializable.SerializeWithWriteBuffer(context.Background(), expectedWriteBuffer)
		if err != nil {
			return errors.Wrap(err, "error serializing expectedMessage")
		}
		expectedRawOutput := expectedWriteBuffer.GetBytes()
		expectedRawOutputLength := uint32(len(expectedRawOutput))

		startTransportPolling := time.Now()
		// Read exactly this amount of bytes from the transport
		t.Logf("Reading bytes from transport instance (expectedRawOutputLength %d)", expectedRawOutputLength)
		for testTransportInstance.GetNumDrainableBytes() < expectedRawOutputLength {
			if time.Since(startTransportPolling) > 2*time.Second {
				drainableBytes := testTransportInstance.GetNumDrainableBytes()
				actualRawOutput := testTransportInstance.DrainWriteBuffer(drainableBytes)
				return errors.Errorf("Not enough data available: actual(%d)<expected(%d), \nactual:   %#X\nexpected: %#X\nHexdumps:\n%s",
					drainableBytes, expectedRawOutputLength, actualRawOutput, expectedRawOutput, utils.DiffHex(expectedRawOutput, actualRawOutput))
			}
			time.Sleep(2 * time.Millisecond)
		}
		actualRawOutput := testTransportInstance.DrainWriteBuffer(expectedRawOutputLength)
		if testTransportInstance.GetNumDrainableBytes() != 0 {
			t.Logf("leftover drainable bytes (%d)", testTransportInstance.GetNumDrainableBytes())
		}

		var bufferFactory func([]byte, ...utils.ReadBufferByteBasedOptions) utils.ReadBufferByteBased
		if m.byteOrder == binary.BigEndian {
			bufferFactory = utils.NewReadBufferByteBased
		} else {
			bufferFactory = func(bytes []byte, options ...utils.ReadBufferByteBasedOptions) utils.ReadBufferByteBased {
				t.Log("using little endian")
				return utils.NewReadBufferByteBased(bytes, utils.WithByteOrderForReadBufferByteBased(binary.LittleEndian))
			}
		}
		// Compare the bytes read with the ones we expect
		t.Log("Comparing outputs")
		for i := range expectedRawOutput {
			if expectedRawOutput[i] != actualRawOutput[i] {
				if m.rootTypeParser != nil {
					readBufferByteBased := bufferFactory(actualRawOutput)
					actual, err := m.rootTypeParser(readBufferByteBased)
					t.Logf("A readable render of expectation:\n%v\nvs actual paket\n%v\n%#v", expectedSerializable, actual, err)
				}
				return errors.Errorf("actual output doesn't match expected output:\nactual:   %#X\nexpected: %#X\nHexdumps:\n%s", actualRawOutput, expectedRawOutput, utils.DiffHex(expectedRawOutput, actualRawOutput))
			}
		}
		t.Log("outputs are matching")
		// If there's a difference, parse the input and display it to simplify debugging
	case StepTypeOutgoingPlcBytes:
		// Read exactly this amount of bytes from the transport
		t.Log("Reading bytes")
		expectedRawInput, err := hex.DecodeString(step.payload.Text)
		if err != nil {
			return errors.Wrap(err, "error decoding hex-encoded byte data")
		}
		t.Logf("\n%s", hex.Dump(expectedRawInput))
		rawInput := testTransportInstance.DrainWriteBuffer(uint32(len(expectedRawInput)))

		// Compare the bytes read with the ones we expect
		t.Log("Comparing bytes")
		for i := range expectedRawInput {
			if expectedRawInput[i] != rawInput[i] {
				return errors.Errorf("actual output doesn't match expected output:\nactual:   %#X\nexpected: %#X\nHexdumps:\n%s", rawInput, expectedRawInput, utils.DiffHex(expectedRawInput, rawInput))
			}
		}
		// If there's a difference, parse the input and display it to simplify debugging
	case StepTypeIncomingPlcMessage:
		typeName := step.payload.Name
		payloadString := step.payload.XMLPretty()

		// Parse the xml into a real model
		t.Log("Parsing model")
		expectedMessage, err := m.parseMessage(typeName, payloadString, step)
		if err != nil {
			return errors.Wrap(err, "error parsing message")
		}
		t.Logf("Parsed message (%T)\n%[1]s", expectedMessage)

		// Serialize the model into bytes
		t.Log("Serializing bytes")
		expectedSerializable, ok := expectedMessage.(utils.Serializable)
		if !ok {
			return errors.New("error converting type into Serializable type")
		}
		var wb utils.WriteBufferByteBased
		if m.byteOrder == binary.BigEndian {
			wb = utils.NewWriteBufferByteBased()
		} else {
			t.Log("using little endian")
			wb = utils.NewWriteBufferByteBased(utils.WithByteOrderForByteBasedBuffer(binary.LittleEndian))
		}
		err = expectedSerializable.SerializeWithWriteBuffer(context.Background(), wb)
		if err != nil {
			return errors.Wrap(err, "error serializing expectedMessage")
		}

		// Send these bytes to the transport
		_bytes := wb.GetBytes()
		t.Logf("Writing to transport\n%s", hex.Dump(_bytes))
		testTransportInstance.FillReadBuffer(_bytes)
	case StepTypeIncomingPlcBytes:
		// Get the raw hex-data.
		t.Log("Get hex data")
		rawInput, err := hex.DecodeString(step.payload.Text)
		if err != nil {
			return errors.Wrap(err, "error decoding hex-encoded byte data: ")
		}

		// Send these bytes to the transport
		t.Logf("Writing bytes to transport\n%[1]s", hex.Dump(rawInput))
		testTransportInstance.FillReadBuffer(rawInput)
	case StepTypeDelay:
		// Get the number of milliseconds
		t.Log("Getting millis")
		delay, err := strconv.Atoi(step.payload.Text)
		if err != nil {
			return errors.Wrap(err, "invalid delay format")
		}
		// Sleep for that long
		t.Logf("Sleeping (delay: %dms)", delay)
		time.Sleep(time.Duration(delay) * time.Millisecond)
	case StepTypeTerminate:
		// Simply close the transport connection
		t.Log("closing transport")
		err := testTransportInstance.Close()
		if err != nil {
			return errors.Wrap(err, "error closing transport")
		}
	}
	m.LogDelimiterSection(t, "┄", " - Finished step: %s after %s ", step.name, time.Since(start))
	return nil
}

func (m DriverTestsuite) parseMessage(typeName string, payloadString string, step DriverTestStep) (any, error) {
	if m.parser == nil {
		return nil, errors.Errorf("Protocol name %s has no mapped parser", m.protocolName)
	}
	parse, err := m.parser.Parse(typeName, payloadString, step.parserArguments...)
	if err != nil {
		return nil, errors.Wrap(err, "error parsing xml")
	}
	return parse, err
}

type DriverTestcase struct {
	name                      string
	steps                     []DriverTestStep
	readRequestResultChannel  <-chan apiModel.PlcReadRequestResult
	writeRequestResultChannel <-chan apiModel.PlcWriteRequestResult
}

type DriverTestStep struct {
	name            string
	stepType        StepType
	parserArguments []string
	payload         xmldom.Node
}

type StepType uint8

//go:generate go tool stringer -type StepType
//go:generate go tool plc4xLicencer -type=StepType
const (
	StepTypeOutgoingPlcMessage StepType = 0x01
	StepTypeOutgoingPlcBytes   StepType = 0x02
	StepTypeIncomingPlcMessage StepType = 0x03
	StepTypeIncomingPlcBytes   StepType = 0x04
	StepTypeApiRequest         StepType = 0x05
	StepTypeApiResponse        StepType = 0x06
	StepTypeDelay              StepType = 0x07
	StepTypeTerminate          StepType = 0x08
)

func RunDriverTestsuite(t *testing.T, driver plc4go.PlcDriver, testPath string, parser XmlParser, _options ...options.WithOption) {
	t.Log("Extract testsuite options")
	var rootTypeParser func(utils.ReadBufferByteBased) (any, error)
	skippedTestCasesMap := map[string]bool{}
	for _, withOption := range _options {
		switch option := withOption.(type) {
		case withRootTypeParser:
			t.Logf("Using root type parser for better output")
			rootTypeParser = option.rootTypeParser
		case withSkippedTestCases:
			t.Log("Skipping test cases:")
			for _, skippedTestCase := range option.skippedTestCases {
				t.Logf("Skipping %s", skippedTestCase)
				skippedTestCasesMap[skippedTestCase] = true
			}
		}
	}
	t.Log("Read the test-specification as XML file")
	// Read the test-specification as XML file
	rootNode := ParseDriverTestsuiteXml(t, testPath)

	t.Log("Parse the contents of the test-specification")
	// Parse the contents of the test-specification
	testsuite := ParseDriverTestsuite(t, *rootNode, parser, rootTypeParser)

	// We don't want to await completion of connection initialization
	if connectionConnectAwaiter, ok := driver.(ConnectionConnectAwaiter); ok {
		t.Log("We don't wait for setup and disconnect")
		connectionConnectAwaiter.SetAwaitSetupComplete(false)
		connectionConnectAwaiter.SetAwaitDisconnectComplete(false)
	}

	t.Log("Initialize the driver manager")
	// Initialize the driver manager
	driverManager := plc4go.NewPlcDriverManager(converter.WithOptionToExternal(_options...)...)
	t.Cleanup(func() {
		assert.NoError(t, driverManager.Close())
	})
	transport := test.NewTransport(_options...)
	driverManager.(spi.TransportAware).RegisterTransport(transport)
	driverManager.RegisterDriver(driver)

	t.Logf("Running %d testcases", len(testsuite.testcases))
	for _, testcase := range testsuite.testcases {
		t.Run(testcase.name, func(t *testing.T) {
			defer func() {
				if err := recover(); err != nil {
					testsuite.LogDelimiterSection(t, "=", "Panic Failure\n%+v\n%s", err, debug.Stack())
					t.FailNow()
				}
			}()
			if skippedTestCasesMap[testcase.name] {
				t.Logf("Testcase %s skipped", testcase.name)
				t.Skipf("Testcase %s skipped", testcase.name)
				return
			}
			t.Logf("Running testcase %s", testcase.name)
			if err := testsuite.Run(t, driverManager, testcase); err != nil {
				testsuite.LogDelimiterSection(t, "=", "Failure:\n%s", err)
				t.FailNow()
			}
		})
	}
}

type ConnectionConnectAwaiter interface {
	// SetAwaitSetupComplete sets a flag that the driver should await a connection completion
	SetAwaitSetupComplete(awaitComplete bool)
	// SetAwaitDisconnectComplete sets a flag that the driver should await a dis-connection completion
	SetAwaitDisconnectComplete(awaitComplete bool)
}

func ParseDriverTestsuiteXml(t *testing.T, testPath string) *xmldom.Node {
	// Get the current working directory
	path, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}

	// Check if the test-file is available
	pathOffset := "/../../../"
	testSuitePath := path + pathOffset + testPath
	info, err := os.Stat(testSuitePath)
	if os.IsNotExist(err) {
		t.Logf("Test-File %s doesn't exist", testSuitePath)
		t.Fatal(err)
	}
	if info.IsDir() {
		t.Logf("Test-File %s refers to a directory", testSuitePath)
		t.Fatal(err)
	}

	// Open a reader for this file
	dat, err := os.Open(testSuitePath)
	if err != nil {
		t.Log("error opening file")
		t.Fatal(err)
	}

	// Read the xml
	node := xmldom.Must(xmldom.Parse(dat)).Root
	return node
}

func ParseDriverTestsuite(t *testing.T, node xmldom.Node, parser XmlParser, rootTypeParser func(utils.ReadBufferByteBased) (any, error)) *DriverTestsuite {
	if node.Name != "driver-testsuite" {
		t.Fatal("invalid document structure")
	}
	var byteOrder binary.ByteOrder
	if node.GetAttributeValue("byteOrder") != "LITTLE_ENDIAN" {
		byteOrder = binary.BigEndian
	} else {
		t.Log("using little endian")
		byteOrder = binary.LittleEndian
	}
	var testsuiteName string
	var protocolName string
	var outputFlavor string
	var driverName string
	driverParameters := make(map[string]string)
	var setupSteps []DriverTestStep
	var teardownSteps []DriverTestStep
	var testcases []DriverTestcase
	for _, childPtr := range node.Children {
		child := *childPtr
		switch child.Name {
		case "name":
			testsuiteName = child.Text
		case "protocolName":
			protocolName = child.Text
		case "outputFlavor":
			outputFlavor = child.Text
		case "driver-name":
			driverName = child.Text
		case "driver-parameters":
			parameterList := child.FindByName("parameter")
			for _, parameter := range parameterList {
				nameElement := parameter.FindOneByName("name")
				valueElement := parameter.FindOneByName("value")
				if nameElement == nil || valueElement == nil {
					t.Fatal("invalid parameter found: no present")
				}
				name := nameElement.Text
				value := valueElement.Text
				if name == "" || value == "" {
					t.Fatal("invalid parameter found: empty")
				}
				driverParameters[name] = value
			}
		case "setup":
			steps, err := ParseDriverTestsuiteSteps(t, child)
			if err != nil {
				t.Error("error parsing setup steps")
				t.Fatal(err)
			}
			setupSteps = steps
		case "teardown":
			steps, err := ParseDriverTestsuiteSteps(t, child)
			if err != nil {
				t.Error("error parsing teardown steps")
				t.Fatal(err)
			}
			teardownSteps = steps
		case "testcase":
			testcaseName := child.FindOneByName("name").Text
			stepsNode := child.FindOneByName("steps")
			steps, err := ParseDriverTestsuiteSteps(t, *stepsNode)
			if err != nil {
				t.Errorf("error parsing testcase %s", testcaseName)
				t.Fatal(err)
			}
			testcase := DriverTestcase{
				name:  testcaseName,
				steps: steps,
			}
			testcases = append(testcases, testcase)
		default:
			t.Fatalf("invalid document structure. Unhandled element %s", child.Name)
		}
	}
	t.Logf("Parsed testsuite name: %s, driver name: %s", testsuiteName, driverName)

	return &DriverTestsuite{
		name:             testsuiteName,
		protocolName:     protocolName,
		outputFlavor:     outputFlavor,
		driverName:       driverName,
		driverParameters: driverParameters,
		byteOrder:        byteOrder,
		parser:           parser,
		rootTypeParser:   rootTypeParser,
		setupSteps:       setupSteps,
		teardownSteps:    teardownSteps,
		testcases:        testcases,
	}
}

func ParseDriverTestsuiteSteps(t *testing.T, node xmldom.Node) ([]DriverTestStep, error) {
	t.Logf("Parsing driver testsuite steps (rootElement %s)", node.Name)
	var testSteps []DriverTestStep
	for _, step := range node.Children {
		name := step.GetAttributeValue("name")
		t.Logf("Parsing step (name: %s, rootElement %s)", name, node.Name)
		var stepType StepType
		switch step.Name {
		case "api-request":
			stepType = StepTypeApiRequest
		case "api-response":
			stepType = StepTypeApiResponse
		case "outgoing-plc-message":
			stepType = StepTypeOutgoingPlcMessage
		case "incoming-plc-message":
			stepType = StepTypeIncomingPlcMessage
		case "outgoing-plc-bytes":
			stepType = StepTypeOutgoingPlcBytes
		case "incoming-plc-bytes":
			stepType = StepTypeIncomingPlcBytes
		case "delay":
			stepType = StepTypeDelay
		case "terminate":
			stepType = StepTypeTerminate
		default:
			return nil, errors.Errorf("Unknown step with name %s", step.Name)
		}
		var parserArguments []string
		var payload *xmldom.Node
		t.Logf("Looking for payload (name: %s, rootElement %s)", name, node.Name)
		for _, childNode := range step.Children {
			t.Logf("Found payload candidate (child node name: %s, name: %s, rootElement %s)", childNode.Name, name, node.Name)
			if childNode.Name == "parser-arguments" {
				for _, parserArgumentNode := range childNode.Children {
					parserArguments = append(parserArguments, parserArgumentNode.Text)
				}
			} else if payload == nil {
				payload = childNode
			} else {
				return nil, errors.New("test step can only contain a single payload element")
			}
		}
		if stepType == StepTypeDelay {
			payload = step
		}
		if payload == nil {
			return nil, errors.New("missing payload element")
		}
		testSteps = append(testSteps, DriverTestStep{
			name:            name,
			stepType:        stepType,
			parserArguments: parserArguments,
			payload:         *payload,
		})
	}
	return testSteps, nil
}

func (DriverTestsuite) LogDelimiterSection(t *testing.T, delimiter, format string, a ...any) {
	t.Helper()
	delimiter = strings.Repeat(delimiter, 55)
	if !shouldNoColor() {
		delimiter = fmt.Sprintf("\x1b[%dm%v\x1b[0m", 32, delimiter)
	}
	message := "\n"
	message += delimiter + "\n"
	message += fmt.Sprintf(format, a...) + "\n"
	message += delimiter
	t.Log(message)
}
