// Copyright 2021 The etcd Authors
//
// 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 backend_test

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"go.uber.org/zap/zaptest"

	"go.etcd.io/etcd/server/v3/storage/backend"
	betesting "go.etcd.io/etcd/server/v3/storage/backend/testing"
	"go.etcd.io/etcd/server/v3/storage/schema"
)

var (
	bucket = schema.Test
	key    = []byte("key")
)

func TestBackendPreCommitHook(t *testing.T) {
	be := newTestHooksBackend(t, backend.DefaultBackendConfig(zaptest.NewLogger(t)))

	tx := be.BatchTx()
	prepareBuckenAndKey(tx)
	tx.Commit()

	// Empty commit.
	tx.Commit()

	assert.Equalf(t, ">cc", getCommitsKey(t, be), "expected 2 explicit commits")
	tx.Commit()
	assert.Equalf(t, ">ccc", getCommitsKey(t, be), "expected 3 explicit commits")
}

func TestBackendAutoCommitLimitHook(t *testing.T) {
	cfg := backend.DefaultBackendConfig(zaptest.NewLogger(t))
	cfg.BatchLimit = 3
	be := newTestHooksBackend(t, cfg)

	tx := be.BatchTx()
	prepareBuckenAndKey(tx) // writes 2 entries.

	for i := 3; i <= 9; i++ {
		write(tx, []byte("i"), []byte{byte(i)})
	}

	assert.Equal(t, ">ccc", getCommitsKey(t, be))
}

func write(tx backend.BatchTx, k, v []byte) {
	tx.Lock()
	defer tx.Unlock()
	tx.UnsafePut(bucket, k, v)
}

func TestBackendAutoCommitBatchIntervalHook(t *testing.T) {
	ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
	defer cancel()

	cfg := backend.DefaultBackendConfig(zaptest.NewLogger(t))
	cfg.BatchInterval = 10 * time.Millisecond
	be := newTestHooksBackend(t, cfg)
	tx := be.BatchTx()
	prepareBuckenAndKey(tx)

	// Edits trigger an auto-commit
	waitUntil(ctx, t, func() bool { return getCommitsKey(t, be) == ">c" })

	time.Sleep(time.Second)
	// No additional auto-commits, as there were no more edits
	assert.Equal(t, ">c", getCommitsKey(t, be))

	write(tx, []byte("foo"), []byte("bar1"))

	waitUntil(ctx, t, func() bool { return getCommitsKey(t, be) == ">cc" })

	write(tx, []byte("foo"), []byte("bar1"))

	waitUntil(ctx, t, func() bool { return getCommitsKey(t, be) == ">ccc" })
}

func waitUntil(ctx context.Context, tb testing.TB, f func() bool) {
	for !f() {
		select {
		case <-ctx.Done():
			tb.Fatalf("Context cancelled/timedout without condition met: %v", ctx.Err())
		default:
		}
		time.Sleep(10 * time.Millisecond)
	}
}

func prepareBuckenAndKey(tx backend.BatchTx) {
	tx.Lock()
	defer tx.Unlock()
	tx.UnsafeCreateBucket(bucket)
	tx.UnsafePut(bucket, key, []byte(">"))
}

func newTestHooksBackend(tb testing.TB, baseConfig backend.BackendConfig) backend.Backend {
	cfg := baseConfig
	cfg.Hooks = backend.NewHooks(func(tx backend.UnsafeReadWriter) {
		k, v := tx.UnsafeRange(bucket, key, nil, 1)
		tb.Logf("OnPreCommit executed: %v %v", string(k[0]), string(v[0]))
		assert.Len(tb, k, 1)
		assert.Len(tb, v, 1)
		tx.UnsafePut(bucket, key, append(v[0], byte('c')))
	})

	be, _ := betesting.NewTmpBackendFromCfg(tb, cfg)
	tb.Cleanup(func() {
		betesting.Close(tb, be)
	})
	return be
}

func getCommitsKey(tb testing.TB, be backend.Backend) string {
	rtx := be.BatchTx()
	rtx.Lock()
	defer rtx.Unlock()
	_, v := rtx.UnsafeRange(bucket, key, nil, 1)
	assert.Len(tb, v, 1)
	return string(v[0])
}
