// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package index

import (
	"errors"
	"testing"

	"github.com/hashicorp/consul/internal/controller/cache/index/indexmock"
	"github.com/hashicorp/consul/proto-public/pbresource"
	"github.com/hashicorp/consul/proto/private/prototest"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
)

var fakeTestError = errors.New("fake test error")

func TestTxn(t *testing.T) {
	suite.Run(t, new(txnSuite))
}

type txnSuite struct {
	suite.Suite

	indexer *indexmock.SingleIndexer
	index   *IndexedData

	r1   *pbresource.Resource
	r2   *pbresource.Resource
	r11  *pbresource.Resource
	r123 *pbresource.Resource
}

func (suite *txnSuite) SetupTest() {
	suite.indexer = indexmock.NewSingleIndexer(suite.T())
	suite.index = New("test", suite.indexer, IndexRequired).IndexedData()

	suite.r1 = testResource("r1")
	suite.r2 = testResource("r2")
	suite.r11 = testResource("r11")
	suite.r123 = testResource("r123")

	exp := suite.indexer.EXPECT()
	exp.FromResource(suite.r1).Return(true, PrefixIndexFromRefOrID(suite.r1.Id), nil).Once()
	exp.FromResource(suite.r2).Return(true, PrefixIndexFromRefOrID(suite.r2.Id), nil).Once()
	exp.FromResource(suite.r11).Return(true, PrefixIndexFromRefOrID(suite.r11.Id), nil).Once()
	exp.FromResource(suite.r123).Return(true, PrefixIndexFromRefOrID(suite.r123.Id), nil).Once()

	txn := suite.index.Txn()
	txn.Insert(suite.r1)
	txn.Insert(suite.r2)
	txn.Insert(suite.r11)
	txn.Insert(suite.r123)
	txn.Commit()
}

func (suite *txnSuite) TestGet() {
	suite.indexer.EXPECT().
		FromArgs(suite.r1.Id).
		RunAndReturn(PrefixReferenceOrIDFromArgs).
		Once()

	actual, err := suite.index.Txn().Get(suite.r1.Id)
	require.NoError(suite.T(), err)
	require.NotNil(suite.T(), actual)
	prototest.AssertDeepEqual(suite.T(), suite.r1, actual)
}

func (suite *txnSuite) TestGet_NotFound() {
	suite.indexer.EXPECT().
		FromArgs(suite.r1.Id).
		Return(nil, nil).
		Once()

	actual, err := suite.index.Txn().Get(suite.r1.Id)
	require.NoError(suite.T(), err)
	require.Nil(suite.T(), actual)
}

func (suite *txnSuite) TestGet_Error() {
	suite.indexer.EXPECT().
		FromArgs(suite.r1.Id).
		Return(nil, fakeTestError).
		Once()

	actual, err := suite.index.Txn().Get(suite.r1.Id)
	require.ErrorIs(suite.T(), err, fakeTestError)
	require.Nil(suite.T(), actual)
}

func (suite *txnSuite) TestListIterator() {
	refQuery := &pbresource.Reference{
		Type: testResourceType,
	}

	suite.indexer.EXPECT().
		FromArgs(refQuery).
		// Calculate a prefix based query for use with the ListIterator
		RunAndReturn(PrefixReferenceOrIDFromArgs).
		Once()

	iter, err := suite.index.Txn().ListIterator(refQuery)
	require.NoError(suite.T(), err)

	r := iter.Next()
	require.NotNil(suite.T(), r)
	prototest.AssertDeepEqual(suite.T(), suite.r1, r)

	r = iter.Next()
	require.NotNil(suite.T(), r)
	prototest.AssertDeepEqual(suite.T(), suite.r11, r)

	r = iter.Next()
	require.NotNil(suite.T(), r)
	prototest.AssertDeepEqual(suite.T(), suite.r123, r)

	r = iter.Next()
	require.NotNil(suite.T(), r)
	prototest.AssertDeepEqual(suite.T(), suite.r2, r)

	r = iter.Next()
	require.Nil(suite.T(), r)
}

