package resource

import (
	"bytes"

	"io"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/client/metadata"
	"github.com/aws/aws-sdk-go/aws/request"
	"github.com/aws/aws-sdk-go/aws/session"
	"helm.sh/helm/v3/pkg/action"
	"helm.sh/helm/v3/pkg/chart"
	"helm.sh/helm/v3/pkg/chartutil"
	"helm.sh/helm/v3/pkg/cli"
	kubefake "helm.sh/helm/v3/pkg/kube/fake"
	"helm.sh/helm/v3/pkg/release"
	"helm.sh/helm/v3/pkg/storage"
	"helm.sh/helm/v3/pkg/storage/driver"
	htime "helm.sh/helm/v3/pkg/time"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	v1 "k8s.io/api/core/v1"
	v1beta1 "k8s.io/api/extensions/v1beta1"
	networkingv1beta1 "k8s.io/api/networking/v1beta1"
	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/cli-runtime/pkg/genericclioptions"
	"k8s.io/cli-runtime/pkg/resource"
	"k8s.io/client-go/discovery"
	fakeclientset "k8s.io/client-go/kubernetes/fake"
	"k8s.io/client-go/rest/fake"
	"k8s.io/client-go/restmapper"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/kubectl/pkg/scheme"
)

type chartOptions struct {
	*chart.Chart
}

type chartOption func(*chartOptions)

type fakeCachedDiscoveryClient struct {
	discovery.DiscoveryInterface
}

var (
	TestFolder  = "testdata"
	TestZipFile = TestFolder + "/test_lambda.zip"
)

// Session is a mock session which is used to hit the mock server
var MockSession = func() *session.Session {
	// server is the mock server that simply writes a 200 status back to the client
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))

	return session.Must(session.NewSession(&aws.Config{
		DisableSSL: aws.Bool(true),
		Endpoint:   aws.String(server.URL),
		Region:     aws.String("us-east-1"),
	}))
}()

var TestManifest = `---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: nginx-deployment

---
apiVersion: v1
kind: Service
metadata:
 name: my-service

---
apiVersion: v1
kind: Service
metadata:
 name: lb-service
 spec:
  type: LoadBalancer

---
apiVersion: apps/v1
kind: DaemonSet
metadata:
 name: nginx-ds

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
 name: nginx-ss

---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: test-ingress`

var TestPendingManifest = `apiVersion: apps/v1
kind: Deployment
metadata:
 name: nginx-deployment-foo`

func newFakeBuilder(t *testing.T) func() *resource.Builder {
	cfg, _ := clientcmd.NewDefaultClientConfigLoadingRules().Load()
	clientConfig := clientcmd.NewDefaultClientConfig(*cfg, &clientcmd.ConfigOverrides{})
	configFlags := genericclioptions.NewTestConfigFlags().
		WithClientConfig(clientConfig).
		WithRESTMapper(testRESTMapper())
	header := http.Header{}
	header.Set("Content-Type", runtime.ContentTypeJSON)
	codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
	return func() *resource.Builder {
		return resource.NewFakeBuilder(
			func(version schema.GroupVersion) (resource.RESTClient, error) {
				return &fake.RESTClient{
					GroupVersion:         schema.GroupVersion{Version: "v1"},
					NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
					Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
						switch p, m := req.URL.Path, req.Method; {
						case p == "/namespaces/test/services" && m == "POST":
							return &http.Response{StatusCode: http.StatusCreated, Header: header, Body: ObjBody(codec, ns("test"))}, nil
						case p == "/namespaces/default/deployments/nginx-deployment" && m == "GET":
							return &http.Response{StatusCode: http.StatusOK, Header: header, Body: ObjBody(codec, dep("nginx-deployment", "default", false))}, nil
						case p == "/namespaces/default/deployments/nginx-deployment-foo" && m == "GET":
							return &http.Response{StatusCode: http.StatusOK, Header: header, Body: ObjBody(codec, dep("nginx-deployment-foo", "default", true))}, nil
						case p == "/namespaces/default/services/my-service" && m == "GET":
							return &http.Response{StatusCode: http.StatusOK, Header: header, Body: ObjBody(codec, svc("my-service", "default", v1.ServiceTypeClusterIP))}, nil
						case p == "/namespaces/default/services/lb-service" && m == "GET":
							return &http.Response{StatusCode: http.StatusOK, Header: header, Body: ObjBody(codec, svc("lb-service", "default", v1.ServiceTypeLoadBalancer))}, nil
						case p == "/namespaces/default/daemonsets/nginx-ds" && m == "GET":
							return &http.Response{StatusCode: http.StatusOK, Header: header, Body: ObjBody(codec, ds("nginx-ds", "default", appsv1.RollingUpdateDaemonSetStrategyType, false))}, nil
						case p == "/namespaces/default/statefulsets/nginx-ss" && m == "GET":
							return &http.Response{StatusCode: http.StatusOK, Header: header, Body: ObjBody(codec, ss("nginx-ss", "default", appsv1.RollingUpdateStatefulSetStrategyType, false))}, nil
						case p == "/namespaces/default/ingress/test-ingress" && m == "GET":
							return &http.Response{StatusCode: http.StatusOK, Header: header, Body: ObjBody(codec, ing("test-ingress", "default", false))}, nil
						default:
							t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
							return nil, nil
						}
					}),
				}, nil
			},
			configFlags.ToRESTMapper,
			func() (restmapper.CategoryExpander, error) {
				return resource.FakeCategoryExpander, nil
			},
		)
	}
}

