package engine

import (
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/event"
	startime "go.starlark.net/lib/time"
	"go.starlark.net/starlark"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/anypb"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func TestEventToDict_GeneralAndProto(t *testing.T) {
	dataMsg := &event.GitPushEvent{
		Project: &event.Project{
			Id:       123,
			FullPath: "a/b/c",
		},
	}
	a, err := anypb.New(dataMsg)
	require.NoError(t, err)
	ce := &event.CloudEvent{
		//Id:          "id123", // missing value
		Source: "src",
		//SpecVersion: "v1",
		//Type:        "com.example",
		Attributes: map[string]*event.CloudEvent_CloudEventAttributeValue{
			"bool": {Attr: &event.CloudEvent_CloudEventAttributeValue_CeBoolean{
				CeBoolean: true,
			}},
			"int": {Attr: &event.CloudEvent_CloudEventAttributeValue_CeInteger{
				CeInteger: 42,
			}},
			"string": {Attr: &event.CloudEvent_CloudEventAttributeValue_CeString{
				CeString: "42",
			}},
			"bytes": {Attr: &event.CloudEvent_CloudEventAttributeValue_CeBytes{
				CeBytes: []byte{4, 2},
			}},
			"timestamp": {Attr: &event.CloudEvent_CloudEventAttributeValue_CeTimestamp{
				CeTimestamp: &timestamppb.Timestamp{
					Seconds: 4,
					Nanos:   2,
				},
			}},
		},
		Data: &event.CloudEvent_ProtoData{
			ProtoData: a,
		},
	}

	val, err := EventToDict(ce)
	require.NoError(t, err)
	t.Log("actual", val)

	projectMsg := starlark.NewDict(0)
	setKey(t, projectMsg, "id", starlark.MakeInt64(dataMsg.Project.Id))
	setKey(t, projectMsg, "full_path", starlark.String(dataMsg.Project.FullPath))

	protoData := starlark.NewDict(0)
	setKey(t, protoData, "project", projectMsg)

	attrsMsg := starlark.NewDict(0)
	setKey(t, attrsMsg, "bool", attr(t, "ce_boolean", starlark.True))
	setKey(t, attrsMsg, "bytes", attr(t, "ce_bytes", starlark.Bytes([]byte{4, 2})))
	setKey(t, attrsMsg, "int", attr(t, "ce_integer", starlark.MakeInt64(42)))
	setKey(t, attrsMsg, "string", attr(t, "ce_string", starlark.String("42")))
	setKey(t, attrsMsg, "timestamp", attr(t, "ce_timestamp", startime.Time(time.Unix(4, 2).UTC())))

	expected := starlark.NewDict(0)
	setKey(t, expected, "source", starlark.String("src"))
	setKey(t, expected, "attributes", attrsMsg)
	setKey(t, expected, "data", protoData)
	t.Log("expected", expected)

	assertEqualAndSameOrder(t, expected, val)
}

func TestEventToDict_JSON(t *testing.T) {
	testcase := []*event.CloudEvent{
		{
			Id: "text",
			Attributes: map[string]*event.CloudEvent_CloudEventAttributeValue{
				attrDataContentType: {Attr: &event.CloudEvent_CloudEventAttributeValue_CeString{
					CeString: dataContentTypeJSON,
				}},
			},
			Data: &event.CloudEvent_TextData{
				TextData: `{"meaning": 42}`,
			},
		},
		{
			Id: "binary",
			Attributes: map[string]*event.CloudEvent_CloudEventAttributeValue{
				attrDataContentType: {Attr: &event.CloudEvent_CloudEventAttributeValue_CeString{
					CeString: dataContentTypeJSON,
				}},
			},
			Data: &event.CloudEvent_BinaryData{
				BinaryData: []byte(`{"meaning": 42}`),
			},
		},
	}

	for _, ce := range testcase {
		t.Run(ce.Id, func(t *testing.T) {
			val, err := EventToDict(ce)
			require.NoError(t, err)
			t.Log("actual", val)

			attrsMsg := starlark.NewDict(0)
			setKey(t, attrsMsg, attrDataContentType, attr(t, "ce_string", starlark.String(dataContentTypeJSON)))

			data := starlark.NewDict(0)
			setKey(t, data, "meaning", starlark.MakeInt64(42))

			expected := starlark.NewDict(0)
			setKey(t, expected, "id", starlark.String(ce.Id))
			setKey(t, expected, "attributes", attrsMsg)
			setKey(t, expected, "data", data)
			t.Log("expected", expected)

			assertEqualAndSameOrder(t, expected, val)
		})
	}
}

func TestProtoMessageToStarlarkValue_TopLevelWKT(t *testing.T) {
	dataMsg := &event.GitPushEvent{
		Project: &event.Project{
			Id:       123,
			FullPath: "a/b/c",
		},
	}
	a, err := anypb.New(dataMsg)
	require.NoError(t, err)

	projectMsg := attr(t, "id", starlark.MakeInt64(dataMsg.Project.Id))
	setKey(t, projectMsg, "full_path", starlark.String(dataMsg.Project.FullPath))
	dataMsgDict := attr(t, "project", projectMsg)

	tests := []struct {
		input         proto.Message
		expectedValue starlark.Value
	}{
		{
			input: &timestamppb.Timestamp{
				Seconds: 4,
				Nanos:   2,
			},
			expectedValue: startime.Time(time.Unix(4, 2).UTC()),
		},
		{
			input:         a,
			expectedValue: dataMsgDict,
		},
	}
	for _, tc := range tests {
		t.Run(fmt.Sprintf("%T", tc.input), func(t *testing.T) {
			val, err := protoMessageToStarlarkValue(tc.input)
			require.NoError(t, err)
			assertEqualAndSameOrder(t, tc.expectedValue, val)
		})
	}
}

func setKey(t *testing.T, d *starlark.Dict, key string, val starlark.Value) {
	require.NoError(t, d.SetKey(starlark.String(key), val))
}

func attr(t *testing.T, key string, val starlark.Value) *starlark.Dict {
	d := starlark.NewDict(1)
	setKey(t, d, key, val)
	return d
}

func assertEqualAndSameOrder(t *testing.T, expected, actual starlark.Value) bool {
	expI, ok1 := expected.(starlark.IterableMapping)
	actI, ok2 := actual.(starlark.IterableMapping)
	if ok1 && ok2 {
		expItems := expI.Items()
		actItems := actI.Items()
		if !assert.Len(t, actItems, len(expItems)) {
			return false
		}
		for i, ai := range actItems {
			ei := expItems[i]
			if !assertEqual(t, ei[0], ai[0]) {
				return false
			}
			if !assertEqualAndSameOrder(t, ei[1], ai[1]) {
				return false
			}
		}
		return true
	}
	return assertEqual(t, expected, actual)
}

func assertEqual(t *testing.T, expected, actual starlark.Value) bool {
	eq, err := starlark.Equal(expected, actual)
	require.NoError(t, err)
	return assert.True(t, eq, "expected: %s\nactual: %s", expected, actual)
}
