package e2e

import (
	"context"
	"fmt"
	"sort"
	"testing"

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

type ServicesE2ETest struct {
	suite.Suite

	dbConn *postgres.PostgresDB
}

type serviceFetchError interface {
	ServiceNotFound() bool
}

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

func (suite *ServicesE2ETest) TearDownTest() {
	writer := suite.dbConn.WriterConn()

	_, err := writer.Exec(fmt.Sprintf("DELETE FROM %s;", postgres.AccountsTableName))
	suite.NoError(err)
	_, err = writer.Exec(fmt.Sprintf("DELETE FROM %s;", postgres.IAMRolesTableName))
	suite.NoError(err)
	_, err = writer.Exec(fmt.Sprintf("DELETE FROM %s;", postgres.AuditLogsTableName))
	suite.NoError(err)
	_, err = writer.Exec(fmt.Sprintf("DELETE FROM %s;", postgres.ServicesTableName))
	suite.NoError(err)

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

func (suite *ServicesE2ETest) TestServicesE2E() {
	t := suite.T()
	ctx := context.Background()

	t.Run("ServiceCreate", func(t *testing.T) {
		t.Run("populates service and returns correct id", func(t *testing.T) {
			expectedName := "service create test"
			serviceID, err := suite.dbConn.ServiceCreate(ctx, &db.Service{
				Name:             expectedName,
				ServiceCatalogID: "123",
			})
			suite.NoError(err)

			service, err := suite.dbConn.ServiceByID(ctx, serviceID)
			suite.NoError(err)
			suite.Equal(service.Name, expectedName)
		})
	})

	t.Run("ServiceUpdate", func(t *testing.T) {
		t.Run("Updates service data", func(t *testing.T) {
			expectedName := "updated name"
			serviceID, err := suite.dbConn.ServiceCreate(ctx, &db.Service{
				Name:             "original name",
				ServiceCatalogID: "000",
			})
			suite.NoError(err)

			_, err = suite.dbConn.ServiceUpdate(ctx, serviceID, &db.ServiceEditable{
				Name:             expectedName,
				ServiceCatalogID: "000",
			})
			suite.NoError(err)

			service, err := suite.dbConn.ServiceByID(ctx, serviceID)
			suite.NoError(err)

			suite.Equal(service.Name, expectedName)
		})

	})

	t.Run("ServiceDelete", func(t *testing.T) {
		t.Run("Deletes a service", func(t *testing.T) {
			expectedName := "service delete test"
			serviceID, err := suite.dbConn.ServiceCreate(ctx, &db.Service{
				Name:             expectedName,
				ServiceCatalogID: "678",
			})
			suite.NoError(err)

			err = suite.dbConn.ServiceDelete(ctx, serviceID)
			suite.NoError(err)

			_, err = suite.dbConn.ServiceByID(ctx, serviceID)
			suite.True(err.(serviceFetchError).ServiceNotFound())
		})

		t.Run("Errors when service to delete not present", func(t *testing.T) {
			err := suite.dbConn.ServiceDelete(ctx, -1)
			suite.Equal(err, db.ErrServiceNotFound)
		})
	})

	t.Run("Services", func(t *testing.T) {
		t.Run("Retrieves all services", func(t *testing.T) {
			// Clear db before this test - given we're pulling all services other tests will contaminate.
			writer := suite.dbConn.WriterConn()
			_, err := writer.Exec(fmt.Sprintf("DELETE FROM %s;", postgres.AccountsTableName))
			suite.NoError(err)
			_, err = writer.Exec(fmt.Sprintf("DELETE FROM %s;", postgres.AuditLogsTableName))
			suite.NoError(err)
			_, err = writer.Exec(fmt.Sprintf("DELETE FROM %s;", postgres.ServicesTableName))
			suite.NoError(err)

			_, err = suite.dbConn.ServiceCreate(ctx, &db.Service{
				Name:             "service1",
				ServiceCatalogID: "111",
			})
			suite.NoError(err)
			_, err = suite.dbConn.ServiceCreate(ctx, &db.Service{
				Name:             "service2",
				ServiceCatalogID: "222",
			})
			suite.NoError(err)

			services, err := suite.dbConn.Services(ctx)
			suite.NoError(err)
			suite.Equal(len(services), 2)

			serviceNames := []string{services[0].Name, services[1].Name}
			sort.Slice(serviceNames, func(i, j int) bool {
				return serviceNames[i] < serviceNames[j]
			})
			suite.Equal(serviceNames, []string{"service1", "service2"})
		})
	})

	t.Run("ServiceById", func(t *testing.T) {
		t.Run("Gets the service with a given id", func(t *testing.T) {
			expectedName := "expected name"
			serviceID, err := suite.dbConn.ServiceCreate(ctx, &db.Service{
				Name:             expectedName,
				ServiceCatalogID: "333",
			})
			suite.NoError(err)

			_, err = suite.dbConn.ServiceCreate(ctx, &db.Service{
				Name:             "wrong name",
				ServiceCatalogID: "444",
			})
			suite.NoError(err)

			service, err := suite.dbConn.ServiceByID(ctx, serviceID)
			suite.NoError(err)
			suite.Equal(service.Name, expectedName)
		})

		t.Run("Errors when no service with provided id", func(t *testing.T) {
			_, err := suite.dbConn.ServiceByID(ctx, -1)
			suite.True(errors.Cause(err).(serviceFetchError).ServiceNotFound())
		})
	})

	t.Run("ServiceByLDAPGroup", func(t *testing.T) {
		t.Run("Gets all services with a given LDAPGroup", func(t *testing.T) {
			commonLDAPGroup := "common ldap group"
			_, err := suite.dbConn.ServiceCreate(ctx, &db.Service{
				Name:             "expected name 1",
				LDAPGroup:        commonLDAPGroup,
				ServiceCatalogID: "666",
			})
			suite.NoError(err)
			_, err = suite.dbConn.ServiceCreate(ctx, &db.Service{
				Name:             "expected name 2",
				LDAPGroup:        commonLDAPGroup,
				ServiceCatalogID: "777",
			})
			suite.NoError(err)

			_, err = suite.dbConn.ServiceCreate(ctx, &db.Service{
				Name:             "unexpected name",
				LDAPGroup:        "other ldap group",
				ServiceCatalogID: "888",
			})
			suite.NoError(err)

			services, err := suite.dbConn.ServicesByLDAPGroup(ctx, commonLDAPGroup)
			suite.NoError(err)

			suite.Equal(len(services), 2)
			suite.Equal(services[0].LDAPGroup, commonLDAPGroup)
			suite.Equal(services[1].LDAPGroup, commonLDAPGroup)

			serviceNames := []string{services[0].Name, services[1].Name}
			sort.Slice(serviceNames, func(i, j int) bool {
				return serviceNames[i] < serviceNames[j]
			})
			suite.Equal([]string{"expected name 1", "expected name 2"}, serviceNames)
		})

		t.Run("returns empty slice when no services with provided LDAPGroup", func(t *testing.T) {
			services, err := suite.dbConn.ServicesByLDAPGroup(ctx, "unknown ldap group")
			suite.NoError(err)
			suite.Equal(0, len(services))
		})
	})
}

func TestServicesE2E(t *testing.T) {
	suite.Run(t, &ServicesE2ETest{})
}
