package notify_test

import (
	"testing"
	"time"

	"github.com/google/uuid"

	"scrabble/backend/internal/engine"
	"scrabble/backend/internal/notify"
	fb "scrabble/pkg/fbs/scrabblefb"
)

func TestHubDeliversToSubscriber(t *testing.T) {
	h := notify.NewHub(4)
	ch, cancel := h.Subscribe()
	defer cancel()

	want := notify.Intent{UserID: uuid.New(), Kind: notify.KindYourTurn, Payload: []byte{1, 2, 3}}
	h.Publish(want)

	select {
	case got := <-ch:
		if got.Kind != want.Kind || got.UserID != want.UserID {
			t.Fatalf("delivered %+v, want %+v", got, want)
		}
	case <-time.After(time.Second):
		t.Fatal("no delivery within timeout")
	}
}

func TestHubDropsWhenSubscriberBufferFull(t *testing.T) {
	h := notify.NewHub(1)
	ch, cancel := h.Subscribe()
	defer cancel()

	in := notify.Intent{UserID: uuid.New(), Kind: notify.KindNudge}
	// Buffer holds one; the second and third are dropped, and Publish must not block.
	h.Publish(in, in, in)

	if got := len(ch); got != 1 {
		t.Fatalf("buffered %d intents, want 1 (rest dropped)", got)
	}
}

func TestHubUnsubscribeClosesChannel(t *testing.T) {
	h := notify.NewHub(2)
	ch, cancel := h.Subscribe()
	cancel()

	if _, ok := <-ch; ok {
		t.Fatal("channel should be closed after unsubscribe")
	}
	// Publishing after unsubscribe must be safe (no panic, no delivery).
	h.Publish(notify.Intent{Kind: notify.KindMatchFound})
}

func TestNopPublisherDiscards(t *testing.T) {
	var p notify.Publisher = notify.Nop{}
	p.Publish(notify.Intent{Kind: notify.KindYourTurn}) // must not panic
}

func TestYourTurnPayloadRoundTrips(t *testing.T) {
	uid, gid := uuid.New(), uuid.New()
	in := notify.YourTurn(uid, gid, time.Unix(1717000000, 0), "Ann", "play", "STOOL", "120:95", 7)
	if in.UserID != uid || in.Kind != notify.KindYourTurn || in.EventID == "" {
		t.Fatalf("intent metadata wrong: %+v", in)
	}
	ev := fb.GetRootAsYourTurnEvent(in.Payload, 0)
	if got := string(ev.GameId()); got != gid.String() {
		t.Fatalf("game id = %q, want %q", got, gid)
	}
	if got := ev.DeadlineUnix(); got != 1717000000 {
		t.Fatalf("deadline = %d, want 1717000000", got)
	}
	if got := ev.MoveCount(); got != 7 {
		t.Fatalf("move_count = %d, want 7", got)
	}
	if string(ev.OpponentName()) != "Ann" || string(ev.LastAction()) != "play" ||
		string(ev.LastWord()) != "STOOL" || string(ev.ScoreLine()) != "120:95" {
		t.Fatalf("enriched fields wrong: name=%q action=%q word=%q score=%q",
			ev.OpponentName(), ev.LastAction(), ev.LastWord(), ev.ScoreLine())
	}
}

func TestNudgePayloadRoundTrips(t *testing.T) {
	uid, gid, from := uuid.New(), uuid.New(), uuid.New()
	in := notify.Nudge(uid, gid, from, "Ann")
	if in.UserID != uid || in.Kind != notify.KindNudge || in.EventID == "" {
		t.Fatalf("intent metadata wrong: %+v", in)
	}
	ev := fb.GetRootAsNudgeEvent(in.Payload, 0)
	if string(ev.GameId()) != gid.String() || string(ev.FromUserId()) != from.String() || string(ev.SenderName()) != "Ann" {
		t.Fatalf("nudge fields wrong: game=%q from=%q name=%q", ev.GameId(), ev.FromUserId(), ev.SenderName())
	}
}

