package boltdb

import (
	"errors"
	"strconv"
	"testing"

	portainer "github.com/portainer/portainer/api"
	"github.com/portainer/portainer/api/dataservices"
	"github.com/stretchr/testify/require"
)

const testBucketName = "test-bucket"
const testId = 1234

type testStruct struct {
	Key   string
	Value string
}

func TestTxs(t *testing.T) {
	t.Parallel()
	conn := DbConnection{Path: t.TempDir()}

	err := conn.Open()
	require.NoError(t, err)
	t.Cleanup(func() {
		err := conn.Close()
		require.NoError(t, err)
	})

	// Error propagation
	err = conn.UpdateTx(func(tx portainer.Transaction) error {
		return errors.New("this is an error")
	})
	require.Error(t, err)

	// Create an object
	newObj := testStruct{Key: "key", Value: "value"}

	err = conn.UpdateTx(func(tx portainer.Transaction) error {
		if err := tx.SetServiceName(testBucketName); err != nil {
			return err
		}

		return tx.CreateObjectWithId(testBucketName, testId, newObj)
	})
	require.NoError(t, err)

	obj := testStruct{}
	err = conn.ViewTx(func(tx portainer.Transaction) error {
		return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
	})
	require.NoError(t, err)

	if obj.Key != newObj.Key || obj.Value != newObj.Value {
		t.Fatalf("expected %s:%s, got %s:%s instead", newObj.Key, newObj.Value, obj.Key, obj.Value)
	}

	// Update an object
	updatedObj := testStruct{Key: "updated-key", Value: "updated-value"}

	err = conn.UpdateTx(func(tx portainer.Transaction) error {
		return tx.UpdateObject(testBucketName, conn.ConvertToKey(testId), &updatedObj)
	})
	require.NoError(t, err)

	err = conn.ViewTx(func(tx portainer.Transaction) error {
		return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
	})
	require.NoError(t, err)

	if obj.Key != updatedObj.Key || obj.Value != updatedObj.Value {
		t.Fatalf("expected %s:%s, got %s:%s instead", updatedObj.Key, updatedObj.Value, obj.Key, obj.Value)
	}

	// Delete an object
	err = conn.UpdateTx(func(tx portainer.Transaction) error {
		return tx.DeleteObject(testBucketName, conn.ConvertToKey(testId))
	})
	require.NoError(t, err)

	err = conn.ViewTx(func(tx portainer.Transaction) error {
		return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
	})
	require.True(t, dataservices.IsErrObjectNotFound(err))

	// Get next identifier
	err = conn.UpdateTx(func(tx portainer.Transaction) error {
		id1 := tx.GetNextIdentifier(testBucketName)
		id2 := tx.GetNextIdentifier(testBucketName)

		if id1+1 != id2 {
			return errors.New("unexpected identifier sequence")
		}

		return nil
	})
	require.NoError(t, err)

	// Try to write in a read transaction
	err = conn.ViewTx(func(tx portainer.Transaction) error {
		return tx.CreateObjectWithId(testBucketName, testId, newObj)
	})
	require.Error(t, err)
}

func BenchmarkGetAll(b *testing.B) {
	const endpointBucket = "endpoints"
	const n = 10000

	conn := DbConnection{Path: b.TempDir()}

	err := conn.Open()
	require.NoError(b, err)
	b.Cleanup(func() {
		err := conn.Close()
		require.NoError(b, err)
	})

	err = conn.UpdateTx(func(tx portainer.Transaction) error {
		if err := tx.SetServiceName(endpointBucket); err != nil {
			return err
		}

		for i := 1; i <= n; i++ {
			ep := portainer.Endpoint{
				ID:              portainer.EndpointID(i),
				Name:            "env-" + strconv.Itoa(i),
				Type:            portainer.DockerEnvironment,
				URL:             "tcp://192.168.1." + strconv.Itoa(i%254+1) + ":2375",
				PublicURL:       "https://env-" + strconv.Itoa(i) + ".example.com",
				GroupID:         portainer.EndpointGroupID(i%10 + 1),
				TagIDs:          []portainer.TagID{portainer.TagID(i%5 + 1), portainer.TagID(i%3 + 1)},
				LastCheckInDate: int64(i) * 1000,
				EdgeID:          "edge-" + strconv.Itoa(i),
			}

			if err := tx.CreateObjectWithId(endpointBucket, i, &ep); err != nil {
				return err
			}
		}

		return nil
	})
	require.NoError(b, err)

	b.ResetTimer()
	b.ReportAllocs()

	for b.Loop() {
		var collection []portainer.Endpoint

		if err := conn.ViewTx(func(tx portainer.Transaction) error {
			return tx.GetAll(endpointBucket, new(portainer.Endpoint), dataservices.AppendFn(&collection))
		}); err != nil {
			b.Fatal(err)
		}
	}
}