type mockAWSClients struct {
	AWSSession *session.Session
	AWSClientsIface
}

func NewMockClient(t *testing.T, m *Model) *Clients {
	t.Helper()
	h := ActionConfigFixture(t)
	makeMeSomeReleases(h.Releases, t)
	c := &Clients{
		ResourceBuilder: newFakeBuilder(t),
		ClientSet: fakeclientset.NewSimpleClientset(
			dep("nginx-deployment", "default", false),
			dep("nginx-deployment-foo", "default", true),
			svc("my-service", "default", v1.ServiceTypeClusterIP),
			svc("lb-service", "default", v1.ServiceTypeLoadBalancer),
			ds("nginx-ds", "default", appsv1.RollingUpdateDaemonSetStrategyType, false),
			ss("nginx-ss", "default", appsv1.RollingUpdateStatefulSetStrategyType, false),
			ing("test-ingress", "default", false),
			//crd("test-crd", "default", false, false),
			//crd("test-crd-foo", "default", true, false),
			//crdBeta("test-crd-beta", "default", false, false),
			//crdBeta("test-crd-beta-foo", "default", true, false),
		),
		HelmClient: h,
		Settings:   cli.New(),
	}
	c.AWSClients = &mockAWSClients{AWSSession: MockSession}
	if m != nil {
		c.LambdaResource = newLambdaResource(c.AWSClients.STSClient(nil, nil), m.ClusterID, m.KubeConfig, m.VPCConfiguration)
	}
	return c
}

func ObjBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser {
	return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
}

func testRESTMapper() meta.RESTMapper {
	groupResources := testDynamicResources()
	mapper := restmapper.NewDiscoveryRESTMapper(groupResources)
	// for backwards compatibility with existing tests, allow rest mappings from the scheme to show up
	// TODO: make this opt-in?
	mapper = meta.FirstHitRESTMapper{
		MultiRESTMapper: meta.MultiRESTMapper{
			mapper,
			testrestmapper.TestOnlyStaticRESTMapper(runtime.NewScheme()),
		},
	}

	fakeDs := &fakeCachedDiscoveryClient{}
	expander := restmapper.NewShortcutExpander(mapper, fakeDs)
	return expander
}

