/*
 * 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 (
	"fmt"
	"math/rand"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"

	"github.com/apache/plc4x/plc4go/pkg/api"
	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
	spiValues "github.com/apache/plc4x/plc4go/spi/values"
)

type ManualTestCase struct {
	Address           string
	ExpectedReadValue any
	WriteValue        any
	UnwrappedValue    bool
}

type ManualTestSuite struct {
	ConnectionString string
	DriverManager    plc4go.PlcDriverManager
	TestCases        []ManualTestCase
	t                *testing.T
}

func NewManualTestSuite(t *testing.T, connectionString string, driverManager plc4go.PlcDriverManager) *ManualTestSuite {
	return &ManualTestSuite{
		ConnectionString: connectionString,
		DriverManager:    driverManager,
		t:                t,
	}
}

func (m *ManualTestSuite) AddTestCase(address string, expectedReadValue any, testCaseOptions ...WithTestCaseOption) {
	testCase := ManualTestCase{Address: address, ExpectedReadValue: expectedReadValue, UnwrappedValue: true}
	for _, testCaseOption := range testCaseOptions {
		testCaseOption(testCase)
	}
	m.TestCases = append(m.TestCases, testCase)
}

type WithTestCaseOption func(testCase ManualTestCase)

// WithUnwrappedValue is a WithTestCaseOption which can be used to control if plc4go.PlcValue
func WithUnwrappedValue(unwrap bool) WithTestCaseOption {
	return func(testCase ManualTestCase) {
		testCase.UnwrappedValue = unwrap
	}
}

func (m *ManualTestSuite) Run() plc4go.PlcConnection {
	connectionResult := <-m.DriverManager.GetConnection(m.ConnectionString)
	if err := connectionResult.GetErr(); err != nil {
		tracer, ok := errors.Cause(err).(interface{ StackTrace() errors.StackTrace })
		if ok {
			stackTrace := tracer.StackTrace()
			for _, f := range stackTrace {
				fmt.Printf("%+s:%d\n", f, f)
			}
			fmt.Println()
		}
		m.t.Error(err)
		m.t.FailNow()
	}
	connection := connectionResult.GetConnection()
	m.t.Cleanup(func() {
		connection.Close()
	})
	m.t.Log("Reading all types in separate requests")
	// Run all entries separately:
	for _, testCase := range m.TestCases {
		tagName := testCase.Address
		m.t.Run(tagName, func(t *testing.T) {
			m.runSingleTest(t, connection, tagName, testCase)
		})
	}
	m.t.Run("combinedTest", func(t *testing.T) {
		m.runBurstTest(t, connection)
	})
	return connection
}

func (m *ManualTestSuite) runSingleTest(t *testing.T, connection plc4go.PlcConnection, tagName string, testCase ManualTestCase) {
	// Prepare the read-request
	readRequestBuilder := connection.ReadRequestBuilder()
	readRequestBuilder.AddTagAddress(tagName, testCase.Address)
	readRequest, err := readRequestBuilder.Build()
	if err != nil {
		t.Fatal(err)
		return
	}

	// Execute the read request
	readResponseResult := <-readRequest.Execute()
	if readResponseResult.GetErr() != nil {
		t.Fatalf("Error getting response %v", readResponseResult.GetErr())
		return
	}
	readResponse := readResponseResult.GetResponse()

	// Check the result
	assert.Equalf(t, 1, len(readResponse.GetTagNames()), "response should have a tag for %s", tagName)
	assert.Equalf(t, tagName, readResponse.GetTagNames()[0], "first tag should be equal to %s", tagName)
	assert.Equalf(t, apiModel.PlcResponseCode_OK, readResponse.GetResponseCode(tagName), "response code should be ok for %s", tagName)
	assert.NotNil(t, readResponse.GetValue(tagName), tagName)
	expectation := reflect.ValueOf(testCase.ExpectedReadValue)
	if readResponse.GetValue(tagName).IsList() && (expectation.Kind() == reflect.Slice || expectation.Kind() == reflect.Array) {
		plcList := readResponse.GetValue(tagName).GetList()
		for j := 0; j < expectation.Len(); j++ {
			var actual any
			actual = plcList[j]
			if testCase.UnwrappedValue {
				switch actualCasted := actual.(type) {
				case spiValues.PlcBOOL:
					actual = actualCasted.GetBool()
				case spiValues.PlcWORD:
					actual = actualCasted.GetInt8()
				default:
					t.Fatalf("%T not yet mapped", actualCasted)
				}
			}
			assert.Equal(t, expectation.Index(j).Interface(), actual, fmt.Sprintf("%s[%d]", tagName, j))
		}
	} else {
		assert.Equal(t, fmt.Sprint(testCase.ExpectedReadValue), readResponse.GetValue(tagName).GetString(), tagName)
	}
}

func (m *ManualTestSuite) runBurstTest(t *testing.T, connection plc4go.PlcConnection) {
	// Read all items in one big request.
	// Shuffle the list of test cases and run the test 10 times.
	t.Log("Reading all items together in random order")
	for i := 0; i < 100; i++ {
		t.Logf(" - run number %d of %d", i, 100)
		shuffledTestcases := append(make([]ManualTestCase, 0), m.TestCases...)
		rand.Seed(time.Now().UnixNano())
		rand.Shuffle(len(shuffledTestcases), func(i, j int) {
			shuffledTestcases[i], shuffledTestcases[j] = shuffledTestcases[j], shuffledTestcases[i]
		})

		sb := strings.Builder{}
		for _, testCase := range shuffledTestcases {
			sb.WriteString(testCase.Address)
			sb.WriteString(", ")
		}
		t.Logf("       using order: %s", sb.String())

		builder := connection.ReadRequestBuilder()
		for _, testCase := range shuffledTestcases {
			tagName := testCase.Address
			builder.AddTagAddress(tagName, testCase.Address)
		}
		readRequest, err := builder.Build()
		if err != nil {
			t.Errorf("Error building request %v", err)
			return
		}

		// Execute the read request
		readResponseResult := <-readRequest.Execute()
		if readResponseResult.GetErr() != nil {
			t.Errorf("Error getting response %v", err)
			return
		}
		readResponse := readResponseResult.GetResponse()

		// Check the result
		assert.Equal(t, len(shuffledTestcases), len(readResponse.GetTagNames()))
		for _, testCase := range shuffledTestcases {
			tagName := testCase.Address
			assert.Equalf(t, apiModel.PlcResponseCode_OK, readResponse.GetResponseCode(tagName), "response code should be ok for %s", tagName)
			assert.NotNil(t, readResponse.GetValue(tagName))
			expectation := reflect.ValueOf(testCase.ExpectedReadValue)
			if readResponse.GetValue(tagName).IsList() && (expectation.Kind() == reflect.Slice || expectation.Kind() == reflect.Array) {
				plcList := readResponse.GetValue(tagName).GetList()
				for j := 0; j < expectation.Len(); j++ {
					var actual any
					actual = plcList[j]
					if testCase.UnwrappedValue {
						switch actualCasted := actual.(type) {
						case spiValues.PlcBOOL:
							actual = actualCasted.GetBool()
						case spiValues.PlcWORD:
							actual = actualCasted.GetInt8()
						default:
							t.Fatalf("%T not yet mapped", actualCasted)
						}
					}
					assert.Equal(t, expectation.Index(j).Interface(), actual, fmt.Sprintf("%s[%d]", tagName, j))
				}
			} else {
				assert.Equal(t, fmt.Sprint(testCase.ExpectedReadValue), readResponse.GetValue(tagName).GetString(), tagName)
			}
		}
	}
}
