package wrapper

import (
	"testing"

	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/nomad/client/serviceregistration"
	regMock "github.com/hashicorp/nomad/client/serviceregistration/mock"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/stretchr/testify/require"
)

func Test_NewHandlerWrapper(t *testing.T) {
	log := hclog.NewNullLogger()
	mockProvider := regMock.NewServiceRegistrationHandler(log)
	wrapper := NewHandlerWrapper(log, mockProvider, mockProvider)
	require.NotNil(t, wrapper)
	require.NotNil(t, wrapper.log)
	require.NotNil(t, wrapper.nomadServiceProvider)
	require.NotNil(t, wrapper.consulServiceProvider)
}

func TestHandlerWrapper_RegisterWorkload(t *testing.T) {
	testCases := []struct {
		testFn func(t *testing.T)
		name   string
	}{
		{
			testFn: func(t *testing.T) {

				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Call the function with no services and check that nothing is
				// registered.
				require.NoError(t, wrapper.RegisterWorkload(&serviceregistration.WorkloadServices{}))
				require.Len(t, consul.GetOps(), 0)
				require.Len(t, nomad.GetOps(), 0)
			},
			name: "zero services",
		},
		{
			testFn: func(t *testing.T) {

				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Generate a minimal workload with an unknown provider.
				workload := serviceregistration.WorkloadServices{
					Services: []*structs.Service{
						{
							Provider: "istio",
						},
					},
				}

				// Call register and ensure an error is returned along with
				// nothing registered in the providers.
				err := wrapper.RegisterWorkload(&workload)
				require.Error(t, err)
				require.Contains(t, err.Error(), "unknown service registration provider: \"istio\"")
				require.Len(t, consul.GetOps(), 0)
				require.Len(t, nomad.GetOps(), 0)

			},
			name: "unknown provider",
		},
		{
			testFn: func(t *testing.T) {

				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Generate a minimal workload with the nomad provider.
				workload := serviceregistration.WorkloadServices{
					Services: []*structs.Service{
						{
							Provider: structs.ServiceProviderNomad,
						},
					},
				}

				// Call register and ensure no error is returned along with the
				// correct operations.
				require.NoError(t, wrapper.RegisterWorkload(&workload))
				require.Len(t, consul.GetOps(), 0)
				require.Len(t, nomad.GetOps(), 1)

			},
			name: "nomad provider",
		},
		{
			testFn: func(t *testing.T) {

				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Generate a minimal workload with the consul provider.
				workload := serviceregistration.WorkloadServices{
					Services: []*structs.Service{
						{
							Provider: structs.ServiceProviderConsul,
						},
					},
				}

				// Call register and ensure no error is returned along with the
				// correct operations.
				require.NoError(t, wrapper.RegisterWorkload(&workload))
				require.Len(t, consul.GetOps(), 1)
				require.Len(t, nomad.GetOps(), 0)
			},
			name: "consul provider",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			tc.testFn(t)
		})
	}
}

func TestHandlerWrapper_RemoveWorkload(t *testing.T) {
	testCases := []struct {
		testFn func(t *testing.T)
		name   string
	}{
		{
			testFn: func(t *testing.T) {
				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Call the function with no services and check that consul is
				// defaulted to.
				wrapper.RemoveWorkload(&serviceregistration.WorkloadServices{})
				require.Len(t, consul.GetOps(), 1)
				require.Len(t, nomad.GetOps(), 0)
			},
			name: "zero services",
		},
		{
			testFn: func(t *testing.T) {

				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Generate a minimal workload with an unknown provider.
				workload := serviceregistration.WorkloadServices{
					Services: []*structs.Service{
						{
							Provider: "istio",
						},
					},
				}

				// Call remove and ensure nothing registered in the providers.
				wrapper.RemoveWorkload(&workload)
				require.Len(t, consul.GetOps(), 0)
				require.Len(t, nomad.GetOps(), 0)
			},
			name: "unknown provider",
		},
		{
			testFn: func(t *testing.T) {

				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Generate a minimal workload with the consul provider.
				workload := serviceregistration.WorkloadServices{
					Services: []*structs.Service{
						{
							Provider: structs.ServiceProviderConsul,
						},
					},
				}

				// Call remove and ensure the correct backend includes
				// operations.
				wrapper.RemoveWorkload(&workload)
				require.Len(t, consul.GetOps(), 1)
				require.Len(t, nomad.GetOps(), 0)
			},
			name: "consul provider",
		},
		{
			testFn: func(t *testing.T) {

				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Generate a minimal workload with the nomad provider.
				workload := serviceregistration.WorkloadServices{
					Services: []*structs.Service{
						{
							Provider: structs.ServiceProviderNomad,
						},
					},
				}

				// Call remove and ensure the correct backend includes
				// operations.
				wrapper.RemoveWorkload(&workload)
				require.Len(t, consul.GetOps(), 0)
				require.Len(t, nomad.GetOps(), 1)
			},
			name: "nomad provider",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			tc.testFn(t)
		})
	}
}

