package tournament_test

import (
	"context"
	"errors"
	"fmt"
	"math/rand"
	"os"
	"sync"
	"testing"
	"time"

	"github.com/drjones/quantum-arcade/pkg/ledger"
	"github.com/drjones/quantum-arcade/pkg/tournament"
	"github.com/jackc/pgx/v5/pgxpool"
)

var runID = fmt.Sprintf("%d-%d", time.Now().UnixNano(), rand.Int63())

func testPool(t *testing.T) *pgxpool.Pool {
	t.Helper()
	dsn := os.Getenv("ARCADE_TEST_DSN")
	if dsn == "" {
		dsn = "postgres://arcade:arcade_dev@localhost:5432/arcade"
	}
	pool, err := pgxpool.New(context.Background(), dsn)
	if err != nil {
		t.Skipf("no database available: %v", err)
	}
	if err := pool.Ping(context.Background()); err != nil {
		t.Skipf("no database available: %v", err)
	}
	return pool
}

type fixture struct {
	t      *testing.T
	svc    *tournament.Service
	ledger *ledger.Ledger
	pool   *pgxpool.Pool
	ctx    context.Context
}

func newFixture(t *testing.T) *fixture {
	t.Helper()
	pool := testPool(t)
	l := ledger.New(pool)
	return &fixture{t: t, svc: tournament.New(pool, l), ledger: l,
		pool: pool, ctx: context.Background()}
}

func (f *fixture) player(label string, fundMsat int64) int64 {
	f.t.Helper()
	pk := []byte(fmt.Sprintf("%s-%s-%s", runID, f.t.Name(), label))
	id, err := f.ledger.EnsurePlayer(f.ctx, pk)
	if err != nil {
		f.t.Fatal(err)
	}
	if fundMsat > 0 {
		if _, err := f.ledger.Deposit(f.ctx, id, fundMsat); err != nil {
			f.t.Fatal(err)
		}
	}
	return id
}

// open creates a tournament already accepting entries and ending in the past,
// so tests can settle without waiting.
func (f *fixture) open(entryFee int64, split []int32, ended bool) *tournament.Tournament {
	f.t.Helper()
	now := time.Now()
	ends := now.Add(time.Hour)
	if ended {
		ends = now.Add(-time.Minute)
	}
	t, err := f.svc.Create(f.ctx, "Test Cup", "rocket", entryFee, split, nil,
		now.Add(-time.Hour), now.Add(-30*time.Minute), ends)
	if err != nil {
		f.t.Fatal(err)
	}
	if err := f.svc.AdvanceSchedules(f.ctx); err != nil {
		f.t.Fatal(err)
	}
	// Registration must be open for entries; AdvanceSchedules may have moved
	// it straight to running.
	if _, err := f.pool.Exec(f.ctx,
		`UPDATE tournaments SET status = 'registering' WHERE id = $1`, t.ID); err != nil {
		f.t.Fatal(err)
	}
	return t
}

/* ---------------- creation ---------------- */

func TestPayoutSplitMustSumToWhole(t *testing.T) {
	f := newFixture(t)
	now := time.Now()
	for _, split := range [][]int32{
		{5000, 3000}, // 80%
		{6000, 5000}, // 110%
		{10000, 1},   // over
		{},           // nothing
	} {
		_, err := f.svc.Create(f.ctx, "bad", "rocket", 1000, split, nil,
			now, now.Add(time.Minute), now.Add(time.Hour))
		if !errors.Is(err, tournament.ErrBadPayoutSplit) {
			t.Errorf("split %v gave %v, want ErrBadPayoutSplit", split, err)
		}
	}
}

func TestCreateOpensAPrizePool(t *testing.T) {
	f := newFixture(t)
	tn := f.open(0, []int32{10000}, false)
	if tn.PoolMsat != 0 {
		t.Fatalf("new pool holds %d, want 0", tn.PoolMsat)
	}
}

/* ---------------- entry ---------------- */

func TestEntryFeeMovesIntoThePool(t *testing.T) {
	f := newFixture(t)
	tn := f.open(10_000, []int32{10000}, false)
	id := f.player("a", 100_000)

	before, _ := f.ledger.Balance(f.ctx, id)
	if err := f.svc.Enter(f.ctx, tn.ID, id); err != nil {
		t.Fatal(err)
	}
	after, _ := f.ledger.Balance(f.ctx, id)

	if before-after != 10_000 {
		t.Fatalf("entry cost %d, want 10000", before-after)
	}
	got, _ := f.svc.Get(f.ctx, tn.ID)
	if got.PoolMsat != 10_000 {
		t.Fatalf("pool holds %d, want 10000", got.PoolMsat)
	}
}