func testDynamicResources() []*restmapper.APIGroupResources {
	return []*restmapper.APIGroupResources{
		{
			Group: metav1.APIGroup{
				Versions: []metav1.GroupVersionForDiscovery{
					{Version: "v1"},
				},
				PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1"},
			},
			VersionedResources: map[string][]metav1.APIResource{
				"v1": {
					{Name: "pods", Namespaced: true, Kind: "Pod"},
					{Name: "services", Namespaced: true, Kind: "Service"},
					{Name: "replicationcontrollers", Namespaced: true, Kind: "ReplicationController"},
					{Name: "componentstatuses", Namespaced: false, Kind: "ComponentStatus"},
					{Name: "nodes", Namespaced: false, Kind: "Node"},
					{Name: "secrets", Namespaced: true, Kind: "Secret"},
					{Name: "configmaps", Namespaced: true, Kind: "ConfigMap"},
					{Name: "namespacedtype", Namespaced: true, Kind: "NamespacedType"},
					{Name: "namespaces", Namespaced: false, Kind: "Namespace"},
					{Name: "resourcequotas", Namespaced: true, Kind: "ResourceQuota"},
				},
			},
		},
		{
			Group: metav1.APIGroup{
				Name: "extensions",
				Versions: []metav1.GroupVersionForDiscovery{
					{Version: "v1beta1"},
				},
				PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1beta1"},
			},
			VersionedResources: map[string][]metav1.APIResource{
				"v1beta1": {
					{Name: "deployments", Namespaced: true, Kind: "Deployment"},
					{Name: "replicasets", Namespaced: true, Kind: "ReplicaSet"},
				},
			},
		},
		{
			Group: metav1.APIGroup{
				Name: "apps",
				Versions: []metav1.GroupVersionForDiscovery{
					{Version: "v1beta1"},
					{Version: "v1beta2"},
					{Version: "v1"},
				},
				PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1"},
			},
			VersionedResources: map[string][]metav1.APIResource{
				"v1beta1": {
					{Name: "deployments", Namespaced: true, Kind: "Deployment"},
					{Name: "replicasets", Namespaced: true, Kind: "ReplicaSet"},
				},
				"v1beta2": {
					{Name: "deployments", Namespaced: true, Kind: "Deployment"},
				},
				"v1": {
					{Name: "deployments", Namespaced: true, Kind: "Deployment"},
					{Name: "replicasets", Namespaced: true, Kind: "ReplicaSet"},
					{Name: "statefulsets", Namespaced: true, Kind: "StatefulSet"},
					{Name: "daemonsets", Namespaced: true, Kind: "DaemonSet"},
				},
			},
		},
		{
			Group: metav1.APIGroup{
				Name: "networking.k8s.io",
				Versions: []metav1.GroupVersionForDiscovery{
					{Version: "v1beta1"},
					{Version: "v0"},
				},
				PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1beta1"},
			},
			VersionedResources: map[string][]metav1.APIResource{
				"v1beta1": {
					{Name: "ingress", Namespaced: true, Kind: "Ingress"},
				},
			},
		},
		{
			Group: metav1.APIGroup{
				Name: "apiextensions.k8s.io",
				Versions: []metav1.GroupVersionForDiscovery{
					{Version: "v1beta1"},
					{Version: "v1"},
				},
				PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1"},
			},
			VersionedResources: map[string][]metav1.APIResource{
				"v1beta1": {
					{Name: "customresourcedefinition", Namespaced: true, Kind: "CustomResourceDefinition"},
				},
				"v1": {
					{Name: "customresourcedefinition", Namespaced: true, Kind: "CustomResourceDefinition"},
				},
			},
		},
	}
}

func ActionConfigFixture(t *testing.T) *action.Configuration {
	t.Helper()
	var verbose = aws.Bool(false)
	return &action.Configuration{
		Releases:     storage.Init(driver.NewMemory()),
		KubeClient:   &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: ioutil.Discard}},
		Capabilities: chartutil.DefaultCapabilities,
		Log: func(format string, v ...interface{}) {
			t.Helper()
			if *verbose {
				t.Logf(format, v...)
			}
		},
	}
}

func awsRequest(op *request.Operation, input, output interface{}) *request.Request {
	c := MockSession.ClientConfig("Mock", aws.NewConfig().WithRegion("us-east-2"))
	metaR := metadata.ClientInfo{
		ServiceName:   "Mock",
		SigningRegion: c.SigningRegion,
		Endpoint:      c.Endpoint,
		APIVersion:    "2015-12-08",
		JSONVersion:   "1.1",
		TargetPrefix:  "MockServer",
	}
	return request.New(*c.Config, metaR, c.Handlers, nil, op, input, output)
}

