package collections

import (
	"bytes"
	"context"
	"io"
	"testing"

	"github.com/stretchr/testify/require"

	appmodule "cosmossdk.io/collections/corecompat"
)

func TestDefaultGenesis(t *testing.T) {
	f := initFixture(t)
	var writers []*bufCloser
	require.NoError(t, f.schema.DefaultGenesis(func(field string) (io.WriteCloser, error) {
		w := newBufCloser(t, "")
		writers = append(writers, w)
		return w, nil
	}))
	require.Len(t, writers, 4)
	require.Equal(t, `[]`, writers[0].String())
	require.Equal(t, `[]`, writers[1].String())
	require.Equal(t, `[]`, writers[2].String())
	require.Equal(t, `[]`, writers[3].String())
}

func TestValidateGenesis(t *testing.T) {
	f := initFixture(t)
	require.NoError(t, f.schema.ValidateGenesis(createTestGenesisSource(t)))
}

func TestImportGenesis(t *testing.T) {
	f := initFixture(t)
	require.NoError(t, f.schema.InitGenesis(f.ctx, createTestGenesisSource(t)))
	// assert map correct genesis
	mapIt, err := f.m.Iterate(f.ctx, nil)
	require.NoError(t, err)
	defer mapIt.Close()

	kvs, err := mapIt.KeyValues()
	require.NoError(t, err)
	require.Equal(t, KeyValue[string, uint64]{Key: "abc", Value: 1}, kvs[0])
	require.Equal(t, KeyValue[string, uint64]{Key: "def", Value: 2}, kvs[1])

	// assert item correct genesis
	x, err := f.i.Get(f.ctx)
	require.NoError(t, err)
	require.Equal(t, "superCoolItem", x)

	// assert keyset correct genesis
	ksIt, err := f.ks.Iterate(f.ctx, nil)
	require.NoError(t, err)
	defer ksIt.Close()

	keys, err := ksIt.Keys()
	require.NoError(t, err)
	require.Equal(t, []string{"0", "1", "2"}, keys)

	// assert sequence correct genesis
	seq, err := f.s.Peek(f.ctx)
	require.NoError(t, err)
	require.Equal(t, uint64(1000), seq)
}

func TestExportGenesis(t *testing.T) {
	f := initFixture(t)
	require.NoError(t, f.schema.InitGenesis(f.ctx, createTestGenesisSource(t)))

	var writers []*bufCloser
	require.NoError(t, f.schema.ExportGenesis(f.ctx, func(field string) (io.WriteCloser, error) {
		w := newBufCloser(t, "")
		writers = append(writers, w)
		return w, nil
	}))
	require.Len(t, writers, 4)
	require.Equal(t, expectedItemGenesis, writers[0].String())
	require.Equal(t, expectedKeySetGenesis, writers[1].String())
	require.Equal(t, expectedMapGenesis, writers[2].String())
	require.Equal(t, expectedSequenceGenesis, writers[3].String())
}

type testFixture struct {
	schema Schema
	ctx    context.Context
	m      Map[string, uint64]
	i      Item[string]
	s      Sequence
	ks     KeySet[string]
}

func initFixture(t *testing.T) *testFixture {
	t.Helper()
	sk, ctx := deps()
	schemaBuilder := NewSchemaBuilder(sk)
	m := NewMap(schemaBuilder, NewPrefix(1), "map", StringKey, Uint64Value)
	i := NewItem(schemaBuilder, NewPrefix(2), "item", StringValue)
	s := NewSequence(schemaBuilder, NewPrefix(3), "sequence")
	ks := NewKeySet(schemaBuilder, NewPrefix(4), "key_set", StringKey)
	schema, err := schemaBuilder.Build()
	require.NoError(t, err)
	return &testFixture{
		schema: schema,
		ctx:    ctx,
		m:      m,
		i:      i,
		s:      s,
		ks:     ks,
	}
}

func createTestGenesisSource(t *testing.T) appmodule.GenesisSource {
	t.Helper()
	expectedOrder := []string{"item", "key_set", "map", "sequence"}
	currentIndex := 0
	return func(field string) (io.ReadCloser, error) {
		require.Equal(t, expectedOrder[currentIndex], field, "unordered genesis")
		currentIndex++

		switch field {
		case "map":
			return newBufCloser(t, expectedMapGenesis), nil
		case "item":
			return newBufCloser(t, expectedItemGenesis), nil
		case "key_set":
			return newBufCloser(t, expectedKeySetGenesis), nil
		case "sequence":
			return newBufCloser(t, expectedSequenceGenesis), nil
		default:
			return nil, nil
		}
	}
}

const (
	expectedMapGenesis      = `[{"key":"abc","value":"1"},{"key":"def","value":"2"}]`
	expectedItemGenesis     = `[{"key":"item","value":"superCoolItem"}]`
	expectedKeySetGenesis   = `[{"key":"0"},{"key":"1"},{"key":"2"}]`
	expectedSequenceGenesis = `[{"key":"item","value":"1000"}]`
)

type bufCloser struct {
	*bytes.Buffer
	closed bool
}

func (b *bufCloser) Close() error {
	b.closed = true
	return nil
}

func newBufCloser(t *testing.T, str string) *bufCloser {
	t.Helper()
	b := &bufCloser{
		Buffer: bytes.NewBufferString(str),
		closed: false,
	}
	// this ensures Close was called by the implementation
	t.Cleanup(func() {
		require.True(t, b.closed)
	})
	return b
}
