package agentk

// TODO: use mockgen and refactor existing tests see https://gitlab.com/gitlab-org/gitlab/-/issues/498218

import (
	"bytes"
	"context"
	"errors"
	"io"
	"testing"

	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/kubectl/pkg/scheme"
	"sigs.k8s.io/yaml"
)

type MockClient struct {
	NamespaceStore   map[string]*unstructured.Unstructured
	SecretStore      map[string]map[string]*unstructured.Unstructured
	ConfigMapStore   map[string]map[string]*unstructured.Unstructured
	ApplyRecorder    []string
	MockErrorApplier error
	MockErrorClient  error
}

func NewMockClient() *MockClient {
	return &MockClient{
		NamespaceStore: map[string]*unstructured.Unstructured{},
		SecretStore:    map[string]map[string]*unstructured.Unstructured{},
		ConfigMapStore: map[string]map[string]*unstructured.Unstructured{},
	}
}

func (m *MockClient) Get(_ context.Context, gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) {
	if gvr.Group == "" && gvr.Version == "v1" && gvr.Resource == "secrets" {
		secretNamespace, exist := m.SecretStore[namespace]
		if !exist {
			return nil, k8serrors.NewNotFound(namespaceGVR.GroupResource(), namespace)
		}
		obj, exist := secretNamespace[name]
		if !exist {
			return nil, k8serrors.NewNotFound(secretGVR.GroupResource(), name)
		}
		return obj, nil
	}

	if gvr.Group == "" && gvr.Version == "v1" && gvr.Resource == "configmaps" {
		secretNamespace, exist := m.ConfigMapStore[namespace]
		if !exist {
			return nil, k8serrors.NewNotFound(namespaceGVR.GroupResource(), namespace)
		}
		obj, exist := secretNamespace[name]
		if !exist {
			return nil, k8serrors.NewNotFound(configMapGVR.GroupResource(), name)
		}
		return obj, nil
	}

	if gvr.Group == "" && gvr.Version == "v1" && gvr.Resource == "namespaces" {
		obj, exist := m.NamespaceStore[name]
		if !exist {
			return nil, k8serrors.NewNotFound(namespaceGVR.GroupResource(), name)
		} else {
			return obj, nil
		}
	}

	return nil, errors.New("resource schema not supported")
}

func (m *MockClient) UpdateOrCreate(_ context.Context, gvr schema.GroupVersionResource, namespace string, obj runtime.Object, force bool) (*unstructured.Unstructured, error) {
	u := &unstructured.Unstructured{}
	if m.MockErrorClient != nil {
		return nil, m.MockErrorClient
	}
	if gvr.Group == "" && gvr.Version == "v1" && gvr.Resource == "secrets" {
		err := scheme.Scheme.Convert(obj, u, nil)
		if err != nil {
			return nil, err
		}
		_, exist := m.SecretStore[namespace]
		if !exist {
			m.SecretStore[namespace] = make(map[string]*unstructured.Unstructured)
		}
		m.SecretStore[namespace][u.GetName()] = u
		return u, nil
	}

	if gvr.Group == "" && gvr.Version == "v1" && gvr.Resource == "configmaps" {
		err := scheme.Scheme.Convert(obj, u, nil)
		if err != nil {
			return nil, err
		}
		_, exist := m.ConfigMapStore[namespace]
		if !exist {
			m.ConfigMapStore[namespace] = make(map[string]*unstructured.Unstructured)
		}
		m.ConfigMapStore[namespace][u.GetName()] = u
		return u, nil
	}

	if gvr.Group == "" && gvr.Version == "v1" && gvr.Resource == "namespaces" {
		err := scheme.Scheme.Convert(obj, u, nil)
		if err != nil {
			return nil, err
		}
		m.NamespaceStore[u.GetName()] = u
		return u, nil
	}

	return nil, errors.New("resource schema not supported")
}

func (m *MockClient) Delete(_ context.Context, gvr schema.GroupVersionResource, namespace, name string) error {
	if gvr.Group == "" && gvr.Version == "v1" && gvr.Resource == "secrets" {
		namespaceMap, exist := m.SecretStore[namespace]
		if !exist {
			return nil
		}
		delete(namespaceMap, name)
	}

	if gvr.Group == "" && gvr.Version == "v1" && gvr.Resource == "configmaps" {
		namespaceMap, exist := m.ConfigMapStore[namespace]
		if !exist {
			return nil
		}
		delete(namespaceMap, name)
	}

	if gvr.Group == "" && gvr.Version == "v1" && gvr.Resource == "namespaces" {
		delete(m.NamespaceStore, name)
	}

	return nil
}

func (m *MockClient) Apply(_ context.Context, config string) <-chan error {
	m.ApplyRecorder = append(m.ApplyRecorder, config)

	if m.MockErrorApplier != nil {
		errorCh := make(chan error)
		go func() {
			defer close(errorCh)
			errorCh <- m.MockErrorApplier
			m.MockErrorApplier = nil
		}()

		return errorCh
	}

	return nil
}

func (m *MockClient) ResetApplyRecorder() {
	m.ApplyRecorder = []string{}
}

func (m *MockClient) Decode(data io.Reader) ([]*unstructured.Unstructured, error) {
	var b bytes.Buffer
	_, err := b.ReadFrom(data)
	if err != nil {
		return nil, err
	}

	objects := make([]*unstructured.Unstructured, 0)

	// Split by document separator
	for doc := range bytes.SplitSeq(b.Bytes(), []byte("---")) {
		doc = bytes.TrimSpace(doc)
		if len(doc) == 0 {
			continue
		}

		var out map[string]any
		err = yaml.Unmarshal(doc, &out)
		if err != nil {
			return nil, err
		}

		invConfigMapKey, ok := out["inventory_configmap"]
		if !ok {
			continue
		}

		invConfigMapName, ok := invConfigMapKey.(map[string]any)["name"]
		if !ok {
			continue
		}

		invConfigMapNamespace, ok := invConfigMapKey.(map[string]any)["namespace"]
		if !ok {
			continue
		}

		invConfigMapObj := m.ConfigMapStore[invConfigMapNamespace.(string)][invConfigMapName.(string)]
		if invConfigMapObj != nil {
			objects = append(objects, invConfigMapObj)
		}
	}

	return objects, nil
}

func generateImagePullSecret(name, namespace string, labels map[string]string, data map[string][]byte) *corev1.Secret {
	return &corev1.Secret{
		TypeMeta: v1.TypeMeta{
			Kind:       "Secret",
			APIVersion: "v1",
		},
		ObjectMeta: v1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
			Labels:    labels,
		},
		Data: data,
		Type: "kubernetes.io/dockercfg",
	}
}

func toUnstructuredSecret(t *testing.T, secret *corev1.Secret) *unstructured.Unstructured {
	object, err := runtime.DefaultUnstructuredConverter.ToUnstructured(secret)
	require.NoError(t, err)

	return &unstructured.Unstructured{
		Object: object,
	}
}

func fromUnstructuredSecret(t *testing.T, unstructuredSecret *unstructured.Unstructured) *corev1.Secret {
	var secret *corev1.Secret
	err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredSecret.Object, &secret)
	require.NoError(t, err)
	return secret
}