func TestCannotEnterTwice(t *testing.T) {
	f := newFixture(t)
	tn := f.open(5_000, []int32{10000}, false)
	id := f.player("a", 100_000)

	if err := f.svc.Enter(f.ctx, tn.ID, id); err != nil {
		t.Fatal(err)
	}
	if err := f.svc.Enter(f.ctx, tn.ID, id); !errors.Is(err, tournament.ErrAlreadyEntered) {
		t.Fatalf("got %v, want ErrAlreadyEntered", err)
	}
	got, _ := f.svc.Get(f.ctx, tn.ID)
	if got.PoolMsat != 5_000 {
		t.Fatalf("pool holds %d after a duplicate attempt, want 5000", got.PoolMsat)
	}
}

// A player who cannot afford the fee must not hold a seat.
func TestUnfundedEntryTakesNoSeat(t *testing.T) {
	f := newFixture(t)
	tn := f.open(50_000, []int32{10000}, false)
	id := f.player("broke", 100)

	if err := f.svc.Enter(f.ctx, tn.ID, id); err == nil {
		t.Fatal("an unfunded player entered")
	}
	got, _ := f.svc.Get(f.ctx, tn.ID)
	if got.Entrants != 0 {
		t.Fatalf("%d entrants after a failed payment, want 0", got.Entrants)
	}

	// And they can enter properly once funded.
	if _, err := f.ledger.Deposit(f.ctx, id, 100_000); err != nil {
		t.Fatal(err)
	}
	if err := f.svc.Enter(f.ctx, tn.ID, id); err != nil {
		t.Fatalf("could not enter after funding: %v", err)
	}
}

func TestConcurrentEntriesChargeOnce(t *testing.T) {
	f := newFixture(t)
	tn := f.open(10_000, []int32{10000}, false)
	id := f.player("a", 1_000_000)

	var wg sync.WaitGroup
	results := make([]error, 8)
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			results[i] = f.svc.Enter(f.ctx, tn.ID, id)
		}(i)
	}
	wg.Wait()

	ok := 0
	for _, err := range results {
		if err == nil {
			ok++
		}
	}
	if ok != 1 {
		t.Fatalf("%d concurrent entries succeeded, want 1", ok)
	}
	got, _ := f.svc.Get(f.ctx, tn.ID)
	if got.PoolMsat != 10_000 {
		t.Fatalf("pool holds %d, want a single fee of 10000", got.PoolMsat)
	}
}

func TestEntrantLimitIsEnforced(t *testing.T) {
	f := newFixture(t)
	now := time.Now()
	max := int32(2)
	tn, err := f.svc.Create(f.ctx, "small", "rocket", 1_000, []int32{10000}, &max,
		now.Add(-time.Hour), now.Add(time.Hour), now.Add(2*time.Hour))
	if err != nil {
		t.Fatal(err)
	}
	if _, err := f.pool.Exec(f.ctx,
		`UPDATE tournaments SET status = 'registering' WHERE id = $1`, tn.ID); err != nil {
		t.Fatal(err)
	}

	for i := 0; i < 2; i++ {
		if err := f.svc.Enter(f.ctx, tn.ID, f.player(fmt.Sprintf("p%d", i), 100_000)); err != nil {
			t.Fatal(err)
		}
	}
	if err := f.svc.Enter(f.ctx, tn.ID, f.player("late", 100_000)); !errors.Is(err, tournament.ErrFull) {
		t.Fatalf("got %v, want ErrFull", err)
	}
}

/* ---------------- scoring ---------------- */

func TestLeaderboardOrdersByScore(t *testing.T) {
	f := newFixture(t)
	tn := f.open(0, []int32{10000}, false)

	scores := map[string]int64{"low": -5_000, "mid": 2_000, "high": 50_000}
	ids := map[string]int64{}
	for name, score := range scores {
		id := f.player(name, 100_000)
		ids[name] = id
		if err := f.svc.Enter(f.ctx, tn.ID, id); err != nil {
			t.Fatal(err)
		}
		if err := f.svc.RecordResult(f.ctx, tn.ID, id, score); err != nil {
			t.Fatal(err)
		}
	}

	board, err := f.svc.Leaderboard(f.ctx, tn.ID, 10)
	if err != nil {
		t.Fatal(err)
	}
	if len(board) != 3 {
		t.Fatalf("board has %d entries, want 3", len(board))
	}
	if board[0].AccountID != ids["high"] {
		t.Fatalf("leader is %d, want %d", board[0].AccountID, ids["high"])
	}
	if board[2].AccountID != ids["low"] {
		t.Fatalf("last is %d, want %d", board[2].AccountID, ids["low"])
	}
	if board[0].Position != 1 {
		t.Fatalf("leader position = %d, want 1", board[0].Position)
	}
}

