package state

import (
	"testing"

	"github.com/hashicorp/go-memdb"
	"github.com/stretchr/testify/require"
)

type indexerTestCase struct {
	read       indexValue
	write      indexValue
	prefix     []indexValue
	writeMulti indexValueMulti
	// extra test cases can be added if the indexer has special handling for
	// specific cases.
	extra []indexerTestCase
}

type indexValue struct {
	source   interface{}
	expected []byte
	// expectedIndexMissing indicates that this test case should not produce an
	// expected value. The indexer should report a required value was missing.
	// This field is only relevant for the writeIndex.
	expectedIndexMissing bool
}

type indexValueMulti struct {
	source   interface{}
	expected [][]byte
}

func TestNewDBSchema_Indexers(t *testing.T) {
	schema := newDBSchema()
	require.NoError(t, schema.Validate())

	var testcases = map[string]func() map[string]indexerTestCase{
		tableACLPolicies:     testIndexerTableACLPolicies,
		tableACLRoles:        testIndexerTableACLRoles,
		tableChecks:          testIndexerTableChecks,
		tableServices:        testIndexerTableServices,
		tableNodes:           testIndexerTableNodes,
		tableConfigEntries:   testIndexerTableConfigEntries,
		tableMeshTopology:    testIndexerTableMeshTopology,
		tableGatewayServices: testIndexerTableGatewayServices,
	}
	addEnterpriseIndexerTestCases(testcases)

	for _, table := range schema.Tables {
		if testcases[table.Name] == nil {
			continue
		}
		t.Run(table.Name, func(t *testing.T) {
			tableTCs := testcases[table.Name]()

			for _, index := range table.Indexes {
				t.Run(index.Name, func(t *testing.T) {
					indexer := index.Indexer
					tc, ok := tableTCs[index.Name]
					if !ok {
						t.Skip("TODO: missing test case")
					}
					tc.run(t, indexer)
				})
			}
		})
	}
}

func (tc indexerTestCase) run(t *testing.T, indexer memdb.Indexer) {
	args := []interface{}{tc.read.source}
	if s, ok := tc.read.source.([]interface{}); ok {
		// Indexes using memdb.CompoundIndex must be expanded to multiple args
		args = s
	}

	if tc.read.source != nil {
		t.Run("readIndex", func(t *testing.T) {
			actual, err := indexer.FromArgs(args...)
			require.NoError(t, err)
			require.Equal(t, tc.read.expected, actual)
		})
	}

	if i, ok := indexer.(memdb.SingleIndexer); ok {
		t.Run("writeIndex", func(t *testing.T) {
			valid, actual, err := i.FromObject(tc.write.source)
			require.NoError(t, err)
			if tc.write.expectedIndexMissing {
				require.False(t, valid, "expected the indexer to produce no index value")
			} else {
				require.True(t, valid, "indexer was missing a required value")
				require.Equal(t, tc.write.expected, actual)
			}
		})
	}

	if i, ok := indexer.(memdb.PrefixIndexer); ok {
		for _, c := range tc.prefix {
			t.Run("prefixIndex", func(t *testing.T) {
				actual, err := i.PrefixFromArgs(c.source)
				require.NoError(t, err)
				require.Equal(t, c.expected, actual)
			})
		}
	}

	if i, ok := indexer.(memdb.MultiIndexer); ok {
		valid, actual, err := i.FromObject(tc.writeMulti.source)
		require.NoError(t, err)
		require.True(t, valid)
		require.Equal(t, tc.writeMulti.expected, actual)
	}

	for _, extra := range tc.extra {
		t.Run("extra", func(t *testing.T) {
			extra.run(t, indexer)
		})
	}
}