func TestGameOverPayloadRoundTrips(t *testing.T) {
	uid, gid := uuid.New(), uuid.New()
	summary := notify.GameSummary{ID: gid.String(), Status: "finished", MoveCount: 18, Seats: []notify.SeatStanding{{Seat: 0, Score: 120, IsWinner: true}}}
	in := notify.GameOver(uid, gid, "won", "120:95:80", summary)
	if in.UserID != uid || in.Kind != notify.KindGameOver || in.EventID == "" {
		t.Fatalf("intent metadata wrong: %+v", in)
	}
	ev := fb.GetRootAsGameOverEvent(in.Payload, 0)
	if string(ev.GameId()) != gid.String() || string(ev.Result()) != "won" || string(ev.ScoreLine()) != "120:95:80" {
		t.Fatalf("game_over fields wrong: game=%q result=%q score=%q", ev.GameId(), ev.Result(), ev.ScoreLine())
	}
	g := ev.Game(nil)
	if g == nil || string(g.Id()) != gid.String() || g.MoveCount() != 18 || g.SeatsLength() != 1 {
		t.Fatalf("final game summary wrong: %+v", g)
	}
}

func TestOpponentMovedPayloadRoundTrips(t *testing.T) {
	uid, gid := uuid.New(), uuid.New()
	move := engine.MoveRecord{Player: 1, Action: engine.ActionPlay, Words: []string{"STOOL"}, Score: 24, Total: 130}
	summary := notify.GameSummary{ID: gid.String(), MoveCount: 9, ToMove: 0, Kind: 2, Seats: []notify.SeatStanding{{Seat: 1, Score: 130}}}
	in := notify.OpponentMoved(uid, gid, move, summary, 42)
	if in.Kind != notify.KindOpponentMoved {
		t.Fatalf("kind = %q", in.Kind)
	}
	ev := fb.GetRootAsOpponentMovedEvent(in.Payload, 0)
	if string(ev.GameId()) != gid.String() {
		t.Fatalf("game id = %q", ev.GameId())
	}
	// The delta: the move, the post-move summary and the bag size.
	if ev.BagLen() != 42 {
		t.Fatalf("bag_len = %d, want 42", ev.BagLen())
	}
	m := ev.Move(nil)
	if m == nil || m.Player() != 1 || string(m.Action()) != "play" || m.Total() != 130 {
		t.Fatalf("move wrong: %+v", m)
	}
	if g := ev.Game(nil); g == nil || g.MoveCount() != 9 || g.ToMove() != 0 || g.Kind() != 2 {
		t.Fatalf("game summary wrong: %+v", ev.Game(nil))
	}
}

func TestMatchFoundCarriesInitialState(t *testing.T) {
	uid, gid := uuid.New(), uuid.New()
	state := notify.PlayerState{
		Game:   notify.GameSummary{ID: gid.String(), Variant: "scrabble_en", Seats: []notify.SeatStanding{{Seat: 0, DisplayName: "Ann"}}},
		Seat:   0,
		Rack:   []int{0, 1, 2, 255},
		BagLen: 86,
	}
	in := notify.MatchFound(uid, gid, state)
	if in.UserID != uid || in.Kind != notify.KindMatchFound {
		t.Fatalf("intent metadata wrong: %+v", in)
	}
	ev := fb.GetRootAsMatchFoundEvent(in.Payload, 0)
	if string(ev.GameId()) != gid.String() {
		t.Fatalf("game id = %q", ev.GameId())
	}
	st := ev.State(nil)
	if st == nil || st.Seat() != 0 || st.BagLen() != 86 || st.RackLength() != 4 || st.Rack(3) != 255 {
		t.Fatalf("initial state wrong: %+v", st)
	}
	if g := st.Game(nil); g == nil || string(g.Variant()) != "scrabble_en" {
		t.Fatalf("state game wrong: %+v", st.Game(nil))
	}
}