func TestHandlerWrapper_UpdateWorkload(t *testing.T) {
	testCases := []struct {
		testFn func(t *testing.T)
		name   string
	}{
		{
			testFn: func(t *testing.T) {

				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Call the function with no services and check that nothing is
				// registered in either mock backend.
				err := wrapper.UpdateWorkload(&serviceregistration.WorkloadServices{},
					&serviceregistration.WorkloadServices{})
				require.NoError(t, err)
				require.Len(t, consul.GetOps(), 0)
				require.Len(t, nomad.GetOps(), 0)

			},
			name: "zero new or old",
		},
		{
			testFn: func(t *testing.T) {

				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Create a single workload that we can use twice, using the
				// consul provider.
				workload := serviceregistration.WorkloadServices{
					Services: []*structs.Service{
						{
							Provider: structs.ServiceProviderConsul,
						},
					},
				}

				// Call the function and ensure the consul backend has the
				// expected operations.
				require.NoError(t, wrapper.UpdateWorkload(&workload, &workload))
				require.Len(t, nomad.GetOps(), 0)

				consulOps := consul.GetOps()
				require.Len(t, consulOps, 1)
				require.Equal(t, "update", consulOps[0].Op)
			},
			name: "consul new and old",
		},
		{
			testFn: func(t *testing.T) {

				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Create a single workload that we can use twice, using the
				// nomad provider.
				workload := serviceregistration.WorkloadServices{
					Services: []*structs.Service{
						{
							Provider: structs.ServiceProviderNomad,
						},
					},
				}

				// Call the function and ensure the nomad backend has the
				// expected operations.
				require.NoError(t, wrapper.UpdateWorkload(&workload, &workload))
				require.Len(t, consul.GetOps(), 0)

				nomadOps := nomad.GetOps()
				require.Len(t, nomadOps, 1)
				require.Equal(t, "update", nomadOps[0].Op)
			},
			name: "nomad new and old",
		},
		{
			testFn: func(t *testing.T) {

				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Create each workload.
				newWorkload := serviceregistration.WorkloadServices{
					Services: []*structs.Service{
						{
							Provider: structs.ServiceProviderNomad,
						},
					},
				}

				oldWorkload := serviceregistration.WorkloadServices{
					Services: []*structs.Service{
						{
							Provider: structs.ServiceProviderConsul,
						},
					},
				}

				// Call the function and ensure the backends have the expected
				// operations.
				require.NoError(t, wrapper.UpdateWorkload(&oldWorkload, &newWorkload))

				nomadOps := nomad.GetOps()
				require.Len(t, nomadOps, 1)
				require.Equal(t, "add", nomadOps[0].Op)

				consulOps := consul.GetOps()
				require.Len(t, consulOps, 1)
				require.Equal(t, "remove", consulOps[0].Op)
			},
			name: "nomad new and consul old",
		},
		{
			testFn: func(t *testing.T) {

				// Generate the test wrapper and provider mocks.
				wrapper, consul, nomad := setupTestWrapper()

				// Create each workload.
				newWorkload := serviceregistration.WorkloadServices{
					Services: []*structs.Service{
						{
							Provider: structs.ServiceProviderConsul,
						},
					},
				}

				oldWorkload := serviceregistration.WorkloadServices{
					Services: []*structs.Service{
						{
							Provider: structs.ServiceProviderNomad,
						},
					},
				}

				// Call the function and ensure the backends have the expected
				// operations.
				require.NoError(t, wrapper.UpdateWorkload(&oldWorkload, &newWorkload))

				nomadOps := nomad.GetOps()
				require.Len(t, nomadOps, 1)
				require.Equal(t, "remove", nomadOps[0].Op)

				consulOps := consul.GetOps()
				require.Len(t, consulOps, 1)
				require.Equal(t, "add", consulOps[0].Op)
			},
			name: "consul new and nomad old",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			tc.testFn(t)
		})
	}
}

func setupTestWrapper() (*HandlerWrapper, *regMock.ServiceRegistrationHandler, *regMock.ServiceRegistrationHandler) {
	log := hclog.NewNullLogger()
	consulMock := regMock.NewServiceRegistrationHandler(log)
	nomadMock := regMock.NewServiceRegistrationHandler(log)
	wrapper := NewHandlerWrapper(log, consulMock, nomadMock)
	return wrapper, consulMock, nomadMock
}
