// Copyright 2022 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.

package prolly

import (
	"context"
	"errors"
	"fmt"
	"io"
	"testing"

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

	"github.com/dolthub/dolt/go/gen/fb/serial"
	"github.com/dolthub/dolt/go/store/hash"
	"github.com/dolthub/dolt/go/store/prolly/message"
	"github.com/dolthub/dolt/go/store/prolly/tree"
	"github.com/dolthub/dolt/go/store/types"
)

func TestCommitClosure(t *testing.T) {
	ns := tree.NewTestNodeStore()
	ctx := context.Background()

	t.Run("Keys", func(t *testing.T) {
		k0 := NewCommitClosureKey(ns.Pool(), 0, hash.Parse("00000000000000000000000000000000"))
		assert.Equal(t, uint64(0), k0.Height())
		assert.True(t, k0.Addr().Equal(hash.Hash{}))

		k0_ := NewCommitClosureKey(ns.Pool(), 0, hash.Parse("00000000000000000000000000000000"))
		assert.False(t, k0.Less(k0_))

		h := hash.Parse("00000000000000000000000000000001")
		k0_1 := NewCommitClosureKey(ns.Pool(), 0, h)
		assert.True(t, k0_1.Addr().Equal(h))
		assert.True(t, k0.Less(k0_1))
		assert.False(t, k0_1.Less(k0_))

	})

	t.Run("Empty", func(t *testing.T) {
		cc, err := NewEmptyCommitClosure(ns)
		require.NoError(t, err)
		assert.NotNil(t, cc)
		c, err := cc.Count()
		require.NoError(t, err)
		assert.Equal(t, 0, c)
		assert.Equal(t, 0, cc.closure.Root.Count())
		assert.Equal(t, 1, cc.Height())

		i, err := cc.IterAllReverse(ctx)
		_, _, err = i.Next(ctx)
		assert.Error(t, err)
		assert.True(t, errors.Is(err, io.EOF))
	})

	t.Run("Insert", func(t *testing.T) {
		cc, err := NewEmptyCommitClosure(ns)
		require.NoError(t, err)
		e := cc.Editor()
		err = e.Add(ctx, NewCommitClosureKey(ns.Pool(), 0, hash.Parse("00000000000000000000000000000000")))
		assert.NoError(t, err)
		err = e.Add(ctx, NewCommitClosureKey(ns.Pool(), 1, hash.Parse("00000000000000000000000000000000")))
		assert.NoError(t, err)
		cc, err = e.Flush(ctx)
		assert.NoError(t, err)
		ccc, err := cc.Count()
		require.NoError(t, err)
		assert.Equal(t, 2, ccc)

		i, err := cc.IterAllReverse(ctx)
		assert.NoError(t, err)
		k, _, err := i.Next(ctx)
		assert.NoError(t, err)
		assert.Equal(t, uint64(1), k.Height())
		k, _, err = i.Next(ctx)
		assert.NoError(t, err)
		assert.Equal(t, uint64(0), k.Height())
		_, _, err = i.Next(ctx)
		assert.Error(t, err)
		assert.True(t, errors.Is(err, io.EOF))

		e = cc.Editor()
		err = e.Add(ctx, NewCommitClosureKey(ns.Pool(), 0, hash.Parse("00000000000000000000000000000000")))
		assert.NoError(t, err)
		err = e.Add(ctx, NewCommitClosureKey(ns.Pool(), 1, hash.Parse("00000000000000000000000000000000")))
		assert.NoError(t, err)
		cc, err = e.Flush(ctx)
		assert.NoError(t, err)
		ccc, err = cc.Count()
		require.NoError(t, err)
		assert.Equal(t, 2, ccc)
	})

	t.Run("Diff", func(t *testing.T) {
		ccl, err := NewEmptyCommitClosure(ns)
		require.NoError(t, err)
		e := ccl.Editor()
		err = e.Add(ctx, NewCommitClosureKey(ns.Pool(), 0, hash.Parse("00000000000000000000000000000000")))
		assert.NoError(t, err)
		err = e.Add(ctx, NewCommitClosureKey(ns.Pool(), 1, hash.Parse("00000000000000000000000000000000")))
		assert.NoError(t, err)
		ccl, err = e.Flush(ctx)
		assert.NoError(t, err)
		cclc, err := ccl.Count()
		require.NoError(t, err)
		assert.Equal(t, 2, cclc)

		ccr, err := NewEmptyCommitClosure(ns)
		require.NoError(t, err)
		e = ccr.Editor()
		err = e.Add(ctx, NewCommitClosureKey(ns.Pool(), 0, hash.Parse("00000000000000000000000000000000")))
		assert.NoError(t, err)
		err = e.Add(ctx, NewCommitClosureKey(ns.Pool(), 1, hash.Parse("00000000000000000000000000000000")))
		assert.NoError(t, err)
		err = e.Add(ctx, NewCommitClosureKey(ns.Pool(), 1, hash.Parse("00000000000000000000000000000001")))
		assert.NoError(t, err)
		err = e.Add(ctx, NewCommitClosureKey(ns.Pool(), 2, hash.Parse("00000000000000000000000000000000")))
		assert.NoError(t, err)
		ccr, err = e.Flush(ctx)
		assert.NoError(t, err)
		ccrc, err := ccr.Count()
		require.NoError(t, err)
		assert.Equal(t, 4, ccrc)

		var numadds, numdels int
		err = DiffCommitClosures(ctx, ccl, ccr, func(ctx context.Context, d tree.Diff) error {
			if d.Type == tree.AddedDiff {
				numadds++
			} else if d.Type == tree.RemovedDiff {
				numdels++
			}
			return nil
		})
		assert.Error(t, err)
		assert.True(t, errors.Is(err, io.EOF))
		assert.Equal(t, 2, numadds)
		assert.Equal(t, 0, numdels)
	})

	t.Run("WalkAddresses", func(t *testing.T) {
		cc, err := NewEmptyCommitClosure(ns)
		require.NoError(t, err)
		e := cc.Editor()
		for i := 0; i < 4096; i++ {
			err := e.Add(ctx, NewCommitClosureKey(ns.Pool(), uint64(i), hash.Parse(fmt.Sprintf("%0.32d", i))))
			require.NoError(t, err)
		}
		cc, err = e.Flush(ctx)
		require.NoError(t, err)
		ccc, err := cc.Count()
		require.NoError(t, err)
		assert.Equal(t, 4096, ccc)

		// Walk the addresses in the Root.
		msg := serial.Message(tree.ValueFromNode(cc.closure.Root).(types.SerialMessage))
		numaddresses := 0
		err = message.WalkAddresses(ctx, msg, func(ctx context.Context, addr hash.Hash) error {
			numaddresses++
			return nil
		})
		require.NoError(t, err)
		assert.Less(t, numaddresses, 4096)

		// Walk all addresses in the Tree.
		numaddresses = 0
		err = tree.WalkAddresses(ctx, cc.closure.Root, ns, func(ctx context.Context, addr hash.Hash) error {
			numaddresses++
			return nil
		})
		require.NoError(t, err)
		assert.Less(t, 4096, numaddresses)
	})

	t.Run("WalkNodes", func(t *testing.T) {
		cc, err := NewEmptyCommitClosure(ns)
		require.NoError(t, err)
		e := cc.Editor()
		for i := 0; i < 4096; i++ {
			err := e.Add(ctx, NewCommitClosureKey(ns.Pool(), uint64(i), hash.Parse(fmt.Sprintf("%0.32d", i))))
			require.NoError(t, err)
		}
		cc, err = e.Flush(ctx)
		require.NoError(t, err)
		ccc, err := cc.Count()
		require.NoError(t, err)
		assert.Equal(t, 4096, ccc)

		numnodes := 0
		totalentries := 0
		err = tree.WalkNodes(ctx, cc.closure.Root, ns, func(ctx context.Context, node tree.Node) error {
			numnodes++
			totalentries += node.Count()
			return nil
		})
		require.NoError(t, err)
		assert.Less(t, cc.closure.Root.Count(), numnodes)
		assert.Less(t, 4096, totalentries)
	})
}