// Scores accumulate across rounds, and losses count against you.
func TestScoresAccumulateIncludingLosses(t *testing.T) {
	f := newFixture(t)
	tn := f.open(0, []int32{10000}, false)
	id := f.player("a", 100_000)
	if err := f.svc.Enter(f.ctx, tn.ID, id); err != nil {
		t.Fatal(err)
	}

	for _, n := range []int64{10_000, -3_000, 5_000, -1_000} {
		if err := f.svc.RecordResult(f.ctx, tn.ID, id, n); err != nil {
			t.Fatal(err)
		}
	}
	board, _ := f.svc.Leaderboard(f.ctx, tn.ID, 1)
	if board[0].ScoreMsat != 11_000 {
		t.Fatalf("score = %d, want 11000", board[0].ScoreMsat)
	}
	if board[0].RoundsPlayed != 4 {
		t.Fatalf("rounds = %d, want 4", board[0].RoundsPlayed)
	}
}

/* ---------------- settlement ---------------- */

// The whole pool must be paid out. Integer division of a pool across shares
// leaves a remainder, and a dropped remainder is money destroyed.
func TestSettlementDistributesTheEntirePool(t *testing.T) {
	f := newFixture(t)
	// 3333/3333/3334 across a pool that does not divide evenly.
	tn := f.open(3_333, []int32{5000, 3000, 2000}, true)

	var ids []int64
	for i := 0; i < 3; i++ {
		id := f.player(fmt.Sprintf("p%d", i), 100_000)
		ids = append(ids, id)
		if err := f.svc.Enter(f.ctx, tn.ID, id); err != nil {
			t.Fatal(err)
		}
		if err := f.svc.RecordResult(f.ctx, tn.ID, id, int64((3-i)*1000)); err != nil {
			t.Fatal(err)
		}
	}

	poolBefore, _ := f.svc.Get(f.ctx, tn.ID)
	before := make([]int64, len(ids))
	for i, id := range ids {
		before[i], _ = f.ledger.Balance(f.ctx, id)
	}

	board, err := f.svc.Settle(f.ctx, tn.ID)
	if err != nil {
		t.Fatal(err)
	}

	var paid int64
	for i, id := range ids {
		after, _ := f.ledger.Balance(f.ctx, id)
		paid += after - before[i]
	}
	if paid != poolBefore.PoolMsat {
		t.Fatalf("paid out %d of a %d pool — %d msat vanished",
			paid, poolBefore.PoolMsat, poolBefore.PoolMsat-paid)
	}

	after, _ := f.svc.Get(f.ctx, tn.ID)
	if after.PoolMsat != 0 {
		t.Fatalf("%d msat stranded in the pool after settlement", after.PoolMsat)
	}
	if board[0].PrizeMsat <= board[1].PrizeMsat {
		t.Fatalf("first place won %d, second %d", board[0].PrizeMsat, board[1].PrizeMsat)
	}
}

func TestCannotSettleBeforeItEnds(t *testing.T) {
	f := newFixture(t)
	tn := f.open(1_000, []int32{10000}, false) // ends in an hour
	if _, err := f.svc.Settle(f.ctx, tn.ID); !errors.Is(err, tournament.ErrNotFinished) {
		t.Fatalf("got %v, want ErrNotFinished", err)
	}
}

func TestSettlingTwiceIsRefused(t *testing.T) {
	f := newFixture(t)
	tn := f.open(1_000, []int32{10000}, true)
	id := f.player("a", 100_000)
	if err := f.svc.Enter(f.ctx, tn.ID, id); err != nil {
		t.Fatal(err)
	}

	if _, err := f.svc.Settle(f.ctx, tn.ID); err != nil {
		t.Fatal(err)
	}
	afterFirst, _ := f.ledger.Balance(f.ctx, id)

	if _, err := f.svc.Settle(f.ctx, tn.ID); !errors.Is(err, tournament.ErrAlreadySettled) {
		t.Fatalf("got %v, want ErrAlreadySettled", err)
	}
	afterSecond, _ := f.ledger.Balance(f.ctx, id)
	if afterSecond != afterFirst {
		t.Fatalf("a second settlement paid again: %d -> %d", afterFirst, afterSecond)
	}
}

// Two instances settling at once must pay out exactly once.
func TestConcurrentSettlementPaysOnce(t *testing.T) {
	f := newFixture(t)
	tn := f.open(2_000, []int32{10000}, true)
	id := f.player("a", 100_000)
	if err := f.svc.Enter(f.ctx, tn.ID, id); err != nil {
		t.Fatal(err)
	}
	before, _ := f.ledger.Balance(f.ctx, id)

	var wg sync.WaitGroup
	ok := make([]bool, 5)
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			_, err := f.svc.Settle(f.ctx, tn.ID)
			ok[i] = err == nil
		}(i)
	}
	wg.Wait()

	wins := 0
	for _, v := range ok {
		if v {
			wins++
		}
	}
	if wins != 1 {
		t.Fatalf("%d concurrent settlements succeeded, want 1", wins)
	}
	after, _ := f.ledger.Balance(f.ctx, id)
	if after-before != 2_000 {
		t.Fatalf("player received %d, want the single 2000 pool", after-before)
	}
}