func makeMeSomeReleases(store *storage.Storage, t *testing.T) {
	t.Helper()
	one := namedRelease("one", release.StatusDeployed)
	one.Namespace = "default"
	one.Version = 1
	one.Manifest = TestManifest
	two := namedRelease("two", release.StatusFailed)
	two.Namespace = "default"
	two.Version = 2
	two.Manifest = TestManifest
	three := namedRelease("three", release.StatusDeployed)
	three.Namespace = "default"
	three.Version = 3
	three.Manifest = TestPendingManifest
	four := namedRelease("four", "unknown)")
	four.Namespace = "default"
	four.Version = 3
	four.Manifest = TestManifest
	five := namedRelease("five", release.StatusPendingUpgrade)
	five.Namespace = "default"
	five.Version = 3
	five.Manifest = TestManifest
	update := namedRelease("update", release.StatusDeployed)
	update.Namespace = "default"
	update.Info.Description = "eyJDbHVzdGVySUQiOiJla3MiLCJSZWdpb24iOiJldS13ZXN0LTEiLCJOYW1lIjoib25lIiwiTmFtZXNwYWNlIjoiZGVmYXVsdCJ9"
	update.Version = 1
	update.Manifest = TestManifest

	for _, rel := range []*release.Release{one, two, three, four, five} {
		if err := store.Create(rel); err != nil {
			t.Fatal(err)
		}
	}
}

func namedRelease(name string, status release.Status) *release.Release {
	now := htime.Now()
	return &release.Release{
		Name: name,
		Info: &release.Info{
			FirstDeployed: now,
			LastDeployed:  now,
			Status:        status,
			Description:   "umock-id",
		},
		Chart:   buildChart(),
		Version: 1,
	}
}

func buildChart(opts ...chartOption) *chart.Chart {
	c := &chartOptions{
		Chart: &chart.Chart{
			// TODO: This should be more complete.
			Metadata: &chart.Metadata{
				APIVersion: "v1",
				Name:       "hello",
				Version:    "0.1.0",
			},
			// This adds a basic template and hooks.
			Templates: []*chart.File{
				{Name: "templates/temp", Data: []byte(TestManifest)},
			},
		},
	}

	for _, opt := range opts {
		opt(c)
	}

	return c.Chart
}

func svc(name string, namespace string, sType v1.ServiceType) *v1.Service {
	var ingress []v1.LoadBalancerIngress
	if sType == v1.ServiceTypeLoadBalancer {
		ingress = []v1.LoadBalancerIngress{{Hostname: "elb.test.com"}}
	}
	return &v1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Spec: corev1.ServiceSpec{
			Type:      sType,
			ClusterIP: "127.0.0.1",
		},
		Status: corev1.ServiceStatus{
			LoadBalancer: v1.LoadBalancerStatus{
				Ingress: ingress,
			},
		},
	}
}

func dep(name string, namespace string, pending bool) *appsv1.Deployment {
	count := int32(1)
	rcount := int32(1)
	if pending {
		rcount = int32(0)
	}
	return &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: aws.Int32(count),
		},
		Status: appsv1.DeploymentStatus{
			ReadyReplicas: rcount,
		},
	}
}

func ds(name string, namespace string, dtype appsv1.DaemonSetUpdateStrategyType, pending bool) *appsv1.DaemonSet {
	count := int32(1)
	rcount := int32(1)
	dcount := int32(1)
	ucount := int32(1)
	if pending {
		dcount = int32(1)
		rcount = int32(0)
		count = int32(1)
		ucount = int32(0)
	}
	updateS := appsv1.DaemonSetUpdateStrategy{Type: dtype}
	if dtype == appsv1.RollingUpdateDaemonSetStrategyType {
		maxU := intstr.FromInt(0)
		updateS = appsv1.DaemonSetUpdateStrategy{Type: dtype,
			RollingUpdate: &appsv1.RollingUpdateDaemonSet{
				MaxUnavailable: &maxU,
			}}
	}
	return &appsv1.DaemonSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Spec: appsv1.DaemonSetSpec{
			UpdateStrategy: updateS,
		},
		Status: appsv1.DaemonSetStatus{
			DesiredNumberScheduled: dcount,
			NumberReady:            rcount,
			NumberAvailable:        count,
			UpdatedNumberScheduled: ucount,
		},
	}
}

