package broker_test

import (
	"context"
	"fmt"
	"log/slog"
	"runtime"
	"sync"
	"sync/atomic"
	"testing"
	"time"

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

// newBrokerN builds a broker backed by a stub with n dynamically generated signals.
func newBrokerN(tb testing.TB, n int) (*broker.Broker, context.CancelFunc) {
	tb.Helper()
	ctx, cancel := context.WithCancel(context.Background())
	log := slog.Default()
	b := broker.New(ctx, log)
	ds := stub.NewN(n)
	if err := ds.Connect(ctx); err != nil {
		cancel()
		tb.Fatal(err)
	}
	b.Register(ds)
	return b, cancel
}

// TestStress_ManySignalsManyClients subscribes 20 clients to 500 signals each,
// runs for 2 seconds, then verifies delivery counts and zero subscription leaks.
func TestStress_ManySignalsManyClients(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping stress test in short mode")
	}

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

	goroutinesBefore := runtime.NumGoroutine()

	brk, cancel := newBrokerN(t, nSignals)
	defer cancel()

	// Each client has its own channel and subscribes to all nSignals signals.
	type clientState struct {
		ch    chan broker.Update
		unsub []func()
	}
	clients := make([]clientState, nClients)

	for i := range nClients {
		ch := make(chan broker.Update, 2048)
		unsubs := make([]func(), nSignals)
		for j := range nSignals {
			ref := broker.SignalRef{DS: "stub", Name: fmt.Sprintf("pv_%d", j)}
			unsub, err := brk.Subscribe(ref, ch)
			if err != nil {
				t.Fatalf("client %d subscribe pv_%d: %v", i, j, err)
			}
			unsubs[j] = unsub
		}
		clients[i] = clientState{ch: ch, unsub: unsubs}
	}

	if n := brk.ActiveSubscriptions(); n != nSignals {
		t.Errorf("expected %d active upstream subscriptions, got %d", nSignals, n)
	}

	// Drain updates concurrently for the test duration.
	var totalReceived atomic.Int64
	stop := make(chan struct{})
	var wg sync.WaitGroup

	for i := range nClients {
		wg.Add(1)
		ch := clients[i].ch
		go func() {
			defer wg.Done()
			var n int64
			for {
				select {
				case <-ch:
					n++
				case <-stop:
					totalReceived.Add(n)
					return
				}
			}
		}()
	}

	time.Sleep(duration)
	close(stop)
	wg.Wait()

	total := totalReceived.Load()
	// At 10 Hz for duration seconds: nSignals * nClients * duration.Seconds() * 10
	minExpected := int64(nSignals) * int64(nClients) * int64(duration.Seconds()) * 5 // conservative: 50%
	t.Logf("received %d updates (%d clients × %d signals × %.0fs @ 10Hz, min=%d)",
		total, nClients, nSignals, duration.Seconds(), minExpected)
	if total < minExpected {
		t.Errorf("too few updates: got %d, want >= %d", total, minExpected)
	}

	// Unsubscribe all clients.
	for _, c := range clients {
		for _, unsub := range c.unsub {
			unsub()
		}
	}

	// Give the broker time to tear down upstream subscriptions.
	time.Sleep(200 * time.Millisecond)

	if n := brk.ActiveSubscriptions(); n != 0 {
		t.Errorf("subscription leak: %d active subscriptions remain after all clients unsubscribed", n)
	}

	cancel()
	time.Sleep(300 * time.Millisecond)

	goroutinesAfter := runtime.NumGoroutine()
	leaked := goroutinesAfter - goroutinesBefore
	t.Logf("goroutines: before=%d after=%d delta=%d", goroutinesBefore, goroutinesAfter, leaked)
	if leaked > 20 {
		t.Errorf("goroutine leak: started with %d, ended with %d (%d extra)", goroutinesBefore, goroutinesAfter, leaked)
	}
}

// TestStress_RapidSubscribeUnsubscribe hammers subscribe/unsubscribe on a small
// signal set from many goroutines concurrently to expose races and deadlocks.
func TestStress_RapidSubscribeUnsubscribe(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping stress test in short mode")
	}

	const (
		nSignals    = 20
		nGoroutines = 50
		duration    = 2 * time.Second
	)

	brk, cancel := newBrokerN(t, nSignals)
	defer cancel()

	var wg sync.WaitGroup
	stop := make(chan struct{})
	var ops atomic.Int64

	for range nGoroutines {
		wg.Add(1)
		go func() {
			defer wg.Done()
			ch := make(chan broker.Update, 64)
			for {
				select {
				case <-stop:
					return
				default:
				}
				// Pick a signal and rapidly subscribe/unsubscribe.
				idx := int(ops.Load()) % nSignals
				ref := broker.SignalRef{DS: "stub", Name: fmt.Sprintf("pv_%d", idx)}
				unsub, err := brk.Subscribe(ref, ch)
				if err != nil {
					t.Errorf("subscribe pv_%d: %v", idx, err)
					return
				}
				ops.Add(1)
				// Drain a bit before unsubscribing.
				select {
				case <-ch:
				case <-time.After(50 * time.Millisecond):
				}
				unsub()
			}
		}()
	}

	time.Sleep(duration)
	close(stop)
	wg.Wait()

	t.Logf("completed %d subscribe/unsubscribe cycles in %s", ops.Load(), duration)

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

// TestStress_SharedSignalManyClients verifies that a single high-rate signal
// fans out correctly to many concurrent clients with no drops for fast consumers.
func TestStress_SharedSignalManyClients(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping stress test in short mode")
	}

	const (
		nClients = 50
		duration = 2 * time.Second
	)

	brk, cancel := newBroker(t) // uses stub.New() which has counter_fast at 1 ms
	defer cancel()

	ref := broker.SignalRef{DS: "stub", Name: "counter_fast"}
	channels := make([]chan broker.Update, nClients)
	unsubs := make([]func(), nClients)

	for i := range nClients {
		ch := make(chan broker.Update, 4096)
		channels[i] = ch
		unsub, err := brk.Subscribe(ref, ch)
		if err != nil {
			t.Fatalf("subscribe client %d: %v", i, err)
		}
		unsubs[i] = unsub
	}
	defer func() {
		for _, u := range unsubs {
			u()
		}
	}()

	if n := brk.ActiveSubscriptions(); n != 1 {
		t.Errorf("expected exactly 1 upstream subscription for shared signal, got %d", n)
	}

	var totalReceived atomic.Int64
	stop := make(chan struct{})
	var wg sync.WaitGroup

	for i := range nClients {
		wg.Add(1)
		ch := channels[i]
		go func() {
			defer wg.Done()
			var n int64
			for {
				select {
				case <-ch:
					n++
				case <-stop:
					totalReceived.Add(n)
					return
				}
			}
		}()
	}

	time.Sleep(duration)
	close(stop)
	wg.Wait()

	total := totalReceived.Load()
	// counter_fast ticks at 1ms → ~1000 Hz → 2000 updates × 50 clients = 100000 expected minimum
	minExpected := int64(nClients) * int64(duration.Seconds()) * 500 // 50% delivery ok due to buffering
	t.Logf("shared signal: received %d updates across %d clients (min=%d)", total, nClients, minExpected)
	if total < minExpected {
		t.Errorf("too few updates: got %d, want >= %d", total, minExpected)
	}
}