func TestBooksBalanceAfterSettlement(t *testing.T) {
	f := newFixture(t)
	tn := f.open(7_777, []int32{6000, 4000}, true)
	for i := 0; i < 4; i++ {
		id := f.player(fmt.Sprintf("p%d", i), 100_000)
		if err := f.svc.Enter(f.ctx, tn.ID, id); err != nil {
			t.Fatal(err)
		}
		if err := f.svc.RecordResult(f.ctx, tn.ID, id, int64(i)*100); err != nil {
			t.Fatal(err)
		}
	}
	if _, err := f.svc.Settle(f.ctx, tn.ID); err != nil {
		t.Fatal(err)
	}
	total, err := f.ledger.ConservationCheck(f.ctx)
	if err != nil {
		t.Fatal(err)
	}
	if total != 0 {
		t.Fatalf("books do not balance after tournament settlement: %d", total)
	}
}

/* ---------------- cancellation ---------------- */

func TestCancelRefundsEveryEntrant(t *testing.T) {
	f := newFixture(t)
	tn := f.open(12_000, []int32{10000}, false)

	var ids []int64
	var before []int64
	for i := 0; i < 4; i++ {
		id := f.player(fmt.Sprintf("p%d", i), 100_000)
		b, _ := f.ledger.Balance(f.ctx, id)
		before = append(before, b)
		ids = append(ids, id)
		if err := f.svc.Enter(f.ctx, tn.ID, id); err != nil {
			t.Fatal(err)
		}
	}

	if err := f.svc.Cancel(f.ctx, tn.ID); err != nil {
		t.Fatal(err)
	}
	for i, id := range ids {
		after, _ := f.ledger.Balance(f.ctx, id)
		if after != before[i] {
			t.Fatalf("entrant %d has %d after cancellation, want their original %d",
				id, after, before[i])
		}
	}
	got, _ := f.svc.Get(f.ctx, tn.ID)
	if got.PoolMsat != 0 {
		t.Fatalf("%d msat stranded after cancellation", got.PoolMsat)
	}
}

func TestCancelIsIdempotent(t *testing.T) {
	f := newFixture(t)
	tn := f.open(1_000, []int32{10000}, false)
	id := f.player("a", 100_000)
	if err := f.svc.Enter(f.ctx, tn.ID, id); err != nil {
		t.Fatal(err)
	}

	if err := f.svc.Cancel(f.ctx, tn.ID); err != nil {
		t.Fatal(err)
	}
	afterFirst, _ := f.ledger.Balance(f.ctx, id)

	if err := f.svc.Cancel(f.ctx, tn.ID); !errors.Is(err, tournament.ErrAlreadySettled) {
		t.Fatalf("got %v, want ErrAlreadySettled", err)
	}
	afterSecond, _ := f.ledger.Balance(f.ctx, id)
	if afterSecond != afterFirst {
		t.Fatalf("a second cancellation refunded again: %d -> %d", afterFirst, afterSecond)
	}
}

/* ---------------- lifecycle ---------------- */

func TestSchedulesAdvanceByClock(t *testing.T) {
	f := newFixture(t)
	now := time.Now()
	tn, err := f.svc.Create(f.ctx, "later", "rocket", 0, []int32{10000}, nil,
		now.Add(-time.Minute), now.Add(-30*time.Second), now.Add(time.Hour))
	if err != nil {
		t.Fatal(err)
	}
	if err := f.svc.AdvanceSchedules(f.ctx); err != nil {
		t.Fatal(err)
	}
	got, _ := f.svc.Get(f.ctx, tn.ID)
	if got.Status != tournament.StatusRunning {
		t.Fatalf("status = %s, want running once the start time has passed", got.Status)
	}
}

func TestCannotEnterOnceRunning(t *testing.T) {
	f := newFixture(t)
	now := time.Now()
	tn, err := f.svc.Create(f.ctx, "started", "rocket", 1_000, []int32{10000}, nil,
		now.Add(-time.Hour), now.Add(-time.Minute), now.Add(time.Hour))
	if err != nil {
		t.Fatal(err)
	}
	if err := f.svc.AdvanceSchedules(f.ctx); err != nil {
		t.Fatal(err)
	}
	id := f.player("late", 100_000)
	if err := f.svc.Enter(f.ctx, tn.ID, id); !errors.Is(err, tournament.ErrNotRegistering) {
		t.Fatalf("got %v, want ErrNotRegistering", err)
	}
}