func TestNotificationInvitationCarriesInvitation(t *testing.T) {
	uid := uuid.New()
	inv := notify.InvitationSummary{
		ID:              "inv-1",
		Inviter:         notify.AccountRef{AccountID: "a-1", DisplayName: "Ann"},
		Invitees:        []notify.InvitationInvitee{{AccountID: "b-1", DisplayName: "Bob", Seat: 1, Response: "pending"}},
		Variant:         "erudit_ru",
		TurnTimeoutSecs: 86400,
		Status:          "pending",
	}
	in := notify.NotificationInvitation(uid, inv)
	if in.Kind != notify.KindNotification {
		t.Fatalf("kind = %q", in.Kind)
	}
	ev := fb.GetRootAsNotificationEvent(in.Payload, 0)
	if string(ev.Kind()) != notify.NotifyInvitation {
		t.Fatalf("sub-kind = %q, want %q", ev.Kind(), notify.NotifyInvitation)
	}
	got := ev.Invitation(nil)
	if got == nil || string(got.Id()) != "inv-1" || string(got.Variant()) != "erudit_ru" || got.InviteesLength() != 1 {
		t.Fatalf("invitation wrong: %+v", got)
	}
	var iv fb.InvitationInvitee
	if !got.Invitees(&iv, 0) || string(iv.DisplayName()) != "Bob" || iv.Seat() != 1 {
		t.Fatalf("invitee wrong")
	}
	if inviter := got.Inviter(nil); inviter == nil || string(inviter.DisplayName()) != "Ann" {
		t.Fatalf("inviter wrong")
	}
}

func TestNotificationInvitationUpdateCarriesTerminalInvitation(t *testing.T) {
	uid := uuid.New()
	inv := notify.InvitationSummary{ID: "inv-9", Variant: "scrabble_en", Status: "declined"}
	in := notify.NotificationInvitationUpdate(uid, inv)
	if in.Kind != notify.KindNotification {
		t.Fatalf("kind = %q", in.Kind)
	}
	ev := fb.GetRootAsNotificationEvent(in.Payload, 0)
	if string(ev.Kind()) != notify.NotifyInvitationUpdate {
		t.Fatalf("sub-kind = %q, want %q", ev.Kind(), notify.NotifyInvitationUpdate)
	}
	got := ev.Invitation(nil)
	if got == nil || string(got.Id()) != "inv-9" || string(got.Status()) != "declined" {
		t.Fatalf("invitation wrong: %+v", got)
	}
}

func TestNotificationAccountCarriesAccount(t *testing.T) {
	uid := uuid.New()
	in := notify.NotificationAccount(uid, notify.NotifyFriendRequest, notify.AccountRef{AccountID: "a-1", DisplayName: "Ann"})
	ev := fb.GetRootAsNotificationEvent(in.Payload, 0)
	if string(ev.Kind()) != notify.NotifyFriendRequest {
		t.Fatalf("sub-kind = %q", ev.Kind())
	}
	acc := ev.Account(nil)
	if acc == nil || string(acc.AccountId()) != "a-1" || string(acc.DisplayName()) != "Ann" {
		t.Fatalf("account wrong: %+v", acc)
	}
}

func TestChatMessagePayloadRoundTrips(t *testing.T) {
	uid, gid, sid := uuid.New(), uuid.New(), uuid.New()
	in := notify.ChatMessage(uid, gid, sid, "msg-1", "message", "hi", time.Unix(1717000001, 0))
	if in.Kind != notify.KindChatMessage {
		t.Fatalf("kind = %q", in.Kind)
	}
	ev := fb.GetRootAsChatMessage(in.Payload, 0)
	if string(ev.Id()) != "msg-1" || string(ev.SenderId()) != sid.String() || string(ev.Body()) != "hi" || ev.CreatedAtUnix() != 1717000001 {
		t.Fatalf("decoded wrong chat message: %+v", ev)
	}
}

func TestNotificationPayloadRoundTrips(t *testing.T) {
	uid := uuid.New()
	in := notify.Notification(uid, notify.NotifyFriendRequest)
	if in.UserID != uid || in.Kind != notify.KindNotification || in.EventID == "" {
		t.Fatalf("intent metadata wrong: %+v", in)
	}
	ev := fb.GetRootAsNotificationEvent(in.Payload, 0)
	if got := string(ev.Kind()); got != notify.NotifyFriendRequest {
		t.Fatalf("notification sub-kind = %q, want %q", got, notify.NotifyFriendRequest)
	}
}
