package notification

import (
	"strings"
	"testing"

	"galaxy/backend/push"

	"github.com/google/uuid"
)

// jsonFriendlyKinds lists catalog kinds whose payload is small and
// stable enough that the gateway-bound encoding stays JSON instead of
// FlatBuffers. The default for new producers is still FB; declaring a
// kind here is a deliberate decision baked into the build target's
// payload contract.
//
// `game.turn.ready` ships `{game_id, turn}` only, the UI parses it
// inline in `routes/games/[id]/+layout.svelte` (Phase 24), and no
// other consumer reads the payload — adopting the FB encoder would
// require a new TS notification stub set and the regen tooling for
// `pkg/schema/fbs/notification.fbs` without buying anything.
//
// `game.paused` (Phase 25) follows the same JSON-friendly contract:
// payload is `{game_id, turn, reason}` consumed by the same in-game
// shell layout, so there is no value in dragging a FB schema in for
// one consumer.
//
// `diplomail.message.received` (Stage A) carries the message metadata
// plus an unread-count snapshot. Stage A intentionally ships the
// payload as JSON so the diplomail UI can iterate on the contract
// without a FB schema dance; a later stage can promote it.
var jsonFriendlyKinds = map[string]bool{
	KindGameTurnReady:     true,
	KindGamePaused:        true,
	KindDiplomailReceived: true,
}

// TestBuildClientPushEventCoversCatalog asserts that every catalog kind
// is exercised by this test, that FB-typed kinds return a
// `preMarshaledEvent`, and that JSON-friendly kinds (see
// `jsonFriendlyKinds` above) return a `push.JSONEvent`.
func TestBuildClientPushEventCoversCatalog(t *testing.T) {
	t.Parallel()

	gameID := uuid.MustParse("11111111-1111-1111-1111-111111111111")
	applicationID := uuid.MustParse("22222222-2222-2222-2222-222222222222")
	inviterID := uuid.MustParse("33333333-3333-3333-3333-333333333333")

	tests := []struct {
		name    string
		kind    string
		payload map[string]any
	}{
		{"invite received", KindLobbyInviteReceived, map[string]any{
			"game_id":         gameID.String(),
			"inviter_user_id": inviterID.String(),
		}},
		{"invite revoked", KindLobbyInviteRevoked, map[string]any{
			"game_id": gameID.String(),
		}},
		{"application submitted", KindLobbyApplicationSubmitted, map[string]any{
			"game_id":        gameID.String(),
			"application_id": applicationID.String(),
		}},
		{"application approved", KindLobbyApplicationApproved, map[string]any{"game_id": gameID.String()}},
		{"application rejected", KindLobbyApplicationRejected, map[string]any{"game_id": gameID.String()}},
		{"membership removed", KindLobbyMembershipRemoved, map[string]any{"reason": "deleted"}},
		{"membership blocked", KindLobbyMembershipBlocked, map[string]any{
			"game_id": gameID.String(),
			"reason":  "permanent_blocked",
		}},
		{"race name registered", KindLobbyRaceNameRegistered, map[string]any{"race_name": "Skylancer"}},
		{"race name pending", KindLobbyRaceNamePending, map[string]any{
			"race_name":  "Skylancer",
			"expires_at": "2026-05-06T12:00:00Z",
		}},
		{"race name expired", KindLobbyRaceNameExpired, map[string]any{"race_name": "Skylancer"}},
		{"runtime image pull failed", KindRuntimeImagePullFailed, map[string]any{
			"game_id":   gameID.String(),
			"image_ref": "gcr.io/example:1.0.0",
		}},
		{"runtime container start failed", KindRuntimeContainerStartFailed, map[string]any{"game_id": gameID.String()}},
		{"runtime start config invalid", KindRuntimeStartConfigInvalid, map[string]any{
			"game_id": gameID.String(),
			"reason":  "missing engine version",
		}},
		{"game turn ready", KindGameTurnReady, map[string]any{
			"game_id": gameID.String(),
			"turn":    int32(7),
		}},
		{"game paused", KindGamePaused, map[string]any{
			"game_id": gameID.String(),
			"turn":    int32(7),
			"reason":  "generation_failed",
		}},
		{"diplomail message received", KindDiplomailReceived, map[string]any{
			"message_id":   gameID.String(),
			"game_id":      gameID.String(),
			"kind":         "personal",
			"sender_kind":  "player",
			"subject":      "Trade deal",
			"preview":      "Care to talk gas mining?",
			"preview_lang": "en",
			"unread_total": 3,
			"unread_game":  1,
		}},
	}

	seenKinds := map[string]bool{}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			event, err := buildClientPushEvent(tt.kind, tt.payload)
			if err != nil {
				t.Fatalf("build %s: %v", tt.kind, err)
			}
			if event.Kind() != tt.kind {
				t.Fatalf("Kind() = %q, want %q", event.Kind(), tt.kind)
			}
			bytes, err := event.Marshal()
			if err != nil {
				t.Fatalf("Marshal: %v", err)
			}
			if len(bytes) == 0 {
				t.Fatalf("Marshal returned empty bytes")
			}
			_, isJSON := event.(push.JSONEvent)
			wantJSON := jsonFriendlyKinds[tt.kind]
			if isJSON != wantJSON {
				t.Fatalf("kind %s: JSONEvent=%v, want JSONEvent=%v", tt.kind, isJSON, wantJSON)
			}
		})
		seenKinds[tt.kind] = true
	}
	for _, kind := range SupportedKinds() {
		if !seenKinds[kind] {
			t.Errorf("catalog kind %q is not covered by this test", kind)
		}
	}
}

func TestBuildClientPushEventUnknownKindFallsBackToJSON(t *testing.T) {
	t.Parallel()

	event, err := buildClientPushEvent("unknown.kind", map[string]any{"x": 1})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if _, ok := event.(push.JSONEvent); !ok {
		t.Fatalf("expected JSONEvent fallback, got %T", event)
	}
	if event.Kind() != "unknown.kind" {
		t.Fatalf("Kind() = %q", event.Kind())
	}
}

func TestBuildClientPushEventRejectsBrokenPayloads(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		kind    string
		payload map[string]any
		want    string
	}{
		{
			name:    "missing required uuid",
			kind:    KindLobbyApplicationSubmitted,
			payload: map[string]any{"game_id": uuid.NewString()},
			want:    "application_id is missing",
		},
		{
			name:    "non-uuid string",
			kind:    KindLobbyInviteRevoked,
			payload: map[string]any{"game_id": "not-a-uuid"},
			want:    "is not a uuid",
		},
		{
			name:    "uuid not a string",
			kind:    KindLobbyInviteRevoked,
			payload: map[string]any{"game_id": 42},
			want:    "must be a string",
		},
		{
			name:    "missing required string",
			kind:    KindLobbyRaceNameRegistered,
			payload: map[string]any{},
			want:    "race_name is missing",
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			_, err := buildClientPushEvent(tt.kind, tt.payload)
			if err == nil {
				t.Fatal("expected error")
			}
			if !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("unexpected error: %v", err)
			}
		})
	}
}
