/*
  This file is part of Paivana.
  Copyright (C) 2026 Taler Systems SA

  Paivana is free software; you can redistribute it and/or
  modify it under the terms of the GNU Affero General Public License
  as published by the Free Software Foundation; either version
  3, or (at your option) any later version.

  Paivana is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public
  License along with Paivana; see the file COPYING.  If not,
  write to the Free Software Foundation, Inc., 51 Franklin
  Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

// upstream_go: Go-based upstream HTTP server used by the
// paivana reverse-proxy tests.  Implements the same small set
// of canned endpoints as upstream_mhd.c.

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"
)

const upstreamName = "go"

func setHeaders(w http.ResponseWriter) {
	w.Header().Set("X-Upstream", upstreamName)
}

func hello(w http.ResponseWriter, r *http.Request) {
	setHeaders(w)
	w.Header().Set("Content-Type", "text/plain")
	if r.Method == http.MethodHead {
		w.WriteHeader(200)
		return
	}
	fmt.Fprintf(w, "Hello from %s\n", upstreamName)
}

func status(w http.ResponseWriter, r *http.Request) {
	setHeaders(w)
	path := strings.TrimPrefix(r.URL.Path, "/status/")
	code, err := strconv.Atoi(path)
	if err != nil || code < 100 || code > 599 {
		code = 500
	}
	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(code)
	fmt.Fprintf(w, "status %d\n", code)
}

func large(w http.ResponseWriter, r *http.Request) {
	setHeaders(w)
	path := strings.TrimPrefix(r.URL.Path, "/large/")
	n, err := strconv.Atoi(path)
	if err != nil || n < 0 {
		n = 0
	}
	if n > 10*1024*1024 {
		n = 10 * 1024 * 1024
	}
	buf := make([]byte, n)
	for i := 0; i < n; i++ {
		buf[i] = byte('A' + i%26)
	}
	w.Header().Set("Content-Type", "application/octet-stream")
	w.WriteHeader(200)
	w.Write(buf)
}

func slow(w http.ResponseWriter, r *http.Request) {
	setHeaders(w)
	path := strings.TrimPrefix(r.URL.Path, "/slow/")
	ms, err := strconv.Atoi(path)
	if err != nil || ms < 0 {
		ms = 0
	}
	if ms > 30000 {
		ms = 30000
	}
	time.Sleep(time.Duration(ms) * time.Millisecond)
	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(200)
	fmt.Fprint(w, "slept\n")
}

func echo(w http.ResponseWriter, r *http.Request) {
	setHeaders(w)
	w.Header().Set("Content-Type", "application/octet-stream")
	body, _ := io.ReadAll(r.Body)
	w.WriteHeader(200)
	w.Write(body)
}

func upload(w http.ResponseWriter, r *http.Request, label string) {
	setHeaders(w)
	body, _ := io.ReadAll(r.Body)
	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(200)
	fmt.Fprintf(w, "%s %d bytes\n", label, len(body))
}

func echoHeaders(w http.ResponseWriter, r *http.Request) {
	setHeaders(w)
	var keys []string
	for k := range r.Header {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(200)
	for _, k := range keys {
		for _, v := range r.Header[k] {
			fmt.Fprintf(w, "%s: %s\n", k, v)
		}
	}
	// Host isn't in r.Header
	if r.Host != "" {
		fmt.Fprintf(w, "Host: %s\n", r.Host)
	}
}

// connResponse names two of its own response headers in Connection;
// a conforming proxy must strip both (RFC 9110 §7.6.1) but keep
// X-Keep-Resp.
func connResponse(w http.ResponseWriter, r *http.Request) {
	setHeaders(w)
	w.Header().Set("X-Hop-Before", "must-not-leak")
	w.Header().Set("Connection", "X-Hop-Before, X-Hop-After")
	w.Header().Set("X-Hop-After", "must-not-leak")
	w.Header().Set("X-Keep-Resp", "survivor")
	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(200)
	fmt.Fprint(w, "conn\n")
}

func deleteItem(w http.ResponseWriter, r *http.Request) {
	setHeaders(w)
	w.WriteHeader(204)
}

func options(w http.ResponseWriter, r *http.Request) {
	setHeaders(w)
	w.Header().Set("Allow", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS")
	w.WriteHeader(204)
}

func route(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodOptions {
		options(w, r)
		return
	}
	switch {
	case r.URL.Path == "/hello":
		hello(w, r)
	case strings.HasPrefix(r.URL.Path, "/status/"):
		status(w, r)
	case strings.HasPrefix(r.URL.Path, "/large/"):
		large(w, r)
	case strings.HasPrefix(r.URL.Path, "/slow/"):
		slow(w, r)
	case r.URL.Path == "/echo" && r.Method == http.MethodPost:
		echo(w, r)
	case r.URL.Path == "/upload" && r.Method == http.MethodPost:
		upload(w, r, "Received")
	case r.URL.Path == "/put" && r.Method == http.MethodPut:
		upload(w, r, "PUT received")
	case r.URL.Path == "/patch" && r.Method == http.MethodPatch:
		upload(w, r, "PATCH received")
	case strings.HasPrefix(r.URL.Path, "/item") && r.Method == http.MethodDelete:
		deleteItem(w, r)
	case r.URL.Path == "/echo-headers":
		echoHeaders(w, r)
	case r.URL.Path == "/conn-response":
		connResponse(w, r)
	default:
		w.Header().Set("Content-Type", "text/plain")
		w.WriteHeader(404)
		fmt.Fprint(w, "not found\n")
	}
}

func main() {
	port := 8402
	if len(os.Args) > 1 {
		// ":0" would bind an ephemeral port while the driver waits
		// for the one it asked for; ":garbage" would fail five
		// seconds into that wait, blaming the wrong thing.
		n, err := strconv.Atoi(os.Args[1])
		if err != nil || n < 1 || n > 65535 {
			fmt.Fprintf(os.Stderr, "invalid port %q\n", os.Args[1])
			os.Exit(1)
		}
		port = n
	}
	// Loopback only: this server echoes an arbitrary POST body back
	// and hands out 10 MiB on request, and has no business being
	// reachable from the network for the duration of `make check'.
	srv := &http.Server{
		Addr:    fmt.Sprintf("127.0.0.1:%d", port),
		Handler: http.HandlerFunc(route),
	}
	fmt.Fprintf(os.Stderr, "upstream_go listening on port %d\n", port)
	if err := srv.ListenAndServe(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
