package ledger_test

import (
	"context"
	"fmt"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/drjones/quantum-arcade/pkg/ledger"
)

// These measure throughput on the paths that decide whether the platform
// survives a crowd. They print numbers rather than asserting thresholds,
// because the numbers depend on the host — but the shape of the result is what
// matters, and a regression shows up immediately.

// Every bet debits the player and credits the house. All of them contend on
// the same house account row, which is the first thing to check: if that lock
// serialises the workload, no amount of hardware helps.
func TestThroughputContendedHouseAccount(t *testing.T) {
	if testing.Short() {
		t.Skip("load test")
	}
	l := ledger.New(testPool(t))
	ctx := context.Background()

	house, err := l.EnsurePlayer(ctx, uniqueKey(t, "house"))
	if err != nil {
		t.Fatal(err)
	}

	const players = 64
	const perPlayer = 20

	ids := make([]int64, players)
	for i := range ids {
		id, err := l.EnsurePlayer(ctx, uniqueKey(t, fmt.Sprintf("p%d", i)))
		if err != nil {
			t.Fatal(err)
		}
		if _, err := l.Deposit(ctx, id, 10_000_000); err != nil {
			t.Fatal(err)
		}
		ids[i] = id
	}

	var ok, failed atomic.Int64
	start := time.Now()

	var wg sync.WaitGroup
	for _, id := range ids {
		wg.Add(1)
		go func(id int64) {
			defer wg.Done()
			for i := 0; i < perPlayer; i++ {
				_, err := l.Post(ctx, "bet", nil, []ledger.Posting{
					{AccountID: id, AmountMsat: -1000},
					{AccountID: house, AmountMsat: 1000},
				})
				if err != nil {
					failed.Add(1)
				} else {
					ok.Add(1)
				}
			}
		}(id)
	}
	wg.Wait()

	elapsed := time.Since(start)
	rate := float64(ok.Load()) / elapsed.Seconds()
	t.Logf("contended (shared house row): %d bets in %v = %.0f bets/sec (%d failed)",
		ok.Load(), elapsed.Round(time.Millisecond), rate, failed.Load())
	t.Logf("  -> a 20s betting window absorbs about %.0f bets", rate*20)
}

// The same workload with the house side spread over several accounts, to
// isolate how much of the cost is lock contention rather than raw database
// throughput.
func TestThroughputShardedHouseAccount(t *testing.T) {
	if testing.Short() {
		t.Skip("load test")
	}
	l := ledger.New(testPool(t))
	ctx := context.Background()

	const shards = 16
	shardIDs := make([]int64, shards)
	for i := range shardIDs {
		id, err := l.EnsurePlayer(ctx, uniqueKey(t, fmt.Sprintf("houseshard%d", i)))
		if err != nil {
			t.Fatal(err)
		}
		shardIDs[i] = id
	}

	const players = 64
	const perPlayer = 20

	ids := make([]int64, players)
	for i := range ids {
		id, err := l.EnsurePlayer(ctx, uniqueKey(t, fmt.Sprintf("sp%d", i)))
		if err != nil {
			t.Fatal(err)
		}
		if _, err := l.Deposit(ctx, id, 10_000_000); err != nil {
			t.Fatal(err)
		}
		ids[i] = id
	}

	var ok, failed atomic.Int64
	start := time.Now()

	var wg sync.WaitGroup
	for n, id := range ids {
		wg.Add(1)
		go func(n int, id int64) {
			defer wg.Done()
			for i := 0; i < perPlayer; i++ {
				// Each player uses a fixed shard, the way a real sharded
				// house account would be selected.
				shard := shardIDs[n%shards]
				_, err := l.Post(ctx, "bet", nil, []ledger.Posting{
					{AccountID: id, AmountMsat: -1000},
					{AccountID: shard, AmountMsat: 1000},
				})
				if err != nil {
					failed.Add(1)
				} else {
					ok.Add(1)
				}
			}
		}(n, id)
	}
	wg.Wait()

	elapsed := time.Since(start)
	rate := float64(ok.Load()) / elapsed.Seconds()
	t.Logf("sharded (%d house rows): %d bets in %v = %.0f bets/sec (%d failed)",
		shards, ok.Load(), elapsed.Round(time.Millisecond), rate, failed.Load())
	t.Logf("  -> a 20s betting window absorbs about %.0f bets", rate*20)
}

// Settlement writes every payout for a round. At scale this is one large
// transaction, so its cost per posting is what decides how long a crowd waits
// between rounds.
func TestThroughputBatchSettlement(t *testing.T) {
	if testing.Short() {
		t.Skip("load test")
	}
	l := ledger.New(testPool(t))
	ctx := context.Background()

	house, _ := l.EnsurePlayer(ctx, uniqueKey(t, "settlehouse"))
	if _, err := l.Deposit(ctx, house, 1_000_000_000); err != nil {
		t.Fatal(err)
	}

	for _, size := range []int{10, 100, 500, 1000} {
		winners := make([]int64, size)
		for i := range winners {
			id, err := l.EnsurePlayer(ctx, uniqueKey(t, fmt.Sprintf("w%d-%d", size, i)))
			if err != nil {
				t.Fatal(err)
			}
			winners[i] = id
		}

		postings := make([]ledger.Posting, 0, size+1)
		for _, w := range winners {
			postings = append(postings, ledger.Posting{AccountID: w, AmountMsat: 1000})
		}
		postings = append(postings,
			ledger.Posting{AccountID: house, AmountMsat: -int64(size) * 1000})

		start := time.Now()
		if _, err := l.Post(ctx, "payout", nil, postings); err != nil {
			t.Fatalf("settling %d winners: %v", size, err)
		}
		elapsed := time.Since(start)
		t.Logf("settle %4d winners in one transaction: %8v (%.2fms per winner)",
			size, elapsed.Round(time.Millisecond),
			float64(elapsed.Microseconds())/1000/float64(size))
	}
}

// Balance reads are the most frequent query in the system: every client polls
// after every round.
func TestThroughputBalanceReads(t *testing.T) {
	if testing.Short() {
		t.Skip("load test")
	}
	l := ledger.New(testPool(t))
	ctx := context.Background()

	id, _ := l.EnsurePlayer(ctx, uniqueKey(t, "reader"))
	if _, err := l.Deposit(ctx, id, 1_000_000); err != nil {
		t.Fatal(err)
	}

	const readers = 32
	const each = 100
	var ok atomic.Int64
	start := time.Now()

	var wg sync.WaitGroup
	for i := 0; i < readers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < each; j++ {
				if _, err := l.Balance(ctx, id); err == nil {
					ok.Add(1)
				}
			}
		}()
	}
	wg.Wait()

	elapsed := time.Since(start)
	t.Logf("balance reads: %d in %v = %.0f reads/sec",
		ok.Load(), elapsed.Round(time.Millisecond),
		float64(ok.Load())/elapsed.Seconds())
}
