package e2e

import (
	"context"
	"fmt"
	"testing"
	"time"

	"code.justin.tv/eventbus/controlplane/internal/db"
	"code.justin.tv/eventbus/controlplane/internal/db/postgres"
	"github.com/stretchr/testify/suite"
)

type LeasesE2ETest struct {
	suite.Suite

	dbConn *postgres.PostgresDB
}

func (suite *LeasesE2ETest) SetupTest() {
	conn, err := newLocalDB()
	suite.NoError(err, "no db connection created in LeasesE2ETest")
	suite.dbConn = conn
}

func (suite *LeasesE2ETest) TearDownTest() {
	writer := suite.dbConn.WriterConn()
	_, err := writer.Exec(fmt.Sprintf("DELETE FROM %s", postgres.EventStreamsTableName))
	suite.NoError(err)

	_, err = writer.Exec(fmt.Sprintf("DELETE FROM %s", postgres.LeasesTableName))
	suite.NoError(err)

	err = suite.dbConn.Close()
	suite.NoError(err)
}

func (suite *LeasesE2ETest) TestLeasesE2E() {
	t := suite.T()
	ctx := context.Background()
	t.Run("Acquire", func(t *testing.T) {
		t.Run("can acquire lease successfully", func(t *testing.T) {
			eventStreams, err := suite.dbConn.EventTypeAndStreamsCreate(ctx, &db.EventType{
				Name: "acquire test",
			}, []string{"local"})
			suite.NoError(err)

			_, _, err = suite.dbConn.Acquire(ctx, eventStreams[0].ID, postgres.EventStreamsTableName, 1*time.Second)
			suite.NoError(err)
		})

		t.Run("cannot acquire lease for a resource already in use", func(t *testing.T) {
			eventStreams, err := suite.dbConn.EventTypeAndStreamsCreate(ctx, &db.EventType{
				Name: "cannot acquire test",
			}, []string{"local"})
			suite.NoError(err)

			lease, _, err := suite.dbConn.Acquire(ctx, eventStreams[0].ID, postgres.EventStreamsTableName, 5*time.Second)
			suite.NoError(err)

			_, _, err = suite.dbConn.Acquire(ctx, eventStreams[0].ID, postgres.EventStreamsTableName, 1*time.Second)
			suite.Equal(err, db.ErrLeaseUnavailable)

			err = lease.Release()
			suite.NoError(err)
		})

		t.Run("can acquire leases after timeout", func(t *testing.T) {
			eventStreams, err := suite.dbConn.EventTypeAndStreamsCreate(ctx, &db.EventType{
				Name: "acquire timeout test",
			}, []string{"local"})
			suite.NoError(err)

			_, _, err = suite.dbConn.Acquire(ctx, eventStreams[0].ID, postgres.EventStreamsTableName, 1*time.Second)
			suite.NoError(err)

			time.Sleep(5 * time.Second)
			_, _, err = suite.dbConn.Acquire(ctx, eventStreams[0].ID, postgres.EventStreamsTableName, 1*time.Second)
			suite.NoError(err)
		})
	})

	t.Run("Release", func(t *testing.T) {
		eventStreams, err := suite.dbConn.EventTypeAndStreamsCreate(ctx, &db.EventType{
			Name: "release test",
		}, []string{"local"})
		suite.NoError(err)

		lease, _, err := suite.dbConn.Acquire(ctx, eventStreams[0].ID, postgres.EventStreamsTableName, 1*time.Second)
		suite.NoError(err)
		err = lease.Release()
		suite.NoError(err)
	})

	t.Run("Expires", func(t *testing.T) {
		eventStreams, err := suite.dbConn.EventTypeAndStreamsCreate(ctx, &db.EventType{
			Name: "expires test",
		}, []string{"local"})
		suite.NoError(err)

		startTime := time.Now()
		lease, _, err := suite.dbConn.Acquire(ctx, eventStreams[0].ID, postgres.EventStreamsTableName, 1*time.Second)
		suite.NoError(err)
		expiresAt := lease.Expires()
		suite.Equal(true, startTime.Before(expiresAt))
		suite.Equal(true, expiresAt.Before(startTime.Add(2*time.Second)))
	})

	t.Run("Expired", func(t *testing.T) {
		t.Run("Returns true for expired leases", func(t *testing.T) {
			eventStreams, err := suite.dbConn.EventTypeAndStreamsCreate(ctx, &db.EventType{
				Name: "expired lease",
			}, []string{"local"})
			suite.NoError(err)

			lease, _, err := suite.dbConn.Acquire(ctx, eventStreams[0].ID, postgres.EventStreamsTableName, 10*time.Millisecond)
			suite.NoError(err)
			time.Sleep(50 * time.Millisecond)
			suite.Equal(true, lease.Expired())
		})

		t.Run("Returns false for current leases", func(t *testing.T) {
			eventStreams, err := suite.dbConn.EventTypeAndStreamsCreate(ctx, &db.EventType{
				Name: "non-expired lease",
			}, []string{"local"})
			suite.NoError(err)

			lease, _, err := suite.dbConn.Acquire(ctx, eventStreams[0].ID, postgres.EventStreamsTableName, 500*time.Millisecond)
			suite.NoError(err)
			suite.Equal(false, lease.Expired())
		})
	})
}

func TestLeasesE2E(t *testing.T) {
	suite.Run(t, &LeasesE2ETest{})
}
