package integration_test

import (
	"context"
	"net/http"
	"strings"
	"testing"
	"time"

	"galaxy/integration/testenv"
)

// TestNotificationFlow_LobbyInvite asserts that a `lobby.invite.received`
// intent triggers a push frame on the gateway SubscribeEvents stream
// for the invitee AND a captured email at mailpit.
func TestNotificationFlow_LobbyInvite(t *testing.T) {
	plat := testenv.Bootstrap(t, testenv.BootstrapOptions{})
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()

	// Register an engine version so private-game creation can pass
	// validation.
	admin := testenv.NewBackendAdminClient(plat.Backend.HTTPURL, plat.Backend.AdminUser, plat.Backend.AdminPassword)
	if _, resp, err := admin.Do(ctx, http.MethodPost, "/api/v1/admin/engine-versions", map[string]any{
		"version": "v1.0.0", "image_ref": "galaxy/game:integration", "enabled": true,
	}); err != nil || resp.StatusCode/100 != 2 {
		t.Fatalf("seed engine_version: err=%v resp=%v", err, resp)
	}

	inviter := testenv.RegisterSession(t, plat, "inviter@example.com")
	invitee := testenv.RegisterSession(t, plat, "invitee@example.com")
	inviterUser, err := inviter.LookupUserID(ctx, plat)
	if err != nil {
		t.Fatalf("resolve inviter user_id: %v", err)
	}
	inviteeUser, err := invitee.LookupUserID(ctx, plat)
	if err != nil {
		t.Fatalf("resolve invitee user_id: %v", err)
	}

	// Inviter creates a private game.
	inviterClient := testenv.NewBackendUserClient(plat.Backend.HTTPURL, inviterUser)
	gameBody := map[string]any{
		"game_name":             "Private Sortie",
		"visibility":            "private",
		"min_players":           2,
		"max_players":           4,
		"start_gap_hours":       1,
		"start_gap_players":     2,
		"enrollment_ends_at":    time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339),
		"turn_schedule":         "0 * * * *",
		"target_engine_version": "v1.0.0",
	}
	raw, resp, err := inviterClient.Do(ctx, http.MethodPost, "/api/v1/user/lobby/games", gameBody)
	if err != nil || resp.StatusCode != http.StatusCreated {
		t.Fatalf("create private game: err=%v status=%d body=%s", err, resp.StatusCode, string(raw))
	}
	var game struct {
		GameID string `json:"game_id"`
	}
	if err := decodeJSON(raw, &game); err != nil {
		t.Fatalf("decode game: %v", err)
	}

	// Invitee opens SubscribeEvents stream BEFORE the invite is
	// issued so we cannot miss the push frame.
	gw, err := invitee.DialAuthenticated(ctx, plat)
	if err != nil {
		t.Fatalf("invitee dial: %v", err)
	}
	defer gw.Close()
	streamCtx, streamCancel := context.WithCancel(ctx)
	defer streamCancel()
	events, errCh, err := gw.SubscribeEvents(streamCtx, "gateway.subscribe")
	if err != nil {
		t.Fatalf("subscribe events: %v", err)
	}

	// Drain the bootstrap server-time event before the test gets
	// going so the invite event is the next thing observed.
	select {
	case <-events:
	case err := <-errCh:
		t.Fatalf("subscribe stream error before invite: %v", err)
	case <-time.After(5 * time.Second):
		t.Fatalf("bootstrap event not received within 5s")
	}

	// Now clear mailpit so we can detect the new invite email.
	if err := plat.Mailpit.DeleteAll(ctx); err != nil {
		t.Fatalf("clear mailpit: %v", err)
	}

	// Inviter issues an invite for invitee.
	inviteBody := map[string]any{
		"invited_user_id": inviteeUser,
		"race_name":       "Invitee-Crew",
	}
	raw, resp, err = inviterClient.Do(ctx, http.MethodPost, "/api/v1/user/lobby/games/"+game.GameID+"/invites", inviteBody)
	if err != nil || resp.StatusCode != http.StatusCreated {
		t.Fatalf("issue invite: err=%v status=%d body=%s", err, resp.StatusCode, string(raw))
	}

	// Push: expect a non-bootstrap event.
	pushDeadline := time.After(20 * time.Second)
	gotPush := false
PUSH:
	for {
		select {
		case ev, ok := <-events:
			if !ok {
				break PUSH
			}
			if ev == nil || ev.GetEventType() == "gateway.server_time" {
				continue
			}
			gotPush = true
			break PUSH
		case err := <-errCh:
			t.Fatalf("subscribe stream error during invite: %v", err)
		case <-pushDeadline:
			break PUSH
		}
	}
	if !gotPush {
		t.Fatalf("no push event received for lobby invite within 20s")
	}

	// Email: expect mailpit to receive a message addressed to invitee.
	if _, err := plat.Mailpit.WaitForMessage(ctx, "to:"+invitee.Email, 30*time.Second); err != nil {
		t.Fatalf("invite email not captured: %v", err)
	}
	_ = strings.TrimSpace
}

func decodeJSON(raw []byte, v any) error {
	return jsonUnmarshal(raw, v)
}
