// Copyright 2019 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This file incorporates work covered by the following copyright and
// permission notice:
//
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0

package chunks

import (
	"context"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"

	"github.com/dolthub/dolt/go/store/constants"
	"github.com/dolthub/dolt/go/store/hash"
)

type ChunkStoreTestSuite struct {
	suite.Suite
	Factory *memoryStoreFactory
}

func noopGetAddrs(ctx context.Context, c Chunk) (hash.HashSet, error) {
	return nil, nil
}

func (suite *ChunkStoreTestSuite) TestChunkStorePut() {
	ctx := context.Background()
	store := suite.Factory.CreateStore(ctx, "ns")
	input := "abc"
	c := NewChunk([]byte(input))
	err := store.Put(ctx, c, noopGetAddrs)
	suite.NoError(err)
	h := c.Hash()

	// Reading it via the API should work.
	assertInputInStore(input, h, store, suite.Assert())

	// Put chunk with dangling ref should error on Commit
	data := []byte("bcd")
	nc := NewChunk(data)
	err = store.Put(ctx, nc, func(ctx context.Context, c Chunk) (hash.HashSet, error) {
		return hash.NewHashSet(hash.Of([]byte("nonsense"))), nil
	})
	suite.NoError(err)
	root, err := store.Root(ctx)
	suite.NoError(err)

	_, err = store.Commit(ctx, root, root)
	suite.Error(err)
}

func (suite *ChunkStoreTestSuite) TestChunkStoreRoot() {
	store := suite.Factory.CreateStore(context.Background(), "ns")
	oldRoot, err := store.Root(context.Background())
	suite.NoError(err)
	suite.True(oldRoot.IsEmpty())

	bogusRoot := hash.Parse("8habda5skfek1265pc5d5l1orptn5dr0")
	newRoot := hash.Parse("8la6qjbh81v85r6q67lqbfrkmpds14lg")

	// Try to update root with bogus oldRoot
	result, err := store.Commit(context.Background(), newRoot, bogusRoot)
	suite.NoError(err)
	suite.False(result)

	// Now do a valid root update
	result, err = store.Commit(context.Background(), newRoot, oldRoot)
	suite.NoError(err)
	suite.True(result)
}

func (suite *ChunkStoreTestSuite) TestChunkStoreCommitPut() {
	name := "ns"
	store := suite.Factory.CreateStore(context.Background(), name)
	input := "abc"
	c := NewChunk([]byte(input))
	err := store.Put(context.Background(), c, noopGetAddrs)
	suite.NoError(err)
	h := c.Hash()

	// Reading it via the API should work...
	assertInputInStore(input, h, store, suite.Assert())
	// ...but it shouldn't be persisted yet
	assertInputNotInStore(input, h, suite.Factory.CreateStore(context.Background(), name), suite.Assert())

	r, err := store.Root(context.Background())
	suite.NoError(err)
	_, err = store.Commit(context.Background(), h, r) // Commit persists Chunks
	suite.NoError(err)
	assertInputInStore(input, h, store, suite.Assert())
	assertInputInStore(input, h, suite.Factory.CreateStore(context.Background(), name), suite.Assert())
}

func (suite *ChunkStoreTestSuite) TestChunkStoreGetNonExisting() {
	store := suite.Factory.CreateStore(context.Background(), "ns")
	h := hash.Parse("11111111111111111111111111111111")
	c, err := store.Get(context.Background(), h)
	suite.NoError(err)
	suite.True(c.IsEmpty())
}

func (suite *ChunkStoreTestSuite) TestChunkStoreVersion() {
	store := suite.Factory.CreateStore(context.Background(), "ns")
	oldRoot, err := store.Root(context.Background())
	suite.NoError(err)
	suite.True(oldRoot.IsEmpty())
	newRoot := hash.Parse("11111222223333344444555556666677")
	success, err := store.Commit(context.Background(), newRoot, oldRoot)
	suite.NoError(err)
	suite.True(success)

	suite.Equal(constants.FormatLD1String, store.Version())
}

func (suite *ChunkStoreTestSuite) TestChunkStoreCommitUnchangedRoot() {
	store1, store2 := suite.Factory.CreateStore(context.Background(), "ns"), suite.Factory.CreateStore(context.Background(), "ns")
	input := "abc"
	c := NewChunk([]byte(input))
	err := store1.Put(context.Background(), c, noopGetAddrs)
	suite.NoError(err)
	h := c.Hash()

	// Reading c from store1 via the API should work...
	assertInputInStore(input, h, store1, suite.Assert())
	// ...but not store2.
	assertInputNotInStore(input, h, store2, suite.Assert())

	newRoot, err := store1.Root(context.Background())
	suite.NoError(err)
	oldRoot, err := store1.Root(context.Background())
	suite.NoError(err)
	_, err = store1.Commit(context.Background(), newRoot, oldRoot)
	suite.NoError(err)

	err = store2.Rebase(context.Background())
	suite.NoError(err)

	// Now, reading c from store2 via the API should work...
	assertInputInStore(input, h, store2, suite.Assert())
}

func assertInputInStore(input string, h hash.Hash, s ChunkStore, assert *assert.Assertions) {
	chunk, err := s.Get(context.Background(), h)
	assert.NoError(err)
	assert.False(chunk.IsEmpty(), "Shouldn't get empty chunk for %s", h.String())
	assert.Equal(input, string(chunk.Data()))
}

func assertInputNotInStore(input string, h hash.Hash, s ChunkStore, assert *assert.Assertions) {
	chunk, err := s.Get(context.Background(), h)
	assert.NoError(err)
	assert.True(chunk.IsEmpty(), "Shouldn't get non-empty chunk for %s: %v", h.String(), chunk)
}
