package lobby_test

import (
	"context"
	"database/sql"
	"fmt"
	"sync"
	"testing"
	"time"

	"galaxy/backend/internal/config"
	"galaxy/backend/internal/lobby"

	"github.com/google/uuid"
)

// capturingPublisher records every `LobbyNotification` intent that the
// lobby service emits, so a test can assert the producer side without
// running the real notification.Submit pipeline.
type capturingPublisher struct {
	mu    sync.Mutex
	items []lobby.LobbyNotification
}

func (p *capturingPublisher) PublishLobbyEvent(_ context.Context, ev lobby.LobbyNotification) error {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.items = append(p.items, ev)
	return nil
}

func (p *capturingPublisher) byKind(kind string) []lobby.LobbyNotification {
	p.mu.Lock()
	defer p.mu.Unlock()
	out := make([]lobby.LobbyNotification, 0, len(p.items))
	for _, ev := range p.items {
		if ev.Kind == kind {
			out = append(out, ev)
		}
	}
	return out
}

// newServiceWithPublisher mirrors `newServiceForTest` but lets the
// caller inject a custom NotificationPublisher; the runtime-hooks
// emit path needs to observe intents directly.
func newServiceWithPublisher(t *testing.T, db *sql.DB, now func() time.Time, max int32, publisher lobby.NotificationPublisher) *lobby.Service {
	t.Helper()
	store := lobby.NewStore(db)
	cache := lobby.NewCache()
	if err := cache.Warm(context.Background(), store); err != nil {
		t.Fatalf("warm cache: %v", err)
	}
	svc, err := lobby.NewService(lobby.Deps{
		Store:        store,
		Cache:        cache,
		Notification: publisher,
		Entitlement:  stubEntitlement{max: max},
		Config: config.LobbyConfig{
			SweeperInterval:        time.Second,
			PendingRegistrationTTL: time.Hour,
			InviteDefaultTTL:       time.Hour,
		},
		Now: now,
	})
	if err != nil {
		t.Fatalf("new service: %v", err)
	}
	return svc
}

