package colltest

import (
	"encoding/json"
	"fmt"
	"reflect"
	"testing"

	"github.com/stretchr/testify/require"

	"cosmossdk.io/collections"
	"cosmossdk.io/collections/codec"
)

// TestKeyCodec asserts the correct behavior of a KeyCodec over the type T.
func TestKeyCodec[T any](t *testing.T, keyCodec codec.KeyCodec[T], key T) {
	buffer := make([]byte, keyCodec.Size(key))
	written, err := keyCodec.Encode(buffer, key)
	require.NoError(t, err)
	require.Equal(t, len(buffer), written, "the length of the buffer and the written bytes do not match")
	read, decodedKey, err := keyCodec.Decode(buffer)
	require.NoError(t, err)
	require.Equal(t, len(buffer), read, "encoded key and read bytes must have same size")
	require.Equal(t, key, decodedKey, "encoding and decoding produces different keys")
	// test if terminality is correctly applied
	pairCodec := collections.PairKeyCodec(keyCodec, collections.StringKey)
	pairKey := collections.Join(key, "TEST")
	buffer = make([]byte, pairCodec.Size(pairKey))
	written, err = pairCodec.Encode(buffer, pairKey)
	require.Equal(t, len(buffer), written, "the pair buffer should have been fully written")
	require.NoError(t, err)
	read, decodedPairKey, err := pairCodec.Decode(buffer)
	require.NoError(t, err)
	require.Equal(t, len(buffer), read, "encoded non terminal key and pair key read bytes must have same size")
	require.Equal(t, pairKey, decodedPairKey, "encoding and decoding produces different keys with non terminal encoding")

	// check JSON
	keyJSON, err := keyCodec.EncodeJSON(key)
	require.NoError(t, err)
	decoded, err := keyCodec.DecodeJSON(keyJSON)
	require.NoError(t, err)
	require.Equal(t, key, decoded, "json encoding and decoding did not produce the same results")

	// check type
	require.NotEmpty(t, keyCodec.KeyType())
	// check string
	_ = keyCodec.Stringify(key)
}

// TestValueCodec asserts the correct behavior of a ValueCodec over the type T.
func TestValueCodec[T any](t *testing.T, encoder codec.ValueCodec[T], value T) {
	encodedValue, err := encoder.Encode(value)
	require.NoError(t, err)
	decodedValue, err := encoder.Decode(encodedValue)
	require.NoError(t, err)
	require.Equal(t, value, decodedValue, "encoding and decoding produces different values")

	encodedJSONValue, err := encoder.EncodeJSON(value)
	require.NoError(t, err)
	decodedJSONValue, err := encoder.DecodeJSON(encodedJSONValue)
	require.NoError(t, err)
	require.Equal(t, value, decodedJSONValue, "encoding and decoding in json format produces different values")

	require.NotEmpty(t, encoder.ValueType())

	_ = encoder.Stringify(value)
}

// MockValueCodec returns a mock of collections.ValueCodec for type T, it
// can be used for collections Values testing. It also supports interfaces.
// For the interfaces cases, in order for an interface to be decoded it must
// have been encoded first. Not concurrency safe.
// EG:
// Let's say the value is interface Animal
// if I want to decode Dog which implements Animal, then I need to first encode
// it in order to make the type known by the MockValueCodec.
func MockValueCodec[T any]() codec.ValueCodec[T] {
	typ := reflect.ValueOf(new(T)).Elem().Type()
	isInterface := typ.Kind() == reflect.Interface

	return &mockValueCodec[T]{
		isInterface: isInterface,
		seenTypes:   map[string]reflect.Type{},
		valueType:   fmt.Sprintf("%s.%s", typ.PkgPath(), typ.Name()),
	}
}

type mockValueJSON struct {
	TypeName string          `json:"type_name"`
	Value    json.RawMessage `json:"value"`
}

type mockValueCodec[T any] struct {
	isInterface bool
	seenTypes   map[string]reflect.Type
	valueType   string
}

func (m mockValueCodec[T]) Encode(value T) ([]byte, error) {
	typeName := m.getTypeName(value)
	valueBytes, err := json.Marshal(value)
	if err != nil {
		return nil, err
	}

	return json.Marshal(mockValueJSON{
		TypeName: typeName,
		Value:    valueBytes,
	})
}

func (m mockValueCodec[T]) Decode(b []byte) (t T, err error) {
	wrappedValue := mockValueJSON{}
	err = json.Unmarshal(b, &wrappedValue)
	if err != nil {
		return t, err
	}
	if !m.isInterface {
		err = json.Unmarshal(wrappedValue.Value, &t)
		return t, err
	}

	typ, exists := m.seenTypes[wrappedValue.TypeName]
	if !exists {
		return t, fmt.Errorf("unknown type %s, you're dealing with interfaces... in order to make the interface types known for the MockValueCodec, you need to first encode them", wrappedValue.TypeName)
	}

	newT := reflect.New(typ).Interface()
	err = json.Unmarshal(wrappedValue.Value, newT)
	if err != nil {
		return t, err
	}

	iface := new(T)
	reflect.ValueOf(iface).Elem().Set(reflect.ValueOf(newT).Elem())
	return *iface, nil
}

func (m mockValueCodec[T]) EncodeJSON(value T) ([]byte, error) {
	return m.Encode(value)
}

func (m mockValueCodec[T]) DecodeJSON(b []byte) (T, error) {
	return m.Decode(b)
}

func (m mockValueCodec[T]) Stringify(value T) string {
	return fmt.Sprintf("%#v", value)
}

func (m mockValueCodec[T]) ValueType() string {
	return m.valueType
}

func (m mockValueCodec[T]) getTypeName(value T) string {
	if !m.isInterface {
		return m.valueType
	}
	typ := reflect.TypeOf(value)
	name := fmt.Sprintf("%s.%s", typ.PkgPath(), typ.Name())
	m.seenTypes[name] = typ
	return name
}
