package testing

import (
	"context"
	"fmt"
	"strings"

	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	utilrand "k8s.io/apimachinery/pkg/util/rand"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/testing"

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

// FakeClient is a fake client implements
// sigs.k8s.io/controller-runtime/pkg/client
// TODO(liubo02).
type FakeClient struct {
	testing.Fake
	tracker testing.ObjectTracker
	scheme  *runtime.Scheme
}

const (
	maxNameLength          = 63
	randomLength           = 5
	maxGeneratedNameLength = maxNameLength - randomLength
)

var _ client.WithWatch = &FakeClient{}

// NewClientBuilder returns a new builder to create a fake client.
func NewClientBuilder() *ClientBuilder {
	return &ClientBuilder{}
}

// ClientBuilder builds a fake client.
type ClientBuilder struct {
	scheme             *runtime.Scheme
	initObject         []client.Object
	initLists          []client.ObjectList
	initRuntimeObjects []runtime.Object
}

// WithScheme sets this builder's internal scheme.
// If not set, defaults to client-go's global scheme.Scheme.
func (f *ClientBuilder) WithScheme(scheme *runtime.Scheme) *ClientBuilder {
	f.scheme = scheme
	return f
}

// WithObjects can be optionally used to initialize this fake client with client.Object(s).
func (f *ClientBuilder) WithObjects(initObjs ...client.Object) *ClientBuilder {
	f.initObject = append(f.initObject, initObjs...)
	return f
}

// WithLists can be optionally used to initialize this fake client with client.ObjectList(s).
func (f *ClientBuilder) WithLists(initLists ...client.ObjectList) *ClientBuilder {
	f.initLists = append(f.initLists, initLists...)
	return f
}

// WithRuntimeObjects can be optionally used to initialize this fake client with runtime.Object(s).
func (f *ClientBuilder) WithRuntimeObjects(initRuntimeObjs ...runtime.Object) *ClientBuilder {
	f.initRuntimeObjects = append(f.initRuntimeObjects, initRuntimeObjs...)
	return f
}

// Build builds and returns a new fake client.
func (f *ClientBuilder) Build() *FakeClient {
	if f.scheme == nil {
		f.scheme = scheme.Scheme
	}

	tracker := versionedTracker{
		ObjectTracker: testing.NewObjectTracker(f.scheme, scheme.Codecs.UniversalDecoder()),
		scheme:        f.scheme,
	}
	for _, obj := range f.initObject {
		if err := tracker.Add(obj); err != nil {
			panic(fmt.Errorf("failed to add object %v to fake client: %w", obj, err))
		}
	}
	for _, obj := range f.initLists {
		if err := tracker.Add(obj); err != nil {
			panic(fmt.Errorf("failed to add list %v to fake client: %w", obj, err))
		}
	}
	for _, obj := range f.initRuntimeObjects {
		if err := tracker.Add(obj); err != nil {
			panic(fmt.Errorf("failed to add runtime object %v to fake client: %w", obj, err))
		}
	}

	fc := &FakeClient{
		tracker: tracker,
		scheme:  f.scheme,
	}

	fc.AddReactor("*", "*", testing.ObjectReaction(tracker))
	fc.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
		gvr := action.GetResource()
		ns := action.GetNamespace()
		watch, err := tracker.Watch(gvr, ns)
		if err != nil {
			return false, nil, err
		}
		return true, watch, nil
	})

	return fc
}