func (suite *txnSuite) TestListIterator_Error() {
	suite.indexer.EXPECT().
		// abusing the mock to create a shortened index for us.
		FromArgs("sentinel").
		Return(nil, fakeTestError).
		Once()

	iter, err := suite.index.Txn().ListIterator("sentinel")
	require.ErrorIs(suite.T(), err, fakeTestError)
	require.Nil(suite.T(), iter)
}

func (suite *txnSuite) TestParentsIterator() {
	suite.indexer.EXPECT().
		// abusing the mock to create a shortened index for us.
		FromArgs(suite.r123.Id).
		RunAndReturn(ReferenceOrIDFromArgs).
		Once()

	iter, err := suite.index.Txn().ParentsIterator(suite.r123.Id)
	require.NoError(suite.T(), err)

	r := iter.Next()
	require.NotNil(suite.T(), r)
	prototest.AssertDeepEqual(suite.T(), suite.r1, r)

	r = iter.Next()
	require.NotNil(suite.T(), r)
	prototest.AssertDeepEqual(suite.T(), suite.r123, r)

	r = iter.Next()
	require.Nil(suite.T(), r)
}

func (suite *txnSuite) TestParentsIterator_Error() {
	suite.indexer.EXPECT().
		// abusing the mock to create a shortened index for us.
		FromArgs("sentinel").
		Return(nil, fakeTestError).
		Once()

	iter, err := suite.index.Txn().ParentsIterator("sentinel")
	require.ErrorIs(suite.T(), err, fakeTestError)
	require.Nil(suite.T(), iter)
}

func (suite *txnSuite) TestInsert_MissingRequiredIndex() {
	suite.indexer.EXPECT().
		FromResource(suite.r1).
		Return(false, nil, nil).
		Once()

	err := suite.index.Txn().Insert(suite.r1)
	require.Error(suite.T(), err)
	require.ErrorIs(suite.T(), err, MissingRequiredIndexError{Name: "test"})
}

func (suite *txnSuite) TestInsert_IndexError() {
	suite.indexer.EXPECT().
		FromResource(suite.r1).
		Return(false, nil, fakeTestError).
		Once()

	err := suite.index.Txn().Insert(suite.r1)
	require.Error(suite.T(), err)
	require.ErrorIs(suite.T(), err, fakeTestError)
}

func (suite *txnSuite) TestInsert_UpdateInternalSlice() {
	// So if you look closely this is going to insert the newR
	// resource but is calculating the index value of the r1
	// resource. This is done to exercise the insertion functionality
	// where indexes are non-unique and at each leaf in the radix
	// tree we must keep a list of items.
	newR := testResource("newR")
	suite.indexer.EXPECT().
		FromResource(newR).
		Return(true, PrefixIndexFromRefOrID(suite.r1.Id), nil).
		Once()

	// here we are setting up the expecation for re-inserting r1
	newR1 := testResource("r1")
	suite.indexer.EXPECT().
		FromResource(newR1).
		Return(true, PrefixIndexFromRefOrID(newR1.Id), nil).
		Once()

	// Actually index the resource
	txn := suite.index.Txn()
	require.NoError(suite.T(), txn.Insert(newR))
	require.NoError(suite.T(), txn.Insert(newR1))
	txn.Commit()

	// No validate that the insertions worked correctly.
	suite.indexer.EXPECT().
		FromArgs(newR1.Id).
		RunAndReturn(PrefixReferenceOrIDFromArgs).
		Once()
	iter, err := suite.index.Txn().ListIterator(newR1.Id)
	require.NoError(suite.T(), err)

	var resources []*pbresource.Resource
	for r := iter.Next(); r != nil; r = iter.Next() {
		resources = append(resources, r)
	}

	prototest.AssertElementsMatch(suite.T(), []*pbresource.Resource{newR, newR1, suite.r11, suite.r123}, resources)
}

func (suite *txnSuite) TestDelete() {
	// expected to index the resource during deletion
	suite.indexer.EXPECT().
		FromResource(suite.r1).
		Return(true, PrefixIndexFromRefOrID(suite.r1.Id), nil).
		Once()

	// perform the deletion
	txn := suite.index.Txn()
	require.NoError(suite.T(), txn.Delete(suite.r1))
	txn.Commit()

	// expect to index the ID during the query
	suite.indexer.EXPECT().
		FromArgs(suite.r1.Id).
		RunAndReturn(PrefixReferenceOrIDFromArgs).
		Once()

	// ensure that the deletion worked
	res, err := suite.index.Txn().Get(suite.r1.Id)
	require.NoError(suite.T(), err)
	require.Nil(suite.T(), res)
}

