package tests_test

import (
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"os"
	"path"
	"strings"

	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/util/yaml"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
)

var JunitOutputFile string

func init() {

	flag.StringVar(&JunitOutputFile, "junit-output", "", "Set path to Junit report.")
	flag.Parse()

	envSet := false
	kubeconfig, envSet = os.LookupEnv("KUBECONFIG")

	if !envSet {
		panic("the KUBECONFIG environment variable is not set")
	}

	clusterConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
	if err != nil {
		panic(err)
	}

	clt, err := kubernetes.NewForConfig(clusterConfig)
	if err != nil {
		panic(err)
	}

	client = newTestClient(clt)
	if err = client.loadResourceTypes(); err != nil {
		panic(err)
	}
}

type testClient struct {
	*kubernetes.Clientset
	namespacedResources resourceTypes
}

func newTestClient(client *kubernetes.Clientset) *testClient {
	return &testClient{
		Clientset:           client,
		namespacedResources: make(map[string]resourceVersion),
	}
}

func (clt testClient) loadResourceTypes() error {
	clusterResourceTypesList, err := clt.Discovery().ServerPreferredResources()
	if err != nil {
		return err
	}

	for _, api := range clusterResourceTypesList {
		clt.namespacedResources.addApi(api)
	}

	return nil
}

func (clt testClient) getPod(ctx context.Context, namespace, name string) (*v1.Pod, error) {

	res, err := clt.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}

	return res, nil
}

func (clt testClient) getNamespacedResource(ctx context.Context, resourceType, group, namespace, name string) (*unstructured.Unstructured, error) {

	api, found := clt.namespacedResources[group]
	if !found {
		return nil, fmt.Errorf("can't find API Resource for %s", group)
	}

	if !stringInSlice(api.resources, resourceType) {
		return nil, fmt.Errorf("can't find Resource for %s", resourceType)
	}

	absUrl := fmt.Sprintf("/apis/%s/%s/namespaces/%s/%s/%s", group, api.version, namespace, resourceType, name)

	var obj = new(unstructured.Unstructured)
	res, err := clt.RESTClient().Get().AbsPath(absUrl).DoRaw(ctx)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(res, obj)
	if err != nil {
		return nil, err
	}

	return obj, nil
}

func (clt testClient) getNonNamespacedResource(ctx context.Context, resourceType, group, name string) (*unstructured.Unstructured, error) {

	api, found := clt.namespacedResources[group]
	if !found {
		return nil, fmt.Errorf("can't find API Resource for %s", group)
	}

	if !stringInSlice(api.resources, resourceType) {
		return nil, fmt.Errorf("can't find Resource for %s", resourceType)
	}

	absUrl := fmt.Sprintf("/apis/%s/%s/%s/%s", group, api.version, resourceType, name)

	var obj = new(unstructured.Unstructured)
	res, err := clt.RESTClient().Get().AbsPath(absUrl).DoRaw(ctx)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(res, obj)
	if err != nil {
		return nil, err
	}

	return obj, nil
}

type resourceTypes map[string]resourceVersion

func (clusterResourceTypes resourceTypes) addApi(api *metav1.APIResourceList) {
	splitApiVersion := strings.Split(api.GroupVersion, "/")
	if len(splitApiVersion) == 2 {
		group := splitApiVersion[0]
		version := splitApiVersion[1]

		existing, found := clusterResourceTypes[group]
		if !found || compK8sVersions(existing.version, version) == version {
			resources := make([]string, len(api.APIResources))
			for i, resource := range api.APIResources {
				resources[i] = resource.Name
			}
			clusterResourceTypes[group] = resourceVersion{version: version, resources: resources}
		}
	}
}

type resourceVersion struct {
	version   string
	resources []string
}

func compK8sVersions(v1, v2 string) string {
	i := 0
	for ; i < len(v1) && i < len(v2); i++ {
		if v1[i] > v2[i] {
			return v1
		} else if v2[i] > v1[i] {
			return v2
		}
	}

	if len(v2) < len(v1) {
		return v2
	}

	return v1
}

func getObjectFromFile(reader io.Reader) (*unstructured.Unstructured, error) {
	objFromFile := new(unstructured.Unstructured)
	if err := yaml.NewYAMLOrJSONDecoder(reader, 1024).Decode(objFromFile); err != nil {
		return nil, err
	}

	return objFromFile, nil
}

func getPodFromFile(reader io.Reader) (*v1.Pod, error) {
	objFromFile := new(v1.Pod)
	if err := yaml.NewYAMLOrJSONDecoder(reader, 1024).Decode(objFromFile); err != nil {
		return nil, err
	}

	return objFromFile, nil
}

func stringInSlice(slice []string, requested string) bool {
	for _, fromSlice := range slice {
		if requested == fromSlice {
			return true
		}
	}
	return false
}

func fileInDir(slice []os.DirEntry, file string) bool {
	for _, fromSlice := range slice {
		if file == path.Base(fromSlice.Name()) {
			return true
		}
	}
	return false
}

func BeAllTrueInBoolMap() *boolMapAllTrueMatcher {
	return new(boolMapAllTrueMatcher)
}

type boolMapAllTrueMatcher struct{}

func (boolMapAllTrueMatcher) Match(actual interface{}) (success bool, err error) {
	boolMap, ok := actual.(map[string]bool)
	if !ok {
		return false, errors.New("should be map[bool]string")
	}

	for _, value := range boolMap {
		if !value {
			return false, nil
		}
	}
	return true, nil
}
func (boolMapAllTrueMatcher) FailureMessage(actual interface{}) (message string) {
	return fmt.Sprintf("Expected\n\t%#v\nto be all true", actual)
}
func (boolMapAllTrueMatcher) NegatedFailureMessage(actual interface{}) (message string) {
	return fmt.Sprintf("Expected\n\t%#v\nto not be all true", actual)
}