func (fc *FakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error {
	gvr, err := getGVRFromObject(obj, fc.scheme)
	if err != nil {
		return err
	}
	action := testing.NewGetAction(gvr, key.Namespace, key.Name)
	if len(key.Namespace) == 0 {
		action = testing.NewRootGetAction(gvr, key.Name)
	}

	o, err := fc.Invokes(action, obj)
	if err != nil {
		return err
	}

	if err := fc.scheme.Convert(o, obj, nil); err != nil {
		return err
	}

	return err
}

func (fc *FakeClient) Watch(
	ctx context.Context,
	list client.ObjectList,
	opts ...client.ListOption,
) (watch.Interface, error) {
	gvk, err := apiutil.GVKForObject(list, fc.scheme)
	if err != nil {
		return nil, err
	}

	gvk.Kind = strings.TrimSuffix(gvk.Kind, "List")

	listOpts := client.ListOptions{}
	listOpts.ApplyOptions(opts)

	gvr, _ := meta.UnsafeGuessKindToResource(gvk)

	if len(listOpts.Namespace) == 0 {
		return fc.InvokesWatch(testing.NewRootWatchAction(gvr, *listOpts.AsListOptions()))
	}

	return fc.InvokesWatch(testing.NewWatchAction(gvr, listOpts.Namespace, *listOpts.AsListOptions()))
}

func (fc *FakeClient) List(ctx context.Context, obj client.ObjectList, opts ...client.ListOption) error {
	gvk, err := apiutil.GVKForObject(obj, fc.scheme)
	if err != nil {
		return err
	}

	// listKind := gvk.Kind

	gvk.Kind = strings.TrimSuffix(gvk.Kind, "List")

	listOpts := client.ListOptions{}
	listOpts.ApplyOptions(opts)

	gvr, _ := meta.UnsafeGuessKindToResource(gvk)
	action := testing.NewListAction(gvr, gvk, listOpts.Namespace, *listOpts.AsListOptions())
	if len(listOpts.Namespace) == 0 {
		action = testing.NewRootListAction(gvr, gvk, *listOpts.AsListOptions())
	}
	o, err := fc.Invokes(action, obj)
	if err != nil {
		return err
	}

	if err := fc.scheme.Convert(o, obj, nil); err != nil {
		return err
	}

	if listOpts.LabelSelector != nil {
		objs, err := meta.ExtractList(obj)
		if err != nil {
			return err
		}
		filteredObjs, err := FilterWithLabels(objs, listOpts.LabelSelector)
		if err != nil {
			return err
		}
		err = meta.SetList(obj, filteredObjs)
		if err != nil {
			return err
		}
	}
	return nil
}

func (fc *FakeClient) Scheme() *runtime.Scheme {
	return fc.scheme
}

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

func (fc *FakeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
	createOptions := &client.CreateOptions{}
	createOptions.ApplyOptions(opts)

	for _, dryRunOpt := range createOptions.DryRun {
		if dryRunOpt == metav1.DryRunAll {
			return nil
		}
	}

	gvr, err := getGVRFromObject(obj, fc.scheme)
	if err != nil {
		return err
	}
	accessor, err := meta.Accessor(obj)
	if err != nil {
		return err
	}

	if accessor.GetName() == "" && accessor.GetGenerateName() != "" {
		base := accessor.GetGenerateName()
		if len(base) > maxGeneratedNameLength {
			base = base[:maxGeneratedNameLength]
		}
		accessor.SetName(fmt.Sprintf("%s%s", base, utilrand.String(randomLength)))
	}

	ns := accessor.GetNamespace()
	action := testing.NewCreateAction(gvr, ns, obj)
	if len(ns) == 0 {
		action = testing.NewRootCreateAction(gvr, obj)
	}

	o, err := fc.Invokes(action, obj)
	if err != nil {
		return err
	}
	if err := fc.scheme.Convert(o, obj, nil); err != nil {
		return err
	}

	return nil
}

func (fc *FakeClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
	gvr, err := getGVRFromObject(obj, fc.scheme)
	if err != nil {
		return err
	}
	accessor, err := meta.Accessor(obj)
	if err != nil {
		return err
	}
	delOptions := client.DeleteOptions{}
	delOptions.ApplyOptions(opts)

	ns := accessor.GetNamespace()
	name := accessor.GetName()
	action := testing.NewDeleteAction(gvr, ns, name)
	if len(ns) == 0 {
		action = testing.NewRootDeleteAction(gvr, name)
	}
	if _, err := fc.Invokes(action, obj); err != nil {
		return err
	}
	return nil
}

func (fc *FakeClient) DeleteAllOf(
	ctx context.Context,
	obj client.Object,
	opts ...client.DeleteAllOfOption,
) error {
	// TODO(liubo02): implement delete all of
	return nil
}

func (fc *FakeClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
	return fc.update(ctx, obj, nil, opts...)
}

func (fc *FakeClient) update(
	ctx context.Context,
	obj client.Object,
	subresources []string,
	opts ...client.UpdateOption,
) error {
	updateOptions := &client.UpdateOptions{}
	updateOptions.ApplyOptions(opts)

	for _, dryRunOpt := range updateOptions.DryRun {
		if dryRunOpt == metav1.DryRunAll {
			return nil
		}
	}

	gvr, err := getGVRFromObject(obj, fc.scheme)
	if err != nil {
		return err
	}
	accessor, err := meta.Accessor(obj)
	if err != nil {
		return err
	}

	var action testing.Action
	ns := accessor.GetNamespace()
	switch {
	case len(ns) == 0 && len(subresources) == 0:
		action = testing.NewRootUpdateAction(gvr, obj)
	case len(ns) > 0 && len(subresources) == 0:
		action = testing.NewUpdateAction(gvr, ns, obj)
	case len(ns) == 0 && len(subresources) > 0:
		action = testing.NewRootUpdateSubresourceAction(gvr, strings.Join(subresources, "/"), obj)
	case len(ns) > 0 && len(subresources) > 0:
		action = testing.NewUpdateSubresourceAction(gvr, ns, strings.Join(subresources, "/"), obj)
	}

	o, err := fc.Invokes(action, obj)
	if err != nil {
		return err
	}

	if err := fc.scheme.Convert(o, obj, nil); err != nil {
		return err
	}

	return nil
}

func (fc *FakeClient) Patch(
	ctx context.Context,
	obj client.Object,
	patch client.Patch,
	opts ...client.PatchOption,
) error {
	return fc.patch(ctx, obj, patch, nil, opts...)
}

func (fc *FakeClient) patch(
	ctx context.Context,
	obj client.Object,
	patch client.Patch,
	subresources []string,
	opts ...client.PatchOption,
) error {
	patchOptions := &client.PatchOptions{}
	patchOptions.ApplyOptions(opts)

	for _, dryRunOpt := range patchOptions.DryRun {
		if dryRunOpt == metav1.DryRunAll {
			return nil
		}
	}

	gvr, err := getGVRFromObject(obj, fc.scheme)
	if err != nil {
		return err
	}
	accessor, err := meta.Accessor(obj)
	if err != nil {
		return err
	}
	data, err := patch.Data(obj)
	if err != nil {
		return err
	}

	ns := accessor.GetNamespace()
	name := accessor.GetName()
	var action testing.Action
	switch {
	case len(ns) == 0 && len(subresources) == 0:
		action = testing.NewRootPatchAction(gvr, name, patch.Type(), data)
	case len(ns) > 0 && len(subresources) == 0:
		action = testing.NewPatchAction(gvr, ns, name, patch.Type(), data)
	case len(ns) == 0 && len(subresources) > 0:
		action = testing.NewRootPatchSubresourceAction(gvr, name, patch.Type(), data, subresources...)
	case len(ns) > 0 && len(subresources) > 0:
		action = testing.NewPatchSubresourceAction(gvr, ns, name, patch.Type(), data, subresources...)
	}

	o, err := fc.Invokes(action, &metav1.Status{Status: "patch failed"})
	if err != nil {
		return err
	}

	if err := fc.scheme.Convert(o, obj, nil); err != nil {
		return err
	}

	return nil
}

func (fc *FakeClient) Status() client.StatusWriter {
	return &fakeStatusWriter{client: fc}
}

func getGVRFromObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionResource, error) {
	gvk, err := apiutil.GVKForObject(obj, scheme)
	if err != nil {
		return schema.GroupVersionResource{}, err
	}
	gvr, _ := meta.UnsafeGuessKindToResource(gvk)
	return gvr, nil
}

type fakeStatusWriter struct {
	client *FakeClient
}

func (sw *fakeStatusWriter) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
	return sw.client.update(ctx, obj, []string{"status"}, opts...)
}

func (sw *fakeStatusWriter) Patch(
	ctx context.Context,
	obj client.Object,
	patch client.Patch,
	opts ...client.PatchOption,
) error {
	return sw.client.patch(ctx, obj, patch, []string{"status"}, opts...)
}

// FilterWithLabels returns a copy of the items in objs matching labelSel.
func FilterWithLabels(objs []runtime.Object, labelSel labels.Selector) ([]runtime.Object, error) {
	outItems := make([]runtime.Object, 0, len(objs))
	for _, obj := range objs {
		meta, err := meta.Accessor(obj)
		if err != nil {
			return nil, err
		}
		if labelSel != nil {
			lbls := labels.Set(meta.GetLabels())
			if !labelSel.Matches(lbls) {
				continue
			}
		}
		outItems = append(outItems, obj.DeepCopyObject())
	}
	return outItems, nil
}
