package main

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"io"
	"math/big"
	"net"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/apernet/quic-go"
	"github.com/apernet/quic-go/http3"
	"war3replays/w3cs-wt-bridge/webtransport"
)

func TestSplitListenAddrs(t *testing.T) {
	addrs, err := splitListenAddrs(":443, :4443,,")
	if err != nil {
		t.Fatal(err)
	}
	if strings.Join(addrs, "|") != ":443|:4443" {
		t.Fatalf("addrs = %q", addrs)
	}
	if _, err := splitListenAddrs(" , "); err == nil {
		t.Fatal("an empty list must be rejected")
	}
}

// One address that cannot bind fails the start as a whole: a bridge that
// silently serves only the secondary port would cost every viewer a full
// first try on the primary.
func TestListenAllIsAllOrNothing(t *testing.T) {
	taken, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
	if err != nil {
		t.Fatal(err)
	}
	defer taken.Close()
	conns, err := listenAll([]string{"127.0.0.1:0", taken.LocalAddr().String()})
	if err == nil {
		for _, conn := range conns {
			conn.Close()
		}
		t.Fatal("binding a taken port must fail")
	}
	if !strings.Contains(err.Error(), taken.LocalAddr().String()) {
		t.Fatalf("error must name the address: %v", err)
	}
	if conns != nil {
		t.Fatal("no sockets may stay bound after a failed start")
	}
}

func selfSignedCert(t *testing.T) tls.Certificate {
	t.Helper()
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	template := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject:      pkix.Name{CommonName: "wt-bridge-test"},
		NotBefore:    time.Now().Add(-time.Hour),
		NotAfter:     time.Now().Add(time.Hour),
		IPAddresses:  []net.IP{net.IPv4(127, 0, 0, 1)},
		KeyUsage:     x509.KeyUsageDigitalSignature,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
	}
	der, err := x509.CreateCertificate(rand.Reader, template, template,
		&key.PublicKey, key)
	if err != nil {
		t.Fatal(err)
	}
	return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}
}

// Every bound socket serves the same bridge, and a request knows which
// endpoint its connection arrived on (the "via" in the session log that
// measures the UDP 443 pilot).
func TestEverySocketServesTheSameBridgeAndNamesItsEndpoint(t *testing.T) {
	cert := selfSignedCert(t)
	wtServer := &webtransport.Server{
		CheckOrigin: func(*http.Request) bool { return true },
		H3: &http3.Server{
			TLSConfig: &tls.Config{
				Certificates: []tls.Certificate{cert},
				NextProtos:   []string{http3.NextProtoH3},
			},
			EnableDatagrams: true,
			QUICConfig: &quic.Config{
				EnableDatagrams: true,
				MaxIdleTimeout:  5 * time.Second,
			},
		},
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
		io.WriteString(w, listenAddrFrom(r.Context()))
	})
	wtServer.H3.Handler = mux

	conns, err := listenAll([]string{"127.0.0.1:0", "127.0.0.1:0"})
	if err != nil {
		t.Fatal(err)
	}
	endpoints := make([]string, len(conns))
	for i, conn := range conns {
		endpoints[i] = conn.LocalAddr().String()
	}
	if endpoints[0] == endpoints[1] {
		t.Fatalf("two sockets, one address: %v", endpoints)
	}
	served := make(chan error, 1)
	go func() { served <- serveAll(wtServer, conns) }()
	defer func() {
		for _, conn := range conns {
			conn.Close()
		}
		select {
		case <-served:
		case <-time.After(5 * time.Second):
			t.Error("serveAll did not return after its sockets closed")
		}
	}()

	for _, endpoint := range endpoints {
		transport := &http3.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true,
				NextProtos: []string{http3.NextProtoH3}},
		}
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		req, err := http.NewRequestWithContext(ctx, http.MethodGet,
			"https://"+endpoint+"/ping", nil)
		if err != nil {
			cancel()
			t.Fatal(err)
		}
		response, err := transport.RoundTrip(req)
		if err != nil {
			cancel()
			t.Fatalf("GET via %s: %v", endpoint, err)
		}
		body, err := io.ReadAll(response.Body)
		response.Body.Close()
		transport.Close()
		cancel()
		if err != nil {
			t.Fatal(err)
		}
		if string(body) != endpoint {
			t.Fatalf("request via %s was tagged %q", endpoint, body)
		}
	}
}
