package server

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/coder/websocket"

	"github.com/uopi/uopi/internal/broker"
	"github.com/uopi/uopi/internal/datasource/stub"
)

// newStressServer creates an httptest.Server backed by a stub with n signals.
// The server and broker contexts are tied to t.Context() and cleaned up automatically.
func newStressServer(t *testing.T, nSignals int) (*httptest.Server, *broker.Broker) {
	t.Helper()
	ctx := t.Context()
	log := slog.New(slog.NewTextHandler(io.Discard, nil))

	var ds *stub.Stub
	if nSignals > 0 {
		ds = stub.NewN(nSignals)
	} else {
		ds = stub.New()
	}
	if err := ds.Connect(ctx); err != nil {
		t.Fatal(err)
	}

	brk := broker.New(ctx, log)
	brk.Register(ds)

	mux := http.NewServeMux()
	mux.Handle("/ws", &wsHandler{broker: brk, log: log})

	srv := httptest.NewServer(mux)
	t.Cleanup(srv.Close)
	return srv, brk
}

// wsURL converts an http:// test server URL to ws://.
func wsURL(srv *httptest.Server) string {
	return strings.ReplaceAll(srv.URL, "http://", "ws://") + "/ws"
}

// buildSubMsg encodes a subscribe message for n stub PVs named "pv_0"…"pv_{n-1}".
func buildSubMsg(n int) []byte {
	sigs := make([]sigRef, n)
	for i := range n {
		sigs[i] = sigRef{DS: "stub", Name: fmt.Sprintf("pv_%d", i)}
	}
	data, _ := json.Marshal(inMsg{Type: "subscribe", Signals: sigs})
	return data
}

// connectAndCount dials the WebSocket, sends subMsg, counts incoming messages until
// ctx is cancelled, adds the count to *total, and signals wg.Done.
func connectAndCount(ctx context.Context, t *testing.T, url string, subMsg []byte, total *atomic.Int64, wg *sync.WaitGroup) {
	t.Helper()
	defer wg.Done()

	conn, _, err := websocket.Dial(ctx, url, nil)
	if err != nil {
		t.Errorf("dial: %v", err)
		return
	}
	defer conn.CloseNow()

	if err := conn.Write(ctx, websocket.MessageText, subMsg); err != nil {
		t.Errorf("subscribe write: %v", err)
		return
	}

	var n int64
	for {
		_, _, err := conn.Read(ctx)
		if err != nil {
			break
		}
		n++
	}
	total.Add(n)
}

// TestStress_100PVs_10Clients verifies the full stack (broker + WS handler) can
// deliver updates from 100 signals to 10 concurrent clients without loss or deadlock.
func TestStress_100PVs_10Clients(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping stress test in short mode")
	}

	const (
		nSignals = 100
		nClients = 10
		duration = 3 * time.Second
	)

	srv, brk := newStressServer(t, nSignals)
	url := wsURL(srv)
	subMsg := buildSubMsg(nSignals)

	ctx, cancel := context.WithTimeout(t.Context(), duration+5*time.Second)
	defer cancel()

	var total atomic.Int64
	var wg sync.WaitGroup

	for range nClients {
		wg.Add(1)
		// Each client runs for exactly `duration`, then its context expires and
		// conn.Read returns an error, causing the goroutine to exit cleanly.
		clientCtx, clientCancel := context.WithTimeout(ctx, duration)
		go func() {
			defer clientCancel()
			connectAndCount(clientCtx, t, url, subMsg, &total, &wg)
		}()
	}

	wg.Wait()

	got := total.Load()
	// At 10 Hz, each PV fires ≥ duration*10 times; each client receives all of them.
	// min = nClients × nSignals × duration_s × 10 × 0.5 (conservative — meta msgs too)
	minExpected := int64(nClients) * int64(nSignals) * int64(duration.Seconds()) * 5
	t.Logf("100PVs/10Clients: received %d updates (min expected %d)", got, minExpected)
	if got < minExpected {
		t.Errorf("too few updates: got %d, want >= %d", got, minExpected)
	}

	// Broker must release all upstream subscriptions after clients disconnect.
	time.Sleep(300 * time.Millisecond)
	if n := brk.ActiveSubscriptions(); n != 0 {
		t.Errorf("subscription leak: %d remain after all clients disconnected", n)
	}
}

