// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package protocol

import (
	"context"
	stderr "errors"
	"testing"

	"github.com/Azure/iot-operations-sdks/go/protocol"
	"github.com/Azure/iot-operations-sdks/go/protocol/errors"
	"github.com/stretchr/testify/require"
)

// Simple happy-path sanity check.
func TestCommand(t *testing.T) {
	ctx := context.Background()
	client, server, done := sessionClients(t)
	defer done()

	var listeners protocol.Listeners
	defer listeners.Close()

	enc := protocol.JSON[string]{}
	topic := "command/{ex:token}/suffix"
	value := "test"

	executor, err := protocol.NewCommandExecutor(
		app, server, enc, enc, topic,
		func(
			_ context.Context,
			cr *protocol.CommandRequest[string],
		) (*protocol.CommandResponse[string], error) {
			require.Equal(t, "application/json", cr.ContentType)
			require.Equal(t, byte(1), cr.PayloadFormat)
			return protocol.Respond(
				cr.Payload+cr.ClientID+cr.CorrelationData,
				protocol.WithMetadata(cr.TopicTokens),
			)
		},
		protocol.WithTopicNamespace("ns"),
	)
	require.NoError(t, err)
	listeners = append(listeners, executor)

	invoker, err := protocol.NewCommandInvoker(
		app, client, enc, enc, topic,
		protocol.WithResponseTopicSuffix("response/{executorId}"),
		protocol.WithTopicNamespace("ns"),
		protocol.WithTopicTokens{"token": "test"},
		protocol.WithTopicTokenNamespace("ex:"),
	)
	require.NoError(t, err)
	listeners = append(listeners, invoker)

	err = listeners.Start(ctx)
	require.NoError(t, err)

	res, err := invoker.Invoke(ctx, value,
		protocol.WithTopicTokens{"executorId": server.ID()},
	)
	require.NoError(t, err)

	expected := value + client.ID() + res.CorrelationData
	require.Equal(t, expected, res.Payload)
	require.Equal(t, map[string]string{"ex:token": "test"}, res.Metadata)
	require.Equal(t, map[string]string{
		"ex:token":   "test",
		"executorId": server.ID(),
	}, res.TopicTokens)
}

func TestCommandError(t *testing.T) {
	ctx := context.Background()
	client, server, done := sessionClients(t)
	defer done()

	var listeners protocol.Listeners
	defer listeners.Close()

	req := protocol.Empty{}
	res := protocol.JSON[string]{}
	topic := "command-error"

	executor, err := protocol.NewCommandExecutor(
		app, server, req, res, topic,
		func(
			context.Context,
			*protocol.CommandRequest[any],
		) (*protocol.CommandResponse[string], error) {
			return nil, stderr.New("unknown error")
		},
	)
	require.NoError(t, err)
	listeners = append(listeners, executor)

	invoker, err := protocol.NewCommandInvoker(
		app, client, req, res, topic,
		protocol.WithResponseTopicSuffix("response"),
	)
	require.NoError(t, err)
	listeners = append(listeners, invoker)

	err = listeners.Start(ctx)
	require.NoError(t, err)

	_, err = invoker.Invoke(ctx, nil)
	require.Error(t, err)
	require.Equal(t, err.Error(), "unknown error")
}

func TestCommandManualError(t *testing.T) {
	ctx := context.Background()
	client, server, done := sessionClients(t)
	defer done()

	var listeners protocol.Listeners
	defer listeners.Close()

	req := protocol.Empty{}
	res := protocol.JSON[string]{}
	topic := "command-manual-error"

	executor, err := protocol.NewCommandExecutor(
		app, server, req, res, topic,
		func(
			context.Context,
			*protocol.CommandRequest[any],
		) (*protocol.CommandResponse[string], error) {
			return protocol.Respond("", protocol.WithMetadata{
				"__stat":     "422",
				"__stMsg":    "application error",
				"__apErr":    "true",
				"__propName": "key",
				"__propVal":  "value",
			})
		},
	)
	require.NoError(t, err)
	listeners = append(listeners, executor)

	invoker, err := protocol.NewCommandInvoker(
		app, client, req, res, topic,
		protocol.WithResponseTopicSuffix("response"),
	)
	require.NoError(t, err)
	listeners = append(listeners, invoker)

	err = listeners.Start(ctx)
	require.NoError(t, err)

	_, err = invoker.Invoke(ctx, nil)
	require.Equal(t, err, &errors.Remote{
		Message: "application error",
		Kind: errors.UnknownError{
			PropertyName:  "key",
			PropertyValue: "value",
		},
	})
}
