/*
 * 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"

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

	"github.com/apache/plc4x/plc4go/spi/options"
	"github.com/apache/plc4x/plc4go/spi/utils"
)

type ParserSerializerTestsuite struct {
	name             string
	protocolName     string
	outputFlavor     string
	driverParameters map[string]string
	byteOrder        binary.ByteOrder
	parser           Parser
	rootTypeParser   func(utils.ReadBufferByteBased) (any, error)
	testcases        []ParserSerializerTestcase
}

type ParserSerializerTestcase struct {
	name            string
	rawInputText    string
	rootType        string
	referenceXml    string
	parserArguments []string
}

func (p *ParserSerializerTestsuite) Run(t *testing.T, testcase ParserSerializerTestcase) error {
	t.Logf("running testsuite: %s test: %s", p.name, testcase.name)

	// Get the raw input by decoding the hex-encoded binary input
	t.Log("decoding input")
	rawInput, err := hex.DecodeString(testcase.rawInputText)
	if err != nil {
		return errors.Wrap(err, "Error decoding test input")
	}

	// Create the right read buffer
	t.Log("creating read buffer")
	var readBuffer utils.ReadBuffer
	if p.byteOrder == binary.LittleEndian {
		readBuffer = utils.NewReadBufferByteBased(rawInput, utils.WithByteOrderForReadBufferByteBased(binary.LittleEndian))
	} else {
		readBuffer = utils.NewReadBufferByteBased(rawInput)
	}

	// Parse the input according to the settings of the testcase
	t.Log("parsing input")
	msg, err := p.parser.Parse(testcase.rootType, testcase.parserArguments, readBuffer)
	if err != nil {
		return errors.Wrap(err, "Error parsing input data")
	}
	t.Logf("Parsed Message(%T)", msg)
	t.Logf("\n%s", msg)

	t.Log("Try serializing")
	{
		// First try to use the native xml writer
		serializable := msg.(utils.Serializable)
		buffer := utils.NewXmlWriteBuffer()
		if err := serializable.SerializeWithWriteBuffer(context.Background(), buffer); err == nil {
			actualXml := buffer.GetXmlString()
			if err := CompareResults(t, []byte(actualXml), []byte(testcase.referenceXml)); err != nil {
				border := strings.Repeat("=", 100)
				fmt.Printf(
					"\n"+
						// Border
						"%[1]s\n"+
						// Testcase name
						"%[4]s\n"+
						// diff detected message
						"Diff detected\n"+
						// Border
						"%[1]s\n"+
						// xml
						"%[2]s\n"+
						// Border
						"%[1]s\n%[1]s\n"+
						// Text
						"Differences were found after parsing (Use the above xml in the testsuite to disable this warning).\n"+
						// Diff
						"%[3]s\n"+
						// Double Border
						"%[1]s\n%[1]s\n",
					border,
					actualXml,
					err,
					testcase.name)
				assert.Equal(t, testcase.referenceXml, actualXml)
				return errors.Wrap(err, "Error comparing the results")
			}
		}
	}

	// If all was ok, serialize the object again
	s, ok := msg.(utils.Serializable)
	if !ok {
		return errors.New("Couldn't cast message to Serializable")
	}
	t.Log("Serializing went ok")

	// Create the write buffer
	t.Log("creating write buffer")
	var writeBuffer utils.WriteBufferByteBased
	if p.byteOrder == binary.LittleEndian {
		writeBuffer = utils.NewWriteBufferByteBased(utils.WithByteOrderForByteBasedBuffer(binary.LittleEndian))
	} else {
		writeBuffer = utils.NewWriteBufferByteBased()
	}

	// Serialize the message
	t.Log("Serialize message")
	err = s.SerializeWithWriteBuffer(context.Background(), writeBuffer)
	if !ok {
		return errors.New("Couldn't serialize message back to byte array")
	}

	// Check if the output matches in size and content
	t.Log("comparing output")
	rawOutput := writeBuffer.GetBytes()
	if len(rawInput) != len(rawOutput) {
		t.Errorf("Missmatched number of bytes expected ->%d != %d<-actual\nexpected:\t%x\nactual:\t\t%x", len(rawInput), len(rawOutput), rawInput, rawOutput)
		t.Errorf("Hexdumps:\n%s", utils.DiffHex(rawInput, rawOutput))
		return errors.New("length doesn't match")
	}
	for i, val := range rawInput {
		if rawOutput[i] != val {
			t.Error("Raw output doesn't match input at position: " + strconv.Itoa(i))
			t.Errorf("Hexdumps:\n%s", utils.DiffHex(rawInput, rawOutput))
			return errors.New("index mismatch")
		}
	}
	return nil
}

type Parser interface {
	Parse(typeName string, arguments []string, io utils.ReadBuffer) (any, error)
}

func RunParserSerializerTestsuite(t *testing.T, testPath string, parser Parser, 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
			}
		}
	}

	// Read the test-specification as XML file
	rootNode := ParseParserSerializerTestSuiteXml(t, testPath)

	// Parse the contents of the test-specification
	testsuite := ParseParserSerializerTestSuite(t, *rootNode, parser, rootTypeParser)

	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 {
					t.Fatalf("\n-------------------------------------------------------\nPanic Failure\n%+v\n%s\n-------------------------------------------------------\n\n", err, debug.Stack())
				}
			}()
			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, testcase); err != nil {
				t.Fatalf("\n-------------------------------------------------------\nFailure\n%+v\n-------------------------------------------------------\n\n", err)
			}
		})
	}
	t.Log("Done running testcases")
	// Execute the tests in the testsuite
	t.Log(testsuite.name)
}

func ParseParserSerializerTestSuiteXml(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
	dirOffset := "/../../../"
	testFile := path + dirOffset + testPath
	info, err := os.Stat(testFile)
	if os.IsNotExist(err) {
		t.Logf("Test-File %s doesn't exist", testFile)
		t.Fatal(err)
	}
	if info.IsDir() {
		t.Logf("Test-File %s refers to a directory", testFile)
		t.Fatal(err)
	}

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

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

func ParseParserSerializerTestSuite(t *testing.T, node xmldom.Node, parser Parser, rootTypeParser func(utils.ReadBufferByteBased) (any, error)) *ParserSerializerTestsuite {
	if node.Name != "testsuite" {
		t.Fatal("invalid document structure")
	}
	var byteOrder binary.ByteOrder
	if node.GetAttributeValue("byteOrder") != "LITTLE_ENDIAN" {
		byteOrder = binary.BigEndian
	} else {
		byteOrder = binary.LittleEndian
	}
	var testsuiteName string
	var protocolName string
	var outputFlavor string
	driverParameters := make(map[string]string)
	var testcases []ParserSerializerTestcase
	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-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 "testcase":
			testcaseName := child.FindOneByName("name").Text
			rawInputText := (*(child.FindOneByName("raw"))).Text
			rootType := (*(child.FindOneByName("root-type"))).Text
			parserArgumentsXml := child.FindOneByName("parser-arguments")
			var parserArguments []string
			if parserArgumentsXml != nil {
				for _, parserArgumentXml := range parserArgumentsXml.Children {
					parserArguments = append(parserArguments, parserArgumentXml.Text)
				}
			}
			referenceXml := child.FindOneByName("xml")
			normalizeXml(referenceXml)
			referenceSerialized := referenceXml.FirstChild().XMLPretty()
			testcase := ParserSerializerTestcase{
				name:            testcaseName,
				rawInputText:    rawInputText,
				rootType:        rootType,
				referenceXml:    referenceSerialized,
				parserArguments: parserArguments,
			}
			testcases = append(testcases, testcase)
		default:
			t.Fatalf("invalid document structure. Unhandled element %s", child.Name)
		}
	}
	t.Logf("Parsed testsuite name: %s", testsuiteName)

	return &ParserSerializerTestsuite{
		name:             testsuiteName,
		protocolName:     protocolName,
		outputFlavor:     outputFlavor,
		driverParameters: driverParameters,
		byteOrder:        byteOrder,
		parser:           parser,
		rootTypeParser:   rootTypeParser,
		testcases:        testcases,
	}
}

// Mainly remove linebreaks from text content.
func normalizeXml(input *xmldom.Node) {
	if len(input.Children) > 0 {
		for _, child := range input.Children {
			normalizeXml(child)
		}
	}
	if len(input.Text) > 0 {
		if strings.Contains(input.Text, "\n") {
			input.Text = strings.Replace(input.Text, "\n", "", -1)
		}
	}
}