// TestStress_500PVs_20Clients is the full-scale scenario: hundreds of signals,
// tens of clients. Verifies no crash, no deadlock, and clean teardown.
func TestStress_500PVs_20Clients(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping stress test in short mode")
	}

	const (
		nSignals = 500
		nClients = 20
		duration = 3 * time.Second
	)

	srv, brk := newStressServer(t, nSignals)
	url := wsURL(srv)
	subMsg := buildSubMsg(nSignals)

	ctx, cancel := context.WithTimeout(t.Context(), duration+10*time.Second)
	defer cancel()

	var total atomic.Int64
	var wg sync.WaitGroup

	for range nClients {
		wg.Add(1)
		clientCtx, clientCancel := context.WithTimeout(ctx, duration)
		go func() {
			defer clientCancel()
			connectAndCount(clientCtx, t, url, subMsg, &total, &wg)
		}()
	}

	wg.Wait()

	got := total.Load()
	// Conservative: expect at least 20% of the theoretical maximum
	minExpected := int64(nClients) * int64(nSignals) * int64(duration.Seconds()) * 2
	t.Logf("500PVs/20Clients: received %d updates (min expected %d)", got, minExpected)
	if got < minExpected {
		t.Errorf("too few updates: got %d, want >= %d", got, minExpected)
	}

	time.Sleep(500 * time.Millisecond)
	if n := brk.ActiveSubscriptions(); n != 0 {
		t.Errorf("subscription leak: %d remain after all clients disconnected", n)
	}
}

// TestStress_ClientChurn repeatedly connects and disconnects clients while a
// stable set of signals is being delivered to ensure subscribe/unsubscribe under
// load doesn't cause races, panics, or subscription leaks.
func TestStress_ClientChurn(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping stress test in short mode")
	}

	const (
		nSignals       = 50
		nParallelConns = 30
		duration       = 3 * time.Second
	)

	srv, brk := newStressServer(t, nSignals)
	u := wsURL(srv)
	subMsg := buildSubMsg(nSignals)

	deadline := time.Now().Add(duration)
	var wg sync.WaitGroup
	var total atomic.Int64

	for range nParallelConns {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for time.Now().Before(deadline) {
				// Each iteration: one short-lived connection (~500 ms).
				func() {
					connCtx, connCancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
					defer connCancel()

					conn, _, err := websocket.Dial(connCtx, u, nil)
					if err != nil {
						return // dial failed (server busy) — try next iteration
					}
					defer conn.CloseNow()

					if err := conn.Write(connCtx, websocket.MessageText, subMsg); err != nil {
						return
					}
					var n int64
					for {
						_, _, err := conn.Read(connCtx)
						if err != nil {
							break
						}
						n++
					}
					total.Add(n)
				}()
			}
		}()
	}

	wg.Wait()

	t.Logf("ClientChurn: %d total messages during %s of churn with %d concurrent connections",
		total.Load(), duration, nParallelConns)

	time.Sleep(500 * time.Millisecond)
	if n := brk.ActiveSubscriptions(); n != 0 {
		t.Errorf("subscription leak after churn: %d remain", n)
	}
}

// TestStress_SlowConsumer verifies that a slow-reading client doesn't block
// fast clients or the broker fan-out (drop-on-full behaviour).
func TestStress_SlowConsumer(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping stress test in short mode")
	}

	const (
		nSignals = 100
		duration = 3 * time.Second
	)

	srv, brk := newStressServer(t, nSignals)
	url := wsURL(srv)
	subMsg := buildSubMsg(nSignals)

	ctx, cancel := context.WithTimeout(t.Context(), duration+5*time.Second)
	defer cancel()

	var fastTotal, slowTotal atomic.Int64
	var wg sync.WaitGroup

	// Fast client: reads as quickly as possible.
	wg.Add(1)
	fastCtx, fastCancel := context.WithTimeout(ctx, duration)
	go func() {
		defer fastCancel()
		connectAndCount(fastCtx, t, url, subMsg, &fastTotal, &wg)
	}()

	// Slow client: sleeps 100 ms between each read.
	wg.Add(1)
	slowCtx, slowCancel := context.WithTimeout(ctx, duration)
	go func() {
		defer wg.Done()
		defer slowCancel()

		conn, _, err := websocket.Dial(slowCtx, url, nil)
		if err != nil {
			t.Errorf("slow client dial: %v", err)
			return
		}
		defer conn.CloseNow()

		if err := conn.Write(slowCtx, websocket.MessageText, subMsg); err != nil {
			t.Errorf("slow client subscribe: %v", err)
			return
		}

		var n int64
		for {
			time.Sleep(100 * time.Millisecond)
			_, _, err := conn.Read(slowCtx)
			if err != nil {
				break
			}
			n++
		}
		slowTotal.Add(n)
	}()

	wg.Wait()

	fast := fastTotal.Load()
	slow := slowTotal.Load()
	t.Logf("SlowConsumer: fast=%d updates, slow=%d updates in %s", fast, slow, duration)

	// Fast client must receive substantially more than slow client.
	if fast < slow*3 {
		t.Errorf("fast client should receive many more updates than slow client (fast=%d slow=%d)", fast, slow)
	}

	// Fast client must have received a meaningful number of updates.
	minFast := int64(nSignals) * int64(duration.Seconds()) * 5
	if fast < minFast {
		t.Errorf("fast client received too few updates: got %d, want >= %d", fast, minFast)
	}

	time.Sleep(300 * time.Millisecond)
	if n := brk.ActiveSubscriptions(); n != 0 {
		t.Errorf("subscription leak: %d remain", n)
	}
}