func (suite *txnSuite) TestDelete_NotFound() {
	res := testResource("foo")
	suite.indexer.EXPECT().
		FromResource(res).
		Return(true, PrefixIndexFromRefOrID(res.Id), nil).
		Once()

	// attempt the deletion
	txn := suite.index.Txn()
	require.NoError(suite.T(), txn.Delete(res))
	txn.Commit()
}

func (suite *txnSuite) TestDelete_IdxPresentValNotFound() {
	// The index holds a radix tree that points to a slice of resources.
	// A slice is used to account for non-unique indexes. This test case
	// is meant to specifically exercise the case where the radix leaf
	// node exists but a resource with an equivalent ID is not present
	// in the slice.

	// Calculating the index from the r1 resource will ensure that a
	// radix leaf exists but since newR was never inserted this should
	// exercise the case where the resource is not found within the slice
	newR := testResource("newR")
	suite.indexer.EXPECT().
		FromResource(newR).
		Return(true, PrefixIndexFromRefOrID(suite.r1.Id), nil).
		Once()

	txn := suite.index.Txn()
	require.NoError(suite.T(), txn.Delete(newR))
	txn.Commit()
}

func (suite *txnSuite) TestDelete_SliceModifications() {
	commonIndex := []byte("fake\x00")

	injectResource := func(name string) *pbresource.Resource {
		r := testResource(name)
		suite.indexer.EXPECT().
			FromResource(r).
			Return(true, commonIndex, nil).
			Once()

		txn := suite.index.Txn()
		require.NoError(suite.T(), txn.Insert(r))
		txn.Commit()

		return r
	}

	fr1 := injectResource("fr1")
	fr2 := injectResource("fr2")
	fr3 := injectResource("fr3")
	fr4 := injectResource("fr4")
	fr5 := injectResource("fr5")

	txn := suite.index.Txn()

	// excercise deletion of the first slice element
	suite.indexer.EXPECT().
		FromResource(fr1).
		Return(true, commonIndex, nil).
		Once()

	require.NoError(suite.T(), txn.Delete(fr1))

	// excercise deletion of the last slice element
	suite.indexer.EXPECT().
		FromResource(fr5).
		Return(true, commonIndex, nil).
		Once()

	require.NoError(suite.T(), txn.Delete(fr5))

	// excercise deletion from the middle of the list
	suite.indexer.EXPECT().
		FromResource(fr3).
		Return(true, commonIndex, nil).
		Once()

	require.NoError(suite.T(), txn.Delete(fr3))

	txn.Commit()

	// no verify that only fr2 and fr4 exist
	suite.indexer.EXPECT().
		FromArgs(fr2.Id).
		Return(commonIndex, nil).
		Once()

	iter, err := suite.index.Txn().ListIterator(fr2.Id)
	require.NoError(suite.T(), err)
	require.NotNil(suite.T(), iter)

	var resources []*pbresource.Resource
	for r := iter.Next(); r != nil; r = iter.Next() {
		resources = append(resources, r)
	}

	prototest.AssertElementsMatch(suite.T(), []*pbresource.Resource{fr2, fr4}, resources)
}

func (suite *txnSuite) TestDelete_MissingRequiredIndex() {
	suite.indexer.EXPECT().
		FromResource(suite.r1).
		Return(false, nil, nil).
		Once()

	err := suite.index.Txn().Delete(suite.r1)
	require.Error(suite.T(), err)
	require.ErrorIs(suite.T(), err, MissingRequiredIndexError{Name: "test"})
}

func (suite *txnSuite) TestDelete_IndexError() {
	suite.indexer.EXPECT().
		FromResource(suite.r1).
		Return(false, nil, fakeTestError).
		Once()

	err := suite.index.Txn().Delete(suite.r1)
	require.Error(suite.T(), err)
	require.ErrorIs(suite.T(), err, fakeTestError)
}
