package ledger_test

import (
	"context"
	"errors"
	"math/rand"
	"testing"

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

// Across a long run of random transfers, bets, and payouts, total value must
// never change and no player balance may go negative. This is the property that
// makes end-of-night settlement trustworthy.
func TestRandomActivityConservesValue(t *testing.T) {
	l := ledger.New(testPool(t))
	ctx := context.Background()

	// The test uses its own counterparty rather than the shared house account,
	// and sums only its own accounts. Asserting on a global total would fail
	// whenever another package's tests run against the same database in
	// parallel — a broken test, not a broken ledger.
	house, err := l.EnsurePlayer(ctx, uniqueKey(t, "counterparty"))
	if err != nil {
		t.Fatal(err)
	}
	if _, err := l.Deposit(ctx, house, 5_000_000); err != nil {
		t.Fatal(err)
	}

	const players = 8
	ids := make([]int64, players)
	for i := range ids {
		id, err := l.EnsurePlayer(ctx, uniqueKey(t, string(rune('a'+i))))
		if err != nil {
			t.Fatal(err)
		}
		ids[i] = id
		if _, err := l.Deposit(ctx, id, 100_000); err != nil {
			t.Fatal(err)
		}
	}

	// sumOwn totals only the accounts this test created.
	sumOwn := func() int64 {
		t.Helper()
		var total int64
		for _, id := range append(append([]int64{}, ids...), house) {
			bal, err := l.Balance(ctx, id)
			if err != nil {
				t.Fatal(err)
			}
			total += bal
		}
		return total
	}

	before := sumOwn()

	rng := rand.New(rand.NewSource(1))
	roundID := int64(0)
	for i := 0; i < 400; i++ {
		amt := int64(rng.Intn(5000) + 1)
		player := ids[rng.Intn(players)]

		var err error
		switch rng.Intn(3) {
		case 0: // peer transfer
			other := ids[rng.Intn(players)]
			if other == player {
				continue
			}
			_, err = l.Transfer(ctx, player, other, amt)
		case 1: // bet: player pays the house
			roundID++
			r := roundID
			_, err = l.Post(ctx, "bet", &r, []ledger.Posting{
				{AccountID: player, AmountMsat: -amt},
				{AccountID: house, AmountMsat: amt},
			})
		case 2: // payout: house pays the player
			roundID++
			r := roundID
			_, err = l.Post(ctx, "payout", &r, []ledger.Posting{
				{AccountID: house, AmountMsat: -amt},
				{AccountID: player, AmountMsat: amt},
			})
		}

		// Running out of funds is a legitimate outcome; nothing else is.
		if err != nil && !errors.Is(err, ledger.ErrInsufficientFunds) {
			t.Fatalf("iteration %d: %v", i, err)
		}
	}

	if after := sumOwn(); before != after {
		t.Fatalf("value not conserved: %d -> %d", before, after)
	}

	for _, id := range ids {
		bal, err := l.Balance(ctx, id)
		if err != nil {
			t.Fatal(err)
		}
		if bal < 0 {
			t.Fatalf("account %d went negative: %d", id, bal)
		}
	}
}

// Every transaction sums to zero, so the sum across all accounts including the
// external bridge must be exactly zero at all times.
func TestBooksAlwaysBalanceToZero(t *testing.T) {
	l := ledger.New(testPool(t))
	ctx := context.Background()

	p, err := l.EnsurePlayer(ctx, uniqueKey(t, "p"))
	if err != nil {
		t.Fatal(err)
	}
	if _, err := l.Deposit(ctx, p, 7_777); err != nil {
		t.Fatal(err)
	}
	total, err := l.ConservationCheck(ctx)
	if err != nil {
		t.Fatal(err)
	}
	if total != 0 {
		t.Fatalf("books do not balance: total across all accounts = %d", total)
	}
}