// TestOnRuntimeSnapshotEmitsTurnReady verifies that an engine snapshot
// advancing `current_turn` fans out a `game.turn.ready` intent to every
// active member, that the idempotency key is anchored on (game_id, turn),
// and that a snapshot with the same turn does not re-emit.
func TestOnRuntimeSnapshotEmitsTurnReady(t *testing.T) {
	db := startPostgres(t)
	now := time.Now().UTC()
	clock := func() time.Time { return now }
	publisher := &capturingPublisher{}
	svc := newServiceWithPublisher(t, db, clock, 5, publisher)

	owner := uuid.New()
	seedAccount(t, db, owner)

	game, err := svc.CreateGame(context.Background(), lobby.CreateGameInput{
		OwnerUserID:         &owner,
		Visibility:          lobby.VisibilityPrivate,
		GameName:            "Turn-Ready Fan-Out",
		MinPlayers:          1,
		MaxPlayers:          4,
		StartGapHours:       1,
		StartGapPlayers:     1,
		EnrollmentEndsAt:    now.Add(time.Hour),
		TurnSchedule:        "0 0 * * *",
		TargetEngineVersion: "1.0.0",
	})
	if err != nil {
		t.Fatalf("create game: %v", err)
	}
	if _, err := svc.OpenEnrollment(context.Background(), &owner, false, game.GameID); err != nil {
		t.Fatalf("open enrollment: %v", err)
	}

	// Seed two active members through the store so the test focuses on
	// the runtime hook, not the membership state machine.
	store := lobby.NewStore(db)
	canonicalPolicy, err := lobby.NewPolicy()
	if err != nil {
		t.Fatalf("new policy: %v", err)
	}
	memberA := uuid.New()
	memberB := uuid.New()
	seedAccount(t, db, memberA)
	seedAccount(t, db, memberB)
	for i, m := range []uuid.UUID{memberA, memberB} {
		race := fmt.Sprintf("Race%d", i+1)
		canonical, err := canonicalPolicy.Canonical(race)
		if err != nil {
			t.Fatalf("canonical %q: %v", race, err)
		}
		if _, err := db.ExecContext(context.Background(), `
			INSERT INTO backend.memberships (
				membership_id, game_id, user_id, race_name, canonical_key, status
			) VALUES ($1, $2, $3, $4, $5, 'active')
		`, uuid.New(), game.GameID, m, race, string(canonical)); err != nil {
			t.Fatalf("seed membership %s: %v", m, err)
		}
	}
	if err := svc.Cache().Warm(context.Background(), store); err != nil {
		t.Fatalf("re-warm cache: %v", err)
	}
	if _, err := svc.ReadyToStart(context.Background(), &owner, false, game.GameID); err != nil {
		t.Fatalf("ready-to-start: %v", err)
	}
	if _, err := svc.Start(context.Background(), &owner, false, game.GameID); err != nil {
		t.Fatalf("start: %v", err)
	}

	// First snapshot: prev=0, current_turn=1 → emit on the very first
	// turn after the engine starts producing.
	if err := svc.OnRuntimeSnapshot(context.Background(), game.GameID, lobby.RuntimeSnapshot{
		CurrentTurn:   1,
		RuntimeStatus: "running",
	}); err != nil {
		t.Fatalf("on-runtime-snapshot 1: %v", err)
	}
	intents := publisher.byKind(lobby.NotificationGameTurnReady)
	if len(intents) != 1 {
		t.Fatalf("after turn 1 want 1 turn-ready intent, got %d", len(intents))
	}
	first := intents[0]
	wantKey := fmt.Sprintf("turn-ready:%s:1", game.GameID)
	if first.IdempotencyKey != wantKey {
		t.Errorf("turn 1 idempotency key = %q, want %q", first.IdempotencyKey, wantKey)
	}
	if got := first.Payload["turn"]; got != int32(1) {
		t.Errorf("turn 1 payload turn = %v, want 1", got)
	}
	if got := first.Payload["game_id"]; got != game.GameID.String() {
		t.Errorf("turn 1 payload game_id = %v, want %s", got, game.GameID)
	}
	if len(first.Recipients) != 2 {
		t.Errorf("turn 1 recipients = %d, want 2", len(first.Recipients))
	}
	recipientSet := map[uuid.UUID]struct{}{}
	for _, r := range first.Recipients {
		recipientSet[r] = struct{}{}
	}
	if _, ok := recipientSet[memberA]; !ok {
		t.Errorf("turn 1 missing memberA in recipients")
	}
	if _, ok := recipientSet[memberB]; !ok {
		t.Errorf("turn 1 missing memberB in recipients")
	}

	// Same turn re-delivered (duplicate snapshot, gateway replay) must
	// not re-emit at the lobby layer: prev catches up to merged.
	if err := svc.OnRuntimeSnapshot(context.Background(), game.GameID, lobby.RuntimeSnapshot{
		CurrentTurn:   1,
		RuntimeStatus: "running",
	}); err != nil {
		t.Fatalf("on-runtime-snapshot 1 replay: %v", err)
	}
	if got := len(publisher.byKind(lobby.NotificationGameTurnReady)); got != 1 {
		t.Fatalf("after duplicate turn 1 want 1 intent, got %d", got)
	}

	// Next turn advances → second emit with key anchored on turn 2.
	if err := svc.OnRuntimeSnapshot(context.Background(), game.GameID, lobby.RuntimeSnapshot{
		CurrentTurn:   2,
		RuntimeStatus: "running",
	}); err != nil {
		t.Fatalf("on-runtime-snapshot 2: %v", err)
	}
	intents = publisher.byKind(lobby.NotificationGameTurnReady)
	if len(intents) != 2 {
		t.Fatalf("after turn 2 want 2 turn-ready intents, got %d", len(intents))
	}
	wantKey2 := fmt.Sprintf("turn-ready:%s:2", game.GameID)
	if intents[1].IdempotencyKey != wantKey2 {
		t.Errorf("turn 2 idempotency key = %q, want %q", intents[1].IdempotencyKey, wantKey2)
	}
	if got := intents[1].Payload["turn"]; got != int32(2) {
		t.Errorf("turn 2 payload turn = %v, want 2", got)
	}
}
