package gateway

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"math/big"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"
)

func TestServerOwnsTLSAndGracefulTransportDrain(t *testing.T) {
	entered := make(chan struct{})
	release := make(chan struct{})
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		close(entered)
		<-release
		w.WriteHeader(http.StatusNoContent)
	}))
	defer upstream.Close()
	gateway := newTestGateway(t, upstream.URL, 1, time.Second, 1024)
	certFile, keyFile := writeTestCertificate(t)
	server, err := NewServer(gateway, ServerConfig{
		ListenAddress:   "127.0.0.1:0",
		TLSCertFile:     certFile,
		TLSKeyFile:      keyFile,
		ShutdownTimeout: time.Second + downstreamWriteHeadroom,
	})
	if err != nil {
		t.Fatal(err)
	}
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithCancel(context.Background())
	runDone := make(chan error, 1)
	go func() { runDone <- server.runOnListener(ctx, listener) }()
	client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}}} //nolint:gosec -- test certificate
	baseURL := "https://" + listener.Addr().String()
	waitForTLSReady(t, client, baseURL+"/readyz")
	oldTLSClient := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, MaxVersion: tls.VersionTLS11}}} //nolint:gosec -- deliberate old-version probe
	request, _ := http.NewRequest(http.MethodGet, baseURL+"/livez", nil)
	if _, err := oldTLSClient.Do(request); err == nil {
		t.Fatal("TLS 1.1 unexpectedly reached the TLS edge")
	}
	clientDone := make(chan int, 1)
	go func() {
		call, _ := http.NewRequest(http.MethodPost, baseURL+"/api/v1/items/access", nil)
		response, requestErr := client.Do(call)
		if requestErr != nil {
			clientDone <- 0
			return
		}
		defer response.Body.Close()
		clientDone <- response.StatusCode
	}()
	<-entered
	cancel()
	select {
	case err := <-runDone:
		t.Fatalf("server returned before admitted response completed: %v", err)
	case <-time.After(30 * time.Millisecond):
	}
	if gateway.Ready() {
		t.Fatal("server cancellation did not remove readiness before drain")
	}
	close(release)
	if status := <-clientDone; status != http.StatusNoContent {
		t.Fatalf("client status = %d", status)
	}
	if err := <-runDone; err != nil {
		t.Fatal(err)
	}
}

func TestNewServerRequiresTLSMaterial(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
	defer upstream.Close()
	gateway := newTestGateway(t, upstream.URL, 1, time.Second, 1024)
	if _, err := NewServer(gateway, ServerConfig{ListenAddress: ":8443"}); err == nil {
		t.Fatal("server accepted missing TLS material")
	}
}

func TestServerDelegatesUpgradeRejectionToGatewayProjection(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		t.Fatal("upgrade reached upstream")
	}))
	defer upstream.Close()
	gateway := newTestGateway(t, upstream.URL, 1, time.Second, 1024)
	certFile, keyFile := writeTestCertificate(t)
	server, err := NewServer(gateway, ServerConfig{
		ListenAddress: "127.0.0.1:0", TLSCertFile: certFile, TLSKeyFile: keyFile,
	})
	if err != nil {
		t.Fatal(err)
	}
	request := httptest.NewRequest(http.MethodPost, "/api/v1/items/access", nil)
	request.Header.Set("Connection", "Upgrade")
	request.Header.Set("Upgrade", "websocket")
	response := httptest.NewRecorder()
	server.server.Handler.ServeHTTP(response, request)
	if response.Code != http.StatusBadRequest || response.Header().Get("Content-Type") != "application/json" {
		t.Fatalf("upgrade response = %d content-type=%q body=%q", response.Code, response.Header().Get("Content-Type"), response.Body.String())
	}
	if response.Header().Get("X-Content-Type-Options") != "nosniff" {
		t.Fatal("upgrade rejection bypassed Gateway security headers")
	}
}

func TestServerBoundsDownstreamWritesBeyondTheLargestRouteDeadline(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
	defer upstream.Close()
	gateway := newTestGateway(t, upstream.URL, 1, 3*time.Second, 1024)
	certFile, keyFile := writeTestCertificate(t)
	server, err := NewServer(gateway, ServerConfig{
		ListenAddress: "127.0.0.1:0",
		TLSCertFile:   certFile,
		TLSKeyFile:    keyFile,
	})
	if err != nil {
		t.Fatal(err)
	}
	want := 3*time.Second + downstreamWriteHeadroom
	if server.server.WriteTimeout != want {
		t.Fatalf("write timeout = %v, want %v", server.server.WriteTimeout, want)
	}
	if server.shutdownTimeout != want {
		t.Fatalf("default shutdown timeout = %v, want route-derived %v", server.shutdownTimeout, want)
	}
	if _, err := NewServer(gateway, ServerConfig{
		ListenAddress: "127.0.0.1:0", TLSCertFile: certFile, TLSKeyFile: keyFile,
		ShutdownTimeout: want - time.Millisecond,
	}); err == nil {
		t.Fatal("server accepted shutdown timeout shorter than its complete response lifecycle")
	}
}

func waitForTLSReady(t *testing.T, client *http.Client, url string) {
	t.Helper()
	deadline := time.Now().Add(time.Second)
	for time.Now().Before(deadline) {
		response, err := client.Get(url)
		if err == nil {
			response.Body.Close()
			if response.StatusCode == http.StatusOK {
				return
			}
		}
		time.Sleep(5 * time.Millisecond)
	}
	t.Fatal("TLS gateway did not become ready")
}

func writeTestCertificate(t *testing.T) (string, string) {
	t.Helper()
	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	template := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject:      pkix.Name{CommonName: "localhost"},
		NotBefore:    time.Now().Add(-time.Minute),
		NotAfter:     time.Now().Add(time.Hour),
		KeyUsage:     x509.KeyUsageDigitalSignature,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		IPAddresses:  []net.IP{net.ParseIP("127.0.0.1")},
	}
	certificateDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
	if err != nil {
		t.Fatal(err)
	}
	keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
	if err != nil {
		t.Fatal(err)
	}
	directory := t.TempDir()
	certFile := filepath.Join(directory, "tls.crt")
	keyFile := filepath.Join(directory, "tls.key")
	if err := os.WriteFile(certFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificateDER}), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), 0o600); err != nil {
		t.Fatal(err)
	}
	return certFile, keyFile
}
