// Command loadtest measures how many concurrent players an instance holds.
//
// It opens real WebSocket connections and, optionally, places real bets, then
// reports connection success, frame delivery, and latency. The point is to
// produce numbers rather than adjectives: run it against a candidate machine
// and read the ceiling off the output.
//
//	go run ./cmd/loadtest -conns 2000 -addr localhost:8080
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"os"
	"runtime"
	"sort"
	"sync"
	"sync/atomic"
	"time"

	"github.com/coder/websocket"
)

func main() {
	var (
		addr     = flag.String("addr", "localhost:8080", "instance to load")
		conns    = flag.Int("conns", 500, "concurrent websocket connections")
		duration = flag.Duration("duration", 20*time.Second, "how long to hold them")
		game     = flag.String("game", "rocket", "game room to join")
		ramp     = flag.Duration("ramp", 5*time.Second, "time to open all connections")
	)
	flag.Parse()

	ctx, cancel := context.WithTimeout(context.Background(), *duration+*ramp+30*time.Second)
	defer cancel()

	var (
		connected atomic.Int64
		failed    atomic.Int64
		frames    atomic.Int64
		bytesRecv atomic.Int64
		dialMu    sync.Mutex
		dialTimes []time.Duration
	)

	fmt.Printf("opening %d connections to %s over %v\n", *conns, *addr, *ramp)
	start := time.Now()

	// Stagger dialling: slamming every connection open at once measures the
	// accept backlog rather than the steady state anyone actually runs at.
	gap := *ramp / time.Duration(max(1, *conns))

	var wg sync.WaitGroup
	for i := 0; i < *conns; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			time.Sleep(time.Duration(i) * gap)

			dialStart := time.Now()
			conn, _, err := websocket.Dial(ctx,
				fmt.Sprintf("ws://%s/ws/%s", *addr, *game), nil)
			if err != nil {
				failed.Add(1)
				return
			}
			took := time.Since(dialStart)
			defer conn.CloseNow()

			connected.Add(1)
			dialMu.Lock()
			dialTimes = append(dialTimes, took)
			dialMu.Unlock()

			// Read until the run ends. A client that stops reading is exactly
			// the slow-subscriber case the server has to survive, but here we
			// want the healthy path.
			readCtx, stop := context.WithTimeout(ctx, *duration)
			defer stop()
			for {
				_, data, err := conn.Read(readCtx)
				if err != nil {
					return
				}
				frames.Add(1)
				bytesRecv.Add(int64(len(data)))
			}
		}(i)
	}

	// Report progress while the run is in flight.
	done := make(chan struct{})
	go func() {
		t := time.NewTicker(5 * time.Second)
		defer t.Stop()
		for {
			select {
			case <-done:
				return
			case <-t.C:
				var m runtime.MemStats
				runtime.ReadMemStats(&m)
				fmt.Printf("  t+%-5s connected=%-6d failed=%-5d frames=%-8d client heap=%dMB\n",
					time.Since(start).Round(time.Second),
					connected.Load(), failed.Load(), frames.Load(),
					m.Alloc/1024/1024)
			}
		}
	}()

	wg.Wait()
	close(done)

	elapsed := time.Since(start)
	sort.Slice(dialTimes, func(i, j int) bool { return dialTimes[i] < dialTimes[j] })

	fmt.Println()
	fmt.Println("results")
	fmt.Printf("  connections attempted : %d\n", *conns)
	fmt.Printf("  connected             : %d\n", connected.Load())
	fmt.Printf("  failed                : %d\n", failed.Load())
	if len(dialTimes) > 0 {
		fmt.Printf("  dial p50 / p99 / max  : %v / %v / %v\n",
			dialTimes[len(dialTimes)/2].Round(time.Millisecond),
			dialTimes[len(dialTimes)*99/100].Round(time.Millisecond),
			dialTimes[len(dialTimes)-1].Round(time.Millisecond))
	}
	fmt.Printf("  frames received       : %d\n", frames.Load())
	fmt.Printf("  bytes received        : %.1f MB\n", float64(bytesRecv.Load())/1e6)
	if connected.Load() > 0 {
		fmt.Printf("  frames per connection : %.1f\n",
			float64(frames.Load())/float64(connected.Load()))
		fmt.Printf("  server egress         : %.2f MB/s\n",
			float64(bytesRecv.Load())/1e6/elapsed.Seconds())
	}

	if failed.Load() > 0 {
		fmt.Fprintf(os.Stderr, "\n%d connections were refused: the ceiling is at or below %d\n",
			failed.Load(), *conns)
		os.Exit(1)
	}
	log.Printf("held %d concurrent connections for %v with no failures",
		connected.Load(), duration)
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}
