package test

import (
	"context"
	"fmt"
	"reflect"

	"github.com/google/go-cmp/cmp"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/runtime/schema"

	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

var _ client.Client = &MockClient{}

// A MockGetFn is used to mock client.Client's Get implementation.
type MockGetFn func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error

// A MockListFn is used to mock client.Client's List implementation.
type MockListFn func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error

// A MockCreateFn is used to mock client.Client's Create implementation.
type MockCreateFn func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error

// A MockDeleteFn is used to mock client.Client's Delete implementation.
type MockDeleteFn func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error

// A MockDeleteAllOfFn is used to mock client.Client's Delete implementation.
type MockDeleteAllOfFn func(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error

// A MockUpdateFn is used to mock client.Client's Update implementation.
type MockUpdateFn func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error

// A MockPatchFn is used to mock client.Client's Patch implementation.
type MockPatchFn func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error

// A MockSubresourceGetFn is used to mock client.Client's StatusCreate implementation.
type MockSubresourceGetFn func(ctx context.Context, obj client.Object, sub client.Object, opts ...client.SubResourceGetOption) error

// A MockStatusCreateFn is used to mock client.Client's StatusCreate implementation.
type MockSubresourceCreateFn func(ctx context.Context, obj client.Object, sub client.Object, opts ...client.SubResourceCreateOption) error

// A MockStatusUpdateFn is used to mock client.Client's StatusUpdate implementation.
type MockSubresourceUpdateFn func(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error

// A MockStatusPatchFn is used to mock client.Client's StatusUpdate implementation.
type MockSubresourcePatchFn func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error

// An ObjectFn operates on the supplied Object. You might use an ObjectFn to
// test or update the contents of an Object.
type ObjectFn func(obj client.Object) error
type ObjectListFn func(obj client.ObjectList) error

// NewMockGetFn returns a MockGetFn that returns the supplied error.
func NewMockGetFn(err error, ofn ...ObjectFn) MockGetFn {
	return func(_ context.Context, _ client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
		for _, fn := range ofn {
			if err := fn(obj); err != nil {
				return err
			}
		}
		return err
	}
}

// NewMockListFn returns a MockListFn that returns the supplied error.
func NewMockListFn(err error, ofn ...ObjectListFn) MockListFn {
	return func(_ context.Context, obj client.ObjectList, _ ...client.ListOption) error {
		for _, fn := range ofn {
			if err := fn(obj); err != nil {
				return err
			}
		}
		return err
	}
}

// NewMockCreateFn returns a MockCreateFn that returns the supplied error.
func NewMockCreateFn(err error, ofn ...ObjectFn) MockCreateFn {
	return func(_ context.Context, obj client.Object, opts ...client.CreateOption) error {
		for _, fn := range ofn {
			if err := fn(obj); err != nil {
				return err
			}
		}
		return err
	}
}

// NewMockDeleteFn returns a MockDeleteFn that returns the supplied error.
func NewMockDeleteFn(err error, ofn ...ObjectFn) MockDeleteFn {
	return func(_ context.Context, obj client.Object, _ ...client.DeleteOption) error {
		for _, fn := range ofn {
			if err := fn(obj); err != nil {
				return err
			}
		}
		return err
	}
}

// NewMockDeleteAllOfFn returns a MockDeleteAllOfFn that returns the supplied error.
func NewMockDeleteAllOfFn(err error, ofn ...ObjectFn) MockDeleteAllOfFn {
	return func(_ context.Context, obj client.Object, _ ...client.DeleteAllOfOption) error {
		for _, fn := range ofn {
			if err := fn(obj); err != nil {
				return err
			}
		}
		return err
	}
}

// NewMockUpdateFn returns a MockUpdateFn that returns the supplied error.
func NewMockUpdateFn(err error, ofn ...ObjectFn) MockUpdateFn {
	return func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
		for _, fn := range ofn {
			if err := fn(obj); err != nil {
				return err
			}
		}
		return err
	}
}

// NewMockPatchFn returns a MockPatchFn that returns the supplied error.
func NewMockPatchFn(err error, ofn ...ObjectFn) MockPatchFn {
	return func(_ context.Context, obj client.Object, _ client.Patch, _ ...client.PatchOption) error {
		for _, fn := range ofn {
			if err := fn(obj); err != nil {
				return err
			}
		}
		return err
	}
}

// NewMockStatusUpdateFn returns a MockStatusUpdateFn that returns the supplied error.
func NewMockSubresourceGetFn(err error, ofn ...ObjectFn) MockSubresourceGetFn {
	return func(_ context.Context, obj client.Object, sub client.Object, _ ...client.SubResourceGetOption) error {
		for _, fn := range ofn {
			if err := fn(obj); err != nil {
				return err
			}
		}
		return err
	}
}

// NewMockStatusUpdateFn returns a MockStatusUpdateFn that returns the supplied error.
func NewMockSubresourceCreateFn(err error, ofn ...ObjectFn) MockSubresourceCreateFn {
	return func(_ context.Context, obj client.Object, sub client.Object, _ ...client.SubResourceCreateOption) error {
		for _, fn := range ofn {
			if err := fn(obj); err != nil {
				return err
			}
		}
		return err
	}
}

// NewMockStatusUpdateFn returns a MockStatusUpdateFn that returns the supplied error.
func NewMockSubresourceUpdateFn(err error, ofn ...ObjectFn) MockSubresourceUpdateFn {
	return func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
		for _, fn := range ofn {
			if err := fn(obj); err != nil {
				return err
			}
		}
		return err
	}
}

// NewMockStatusPatchFn returns a MockStatusPatchFn that returns the supplied error.
func NewMockSubresourcePatchFn(err error, ofn ...ObjectFn) MockSubresourcePatchFn {
	return func(_ context.Context, obj client.Object, _ client.Patch, _ ...client.SubResourcePatchOption) error {
		for _, fn := range ofn {
			if err := fn(obj); err != nil {
				return err
			}
		}
		return err
	}
}

// MockClient implements controller-runtime's Client interface, allowing each
// method to be overridden for testing. The controller-runtime provides a fake
// client, but it is has surprising side effects (e.g. silently calling
// os.Exit(1)) and does not allow us control over the errors it returns.
type MockClient struct {
	MockGet               MockGetFn
	MockList              MockListFn
	MockCreate            MockCreateFn
	MockDelete            MockDeleteFn
	MockDeleteAllOf       MockDeleteAllOfFn
	MockUpdate            MockUpdateFn
	MockPatch             MockPatchFn
	MockSubresourceGet    MockSubresourceGetFn
	MockSubresourceCreate MockSubresourceCreateFn
	MockSubresourceUpdate MockSubresourceUpdateFn
	MockSubresourcePatch  MockSubresourcePatchFn
}

// NewMockClient returns a MockClient that does nothing when its methods are
// called.
func NewMockClient() *MockClient {
	return &MockClient{
		MockGet:               NewMockGetFn(nil),
		MockList:              NewMockListFn(nil),
		MockCreate:            NewMockCreateFn(nil),
		MockDelete:            NewMockDeleteFn(nil),
		MockDeleteAllOf:       NewMockDeleteAllOfFn(nil),
		MockUpdate:            NewMockUpdateFn(nil),
		MockPatch:             NewMockPatchFn(nil),
		MockSubresourceGet:    NewMockSubresourceGetFn(nil),
		MockSubresourceCreate: NewMockSubresourceCreateFn(nil),
		MockSubresourceUpdate: NewMockSubresourceUpdateFn(nil),
		MockSubresourcePatch:  NewMockSubresourcePatchFn(nil),
	}
}

// Get calls MockClient's MockGet function.
func (c *MockClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
	return c.MockGet(ctx, key, obj, opts...)
}

// List calls MockClient's MockList function.
func (c *MockClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
	return c.MockList(ctx, list, opts...)
}

// Create calls MockClient's MockCreate function.
func (c *MockClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
	return c.MockCreate(ctx, obj, opts...)
}

// Delete calls MockClient's MockDelete function.
func (c *MockClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
	return c.MockDelete(ctx, obj, opts...)
}

// DeleteAllOf calls MockClient's DeleteAllOf function.
func (c *MockClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
	return c.MockDeleteAllOf(ctx, obj, opts...)
}

// Update calls MockClient's MockUpdate function.
func (c *MockClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
	return c.MockUpdate(ctx, obj, opts...)
}

// Patch calls MockClient's MockPatch function.
func (c *MockClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
	return c.MockPatch(ctx, obj, patch, opts...)
}

// Status returns status writer for status sub-resource
func (c *MockClient) Status() client.StatusWriter {
	return &MockSubresourceClient{
		MockGet:    c.MockSubresourceGet,
		MockCreate: c.MockSubresourceCreate,
		MockUpdate: c.MockSubresourceUpdate,
		MockPatch:  c.MockSubresourcePatch,
	}
}

func (c *MockClient) Scheme() *runtime.Scheme {
	return nil
}

func (c *MockClient) RESTMapper() meta.RESTMapper {
	// TODO: Implement a fake RESTMapper.
	return nil
}

func (c *MockClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
	return schema.GroupVersionKind{}, fmt.Errorf("unimplemented")
}

func (c *MockClient) IsObjectNamespaced(obj runtime.Object) (bool, error) {
	return false, fmt.Errorf("unimplemented")
}

func (c *MockClient) SubResource(subresource string) client.SubResourceClient {
	return &MockSubresourceClient{
		MockGet:    c.MockSubresourceGet,
		MockCreate: c.MockSubresourceCreate,
		MockUpdate: c.MockSubresourceUpdate,
		MockPatch:  c.MockSubresourcePatch,
	}
}

// MockSuberesourceClient provides mock functionality for sub-resource
type MockSubresourceClient struct {
	MockGet    MockSubresourceGetFn
	MockCreate MockSubresourceCreateFn
	MockUpdate MockSubresourceUpdateFn
	MockPatch  MockSubresourcePatchFn
}

func (m *MockSubresourceClient) Get(ctx context.Context, obj client.Object, sub client.Object, opts ...client.SubResourceGetOption) error {
	return m.MockGet(ctx, obj, sub, opts...)
}

func (m *MockSubresourceClient) Create(ctx context.Context, obj client.Object, sub client.Object, opts ...client.SubResourceCreateOption) error {
	return m.MockCreate(ctx, obj, sub, opts...)
}

// Update status sub-resource
func (m *MockSubresourceClient) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
	return m.MockUpdate(ctx, obj, opts...)
}

// Patch mocks the patch method
func (m *MockSubresourceClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
	return m.MockPatch(ctx, obj, patch, opts...)
}

func EquateErrors() cmp.Option {
	return cmp.Comparer(func(a, b error) bool {
		if a == nil || b == nil {
			return a == nil && b == nil
		}

		av := reflect.ValueOf(a)
		bv := reflect.ValueOf(b)
		if av.Type() != bv.Type() {
			return false
		}

		return a.Error() == b.Error()
	})
}
