package dax_test

import (
	"context"
	"testing"
	"time"

	"github.com/featurebasedb/featurebase/v3/dax"
	"github.com/featurebasedb/featurebase/v3/dax/controller/sqldb"
	"github.com/featurebasedb/featurebase/v3/logger"
	"github.com/stretchr/testify/require"
)

func TestTransaction(t *testing.T) {
	t.Run("retryWithTx", func(t *testing.T) {
		ctx := context.Background()
		log := logger.StderrLogger
		trans, err := sqldb.NewTransactor(sqldb.GetTestConfigRandomDB("retry_with_tx"), log) // TODO running migrations takes kind of a long time, consolidate w/ other SQL tests
		require.NoError(t, err, "connecting")
		require.NoError(t, trans.Start())

		orgID1 := dax.OrganizationID("acme")
		db1 := &dax.Database{
			ID:   "db1id",
			Name: "db1",
			Options: dax.DatabaseOptions{
				WorkersMin: 1,
			},
		}
		qdb1 := dax.NewQualifiedDatabase(orgID1, db1)
		qdbid1 := qdb1.QualifiedID()
		schemar := sqldb.NewSchemar(log)

		// The purpose of this test is to ensure that we're enforcing repeatable
		// read isolation level on writes. It tests the RetryWithTx function by
		// retrying a write that fails and ensuring that it succeeds on the next
		// retry. It performs the following steps:
		//
		// tx1: create db1 with units 1
		// tx2: read db1 (units should be 1)
		//      wait... on chan "wait2"
		//      read db1 again (units should still be 1) << repeatableread
		//      set units to 2
		// tx3  set units to 3
		// tx4  read db1 (units should be 3)
		// close "wait2"
		// tx5  read db1 (units should be 2)

		wait2 := make(chan struct{})
		wait3 := make(chan struct{})
		done := make(chan struct{})

		// tx1
		tx1 := func(tx dax.Transaction, writable bool) error {
			dt, ok := tx.(*sqldb.DaxTransaction)
			require.True(t, ok)

			require.NoError(t, schemar.CreateDatabase(dt, qdb1))

			return nil
		}
		require.NoError(t, dax.RetryWithTx(ctx, trans, tx1, true, 1))

		// tx2

		// tx2cnt tracks the number of times that tx2 has been called. We need
		// this because we expect it to read different values depending on which
		// call it's on. And we only want it to close channels the first time
		// through.
		var tx2cnt int

		// exp contains the values that we expect tx2 to read (for
		// "workers-min") on the respective call.
		exp := map[int]int{
			0: 1,
			1: 3,
		}
		tx2 := func(tx dax.Transaction, writable bool) error {
			dt, ok := tx.(*sqldb.DaxTransaction)
			require.True(t, ok)

			qdb, err := schemar.DatabaseByID(dt, qdbid1)
			require.NoError(t, err)
			require.Equal(t, exp[tx2cnt], qdb.Options.WorkersMin)
			if tx2cnt == 0 {
				close(wait3)
			}

			// Wait until tx3 commits before trying to do anything else.
			<-wait2

			qdb, err = schemar.DatabaseByID(dt, qdbid1)
			require.NoError(t, err)
			require.Equal(t, exp[tx2cnt], qdb.Options.WorkersMin)

			// Increment tx2cnt for the next time tx2 gets called.
			tx2cnt++

			return schemar.SetDatabaseOption(dt, qdbid1, dax.DatabaseOptionWorkersMin, "2")
		}

		// Run the calls to tx2 in a go routine because we want to mimic
		// concurrent attempt to read/write the same data.
		go func() {
			require.NoError(t, dax.RetryWithTx(ctx, trans, tx2, true, 2))

			// After the second call of tx2 completes, close the done channel
			// so that tx5 can proceed and verify that tx2 eventually got to
			// commit its transaction.
			close(done)
		}()

		// Wait until tx2 does its first read of the data before allowing tx3 to
		// begin.
		select {
		case <-wait3:
		case <-time.After(10 * time.Second):
			t.Fatal("expected close of channel: wait3")
		}

		// tx3
		tx3 := func(tx dax.Transaction, writable bool) error {
			dt, ok := tx.(*sqldb.DaxTransaction)
			require.True(t, ok)

			qdb, err := schemar.DatabaseByID(dt, qdb1.QualifiedID())
			require.NoError(t, err)
			require.Equal(t, 1, qdb.Options.WorkersMin)

			require.NoError(t, schemar.SetDatabaseOption(dt, qdbid1, dax.DatabaseOptionWorkersMin, "3"))

			return nil
		}

		require.NoError(t, dax.RetryWithTx(ctx, trans, tx3, true, 1))

		// tx4
		tx4 := func(tx dax.Transaction, writable bool) error {
			dt, ok := tx.(*sqldb.DaxTransaction)
			require.True(t, ok)

			qdb, err := schemar.DatabaseByID(dt, qdb1.QualifiedID())
			require.NoError(t, err)
			require.Equal(t, 3, qdb.Options.WorkersMin)

			return nil
		}

		require.NoError(t, dax.RetryWithTx(ctx, trans, tx4, false, 1))

		// Close wait2 so that tx2 can continue retrying transactions.
		close(wait2)

		select {
		case <-done:
		case <-time.After(10 * time.Second):
			t.Fatal("expected close of channel: done")
		}

		// tx5
		tx5 := func(tx dax.Transaction, writable bool) error {
			dt, ok := tx.(*sqldb.DaxTransaction)
			require.True(t, ok)

			qdb, err := schemar.DatabaseByID(dt, qdb1.QualifiedID())
			require.NoError(t, err)
			require.Equal(t, 2, qdb.Options.WorkersMin)

			return nil
		}

		require.NoError(t, dax.RetryWithTx(ctx, trans, tx5, false, 1))
	})

}