func ss(name string, namespace string, dtype appsv1.StatefulSetUpdateStrategyType, pending bool) *appsv1.StatefulSet {
	count := int32(2)
	rcount := int32(2)
	ucount := int32(1)
	if pending {
		rcount = int32(0)
		ucount = int32(1)
	}
	updateS := appsv1.StatefulSetUpdateStrategy{Type: dtype}
	if dtype == appsv1.RollingUpdateStatefulSetStrategyType {
		updateS = appsv1.StatefulSetUpdateStrategy{Type: dtype,
			RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
				Partition: aws.Int32(1),
			}}
	}
	return &appsv1.StatefulSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Spec: appsv1.StatefulSetSpec{
			Replicas:       aws.Int32(count),
			UpdateStrategy: updateS,
		},
		Status: appsv1.StatefulSetStatus{
			ReadyReplicas:   rcount,
			UpdatedReplicas: ucount,
		},
	}
}

func ing(name string, namespace string, pending bool) *v1beta1.Ingress {
	var ingress []v1.LoadBalancerIngress
	if !pending {
		ingress = []v1.LoadBalancerIngress{{Hostname: "ingress.test.com"}}
	}
	return &v1beta1.Ingress{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Status: v1beta1.IngressStatus{
			LoadBalancer: v1.LoadBalancerStatus{
				Ingress: ingress,
			},
		},
	}
}

func ingN(name string, namespace string, pending bool) *networkingv1beta1.Ingress {
	var ingress []v1.LoadBalancerIngress
	if !pending {
		ingress = []v1.LoadBalancerIngress{{Hostname: "ingressN.test.com"}}
	}
	return &networkingv1beta1.Ingress{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Status: networkingv1beta1.IngressStatus{
			LoadBalancer: v1.LoadBalancerStatus{
				Ingress: ingress,
			},
		},
	}
}

func ns(name string) *v1.Namespace {
	return &v1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
}

func vol(name string, namespace string, pending bool) *corev1.PersistentVolumeClaim {
	p := corev1.ClaimBound
	if pending {
		p = corev1.ClaimPending
	}
	return &corev1.PersistentVolumeClaim{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Status: corev1.PersistentVolumeClaimStatus{
			Phase: p,
		},
	}
}

func crd(name string, namespace string, namesAccepted bool, pending bool) *apiextv1.CustomResourceDefinition {
	s := apiextv1.ConditionTrue
	if pending {
		s = apiextv1.ConditionFalse
	}
	c := []apiextv1.CustomResourceDefinitionCondition{{
		Type:   apiextv1.Established,
		Status: s,
	},
	}
	switch {
	case namesAccepted && !pending:
		c = []apiextv1.CustomResourceDefinitionCondition{{
			Type:   apiextv1.NamesAccepted,
			Status: apiextv1.ConditionFalse,
		},
		}
	case namesAccepted && pending:
		c = []apiextv1.CustomResourceDefinitionCondition{{
			Type:   apiextv1.NamesAccepted,
			Status: apiextv1.ConditionTrue,
		},
		}
	}

	return &apiextv1.CustomResourceDefinition{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Status: apiextv1.CustomResourceDefinitionStatus{Conditions: c},
	}
}

func crdBeta(name string, namespace string, namesAccepted bool, pending bool) *apiextv1beta1.CustomResourceDefinition {
	s := apiextv1beta1.ConditionTrue
	if pending {
		s = apiextv1beta1.ConditionFalse
	}
	c := []apiextv1beta1.CustomResourceDefinitionCondition{{
		Type:   apiextv1beta1.Established,
		Status: s,
	},
	}
	switch {
	case namesAccepted && !pending:
		c = []apiextv1beta1.CustomResourceDefinitionCondition{{
			Type:   apiextv1beta1.NamesAccepted,
			Status: apiextv1beta1.ConditionFalse,
		},
		}
	case namesAccepted && pending:
		c = []apiextv1beta1.CustomResourceDefinitionCondition{{
			Type:   apiextv1beta1.NamesAccepted,
			Status: apiextv1beta1.ConditionTrue,
		},
		}
	}

	return &apiextv1beta1.CustomResourceDefinition{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Status: apiextv1beta1.CustomResourceDefinitionStatus{Conditions: c},
	}
}