/*
Copyright 2018 The Knative Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package webhook

import (
	"context"
	"fmt"
	"io"
	"net"
	"net/http"
	"strings"
	"testing"
	"time"

	kubeclient "knative.dev/pkg/client/injection/kube/client/fake"
	_ "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/fake"
	"knative.dev/pkg/system"

	"golang.org/x/sync/errgroup"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"knative.dev/pkg/metrics/metricstest"
	pkgtest "knative.dev/pkg/testing"
	certresources "knative.dev/pkg/webhook/certificates/resources"
)

// createResource creates a testing.Resource with the given name in the system namespace.
func createResource(name string) *pkgtest.Resource {
	return &pkgtest.Resource{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: system.Namespace(),
			Name:      name,
		},
		Spec: pkgtest.ResourceSpec{
			FieldWithValidation: "magic value",
		},
	}
}

const testTimeout = 10 * time.Second

func TestMissingContentType(t *testing.T) {
	wh, serverURL, ctx, cancel, err := testSetup(t)
	if err != nil {
		t.Fatal("testSetup() =", err)
	}

	eg, _ := errgroup.WithContext(ctx)
	eg.Go(func() error { return wh.Run(ctx.Done()) })
	wh.InformersHaveSynced()
	defer func() {
		cancel()
		if err := eg.Wait(); err != nil {
			t.Error("Unable to run controller:", err)
		}
	}()

	if err = waitForServerAvailable(t, serverURL, testTimeout); err != nil {
		t.Fatal("waitForServerAvailable() =", err)
	}

	tlsClient, err := createSecureTLSClient(t, kubeclient.Get(ctx), &wh.Options)
	if err != nil {
		t.Fatal("createSecureTLSClient() =", err)
	}

	req, err := http.NewRequest(http.MethodGet, "https://"+serverURL, nil)
	if err != nil {
		t.Fatal("http.NewRequest() =", err)
	}

	response, err := tlsClient.Do(req)
	if err != nil {
		t.Fatalf("Received %v error from server %s", err, serverURL)
	}

	if got, want := response.StatusCode, http.StatusUnsupportedMediaType; got != want {
		t.Errorf("Response status code = %v, wanted %v", got, want)
	}

	defer response.Body.Close()
	responseBody, err := io.ReadAll(response.Body)
	if err != nil {
		t.Fatal("Failed to read response body", err)
	}

	if !strings.Contains(string(responseBody), "invalid Content-Type") {
		t.Errorf("Response body to contain 'invalid Content-Type' , got = '%s'", string(responseBody))
	}

	// Stats are not reported for internal server errors
	metricstest.CheckStatsNotReported(t, requestCountName, requestLatenciesName)
}

func TestServerWithCustomSecret(t *testing.T) {
	wh, serverURL, ctx, cancel, err := testSetupCustomSecret(t)
	if err != nil {
		t.Fatal("testSetup() =", err)
	}

	eg, _ := errgroup.WithContext(ctx)
	eg.Go(func() error { return wh.Run(ctx.Done()) })
	wh.InformersHaveSynced()
	defer func() {
		cancel()
		if err := eg.Wait(); err != nil {
			t.Error("Unable to run controller:", err)
		}
	}()

	pollErr := waitForServerAvailable(t, serverURL, testTimeout)
	if pollErr != nil {
		t.Fatal("waitForServerAvailable() =", err)
	}
}

func testEmptyRequestBody(t *testing.T, controller interface{}) {
	wh, serverURL, ctx, cancel, err := testSetup(t, controller)
	if err != nil {
		t.Fatal("testSetup() =", err)
	}

	eg, _ := errgroup.WithContext(ctx)
	eg.Go(func() error { return wh.Run(ctx.Done()) })
	wh.InformersHaveSynced()
	defer func() {
		cancel()
		if err := eg.Wait(); err != nil {
			t.Error("Unable to run controller:", err)
		}
	}()

	if err = waitForServerAvailable(t, serverURL, testTimeout); err != nil {
		t.Fatal("waitForServerAvailable() =", err)
	}

	tlsClient, err := createSecureTLSClient(t, kubeclient.Get(ctx), &wh.Options)
	if err != nil {
		t.Fatal("createSecureTLSClient() =", err)
	}

	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/bazinga", serverURL), nil)
	if err != nil {
		t.Fatal("http.NewRequest() =", err)
	}

	req.Header.Add("Content-Type", "application/json")

	response, err := tlsClient.Do(req)
	if err != nil {
		t.Fatal("failed to get resp", err)
	}

	if got, want := response.StatusCode, http.StatusBadRequest; got != want {
		t.Errorf("Response status code = %v, wanted %v", got, want)
	}
	defer response.Body.Close()

	responseBody, err := io.ReadAll(response.Body)
	if err != nil {
		t.Fatal("Failed to read response body", err)
	}

	if !strings.Contains(string(responseBody), "could not decode body") {
		t.Errorf("Response body to contain 'decode failure information' , got = %q", string(responseBody))
	}
}

func TestSetupWebhookHTTPServerError(t *testing.T) {
	defaultOpts := newDefaultOptions()
	defaultOpts.Port = -1 // invalid port
	ctx, wh, cancel := newNonRunningTestWebhook(t, defaultOpts)
	defer cancel()
	kubeClient := kubeclient.Get(ctx)

	nsErr := createNamespace(t, kubeClient, metav1.NamespaceSystem)
	if nsErr != nil {
		t.Fatal("createNamespace() =", nsErr)
	}
	cMapsErr := createTestConfigMap(t, kubeClient)
	if cMapsErr != nil {
		t.Fatal("createTestConfigMap() =", cMapsErr)
	}

	stopCh := make(chan struct{})
	errCh := make(chan error)
	go func() {
		if err := wh.Run(stopCh); err != nil {
			errCh <- err
		}
	}()

	select {
	case <-time.After(6 * time.Second):
		t.Error("Timeout in testing bootstrap webhook http server failed")
	case errItem := <-errCh:
		if !strings.Contains(errItem.Error(), "bootstrap failed") {
			t.Error("Expected bootstrap webhook http server failed")
		}
	}
}

func testSetup(t *testing.T, acs ...interface{}) (*Webhook, string, context.Context, context.CancelFunc, error) {
	t.Helper()

	// ephemeral port
	l, err := net.Listen("tcp", ":0")
	if err != nil {
		t.Fatal("unable to get ephemeral port: ", err)
	}

	defaultOpts := newDefaultOptions()

	ctx, wh, cancel := newNonRunningTestWebhook(t, defaultOpts, acs...)
	wh.testListener = l

	// Create certificate
	secret, err := certresources.MakeSecret(ctx, defaultOpts.SecretName, system.Namespace(), defaultOpts.ServiceName)
	if err != nil {
		t.Fatalf("failed to create certificate")
	}
	kubeClient := kubeclient.Get(ctx)

	if _, err := kubeClient.CoreV1().Secrets(secret.Namespace).Create(context.Background(), secret, metav1.CreateOptions{}); err != nil {
		t.Fatalf("failed to create secret")
	}

	resetMetrics()
	return wh, l.Addr().String(), ctx, cancel, nil
}

func testSetupCustomSecret(t *testing.T, acs ...interface{}) (*Webhook, string, context.Context, context.CancelFunc, error) {
	t.Helper()

	// ephemeral port
	l, err := net.Listen("tcp", ":0")
	if err != nil {
		t.Fatal("unable to get ephemeral port: ", err)
	}

	defaultOptions := newCustomOptions()

	ctx, wh, cancel := newNonRunningTestWebhook(t, defaultOptions, acs...)
	wh.testListener = l

	// Create certificate
	secret, err := customSecretWithOverrides(ctx, defaultOptions.SecretName, system.Namespace(), defaultOptions.ServiceName)
	if err != nil {
		t.Fatalf("failed to create certificate")
	}
	kubeClient := kubeclient.Get(ctx)

	if _, err := kubeClient.CoreV1().Secrets(secret.Namespace).Create(context.Background(), secret, metav1.CreateOptions{}); err != nil {
		t.Fatalf("failed to create secret")
	}

	resetMetrics()
	return wh, l.Addr().String(), ctx, cancel, nil
}

func testSetupNoTLS(t *testing.T, acs ...interface{}) (*Webhook, string, context.Context, context.CancelFunc, error) {
	t.Helper()

	// ephemeral port
	l, err := net.Listen("tcp", ":0")
	if err != nil {
		return nil, "", nil, nil, err
	}

	defaultOpts := newDefaultOptions()
	defaultOpts.SecretName = ""
	ctx, wh, cancel := newNonRunningTestWebhook(t, defaultOpts, acs...)
	wh.testListener = l

	resetMetrics()
	return wh, l.Addr().String(), ctx, cancel, nil
}
