package transcoder

import (
	"reflect"
	"strings"
	"testing"

	commonfbs "galaxy/schema/fbs/common"
	notificationfbs "galaxy/schema/fbs/notification"

	flatbuffers "github.com/google/flatbuffers/go"
	"github.com/google/uuid"
)

var (
	testNotificationGameID        = uuid.MustParse("11111111-1111-1111-1111-111111111111")
	testNotificationApplicationID = uuid.MustParse("22222222-2222-2222-2222-222222222222")
	testNotificationInviterID     = uuid.MustParse("33333333-3333-3333-3333-333333333333")
)

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

	tests := []struct {
		name   string
		source any
		encode func(any) ([]byte, error)
		decode func([]byte) (any, error)
	}{
		{
			name:   "lobby invite received",
			source: &LobbyInviteReceivedEvent{GameID: testNotificationGameID, InviterUserID: testNotificationInviterID},
			encode: func(v any) ([]byte, error) {
				return LobbyInviteReceivedEventToPayload(v.(*LobbyInviteReceivedEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToLobbyInviteReceivedEvent(data) },
		},
		{
			name:   "lobby invite revoked",
			source: &LobbyInviteRevokedEvent{GameID: testNotificationGameID},
			encode: func(v any) ([]byte, error) {
				return LobbyInviteRevokedEventToPayload(v.(*LobbyInviteRevokedEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToLobbyInviteRevokedEvent(data) },
		},
		{
			name:   "lobby application submitted",
			source: &LobbyApplicationSubmittedEvent{GameID: testNotificationGameID, ApplicationID: testNotificationApplicationID},
			encode: func(v any) ([]byte, error) {
				return LobbyApplicationSubmittedEventToPayload(v.(*LobbyApplicationSubmittedEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToLobbyApplicationSubmittedEvent(data) },
		},
		{
			name:   "lobby application approved",
			source: &LobbyApplicationApprovedEvent{GameID: testNotificationGameID},
			encode: func(v any) ([]byte, error) {
				return LobbyApplicationApprovedEventToPayload(v.(*LobbyApplicationApprovedEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToLobbyApplicationApprovedEvent(data) },
		},
		{
			name:   "lobby application rejected",
			source: &LobbyApplicationRejectedEvent{GameID: testNotificationGameID},
			encode: func(v any) ([]byte, error) {
				return LobbyApplicationRejectedEventToPayload(v.(*LobbyApplicationRejectedEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToLobbyApplicationRejectedEvent(data) },
		},
		{
			name:   "lobby membership removed",
			source: &LobbyMembershipRemovedEvent{Reason: "deleted"},
			encode: func(v any) ([]byte, error) {
				return LobbyMembershipRemovedEventToPayload(v.(*LobbyMembershipRemovedEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToLobbyMembershipRemovedEvent(data) },
		},
		{
			name:   "lobby membership blocked",
			source: &LobbyMembershipBlockedEvent{GameID: testNotificationGameID, Reason: "permanent_blocked"},
			encode: func(v any) ([]byte, error) {
				return LobbyMembershipBlockedEventToPayload(v.(*LobbyMembershipBlockedEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToLobbyMembershipBlockedEvent(data) },
		},
		{
			name:   "lobby race name registered",
			source: &LobbyRaceNameRegisteredEvent{RaceName: "Skylancer"},
			encode: func(v any) ([]byte, error) {
				return LobbyRaceNameRegisteredEventToPayload(v.(*LobbyRaceNameRegisteredEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToLobbyRaceNameRegisteredEvent(data) },
		},
		{
			name:   "lobby race name pending",
			source: &LobbyRaceNamePendingEvent{RaceName: "Skylancer", ExpiresAt: "2026-05-06T12:00:00Z"},
			encode: func(v any) ([]byte, error) {
				return LobbyRaceNamePendingEventToPayload(v.(*LobbyRaceNamePendingEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToLobbyRaceNamePendingEvent(data) },
		},
		{
			name:   "lobby race name expired",
			source: &LobbyRaceNameExpiredEvent{RaceName: "Skylancer"},
			encode: func(v any) ([]byte, error) {
				return LobbyRaceNameExpiredEventToPayload(v.(*LobbyRaceNameExpiredEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToLobbyRaceNameExpiredEvent(data) },
		},
		{
			name:   "runtime image pull failed",
			source: &RuntimeImagePullFailedEvent{GameID: testNotificationGameID, ImageRef: "gcr.io/example:1.0.0"},
			encode: func(v any) ([]byte, error) {
				return RuntimeImagePullFailedEventToPayload(v.(*RuntimeImagePullFailedEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToRuntimeImagePullFailedEvent(data) },
		},
		{
			name:   "runtime container start failed",
			source: &RuntimeContainerStartFailedEvent{GameID: testNotificationGameID},
			encode: func(v any) ([]byte, error) {
				return RuntimeContainerStartFailedEventToPayload(v.(*RuntimeContainerStartFailedEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToRuntimeContainerStartFailedEvent(data) },
		},
		{
			name:   "runtime start config invalid",
			source: &RuntimeStartConfigInvalidEvent{GameID: testNotificationGameID, Reason: "missing engine version"},
			encode: func(v any) ([]byte, error) {
				return RuntimeStartConfigInvalidEventToPayload(v.(*RuntimeStartConfigInvalidEvent))
			},
			decode: func(data []byte) (any, error) { return PayloadToRuntimeStartConfigInvalidEvent(data) },
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			payload, err := tt.encode(tt.source)
			if err != nil {
				t.Fatalf("encode payload: %v", err)
			}
			decoded, err := tt.decode(payload)
			if err != nil {
				t.Fatalf("decode payload: %v", err)
			}
			if !reflect.DeepEqual(tt.source, decoded) {
				t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", tt.source, decoded)
			}
		})
	}
}

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

	tests := []struct {
		name string
		call func() error
	}{
		{"lobby invite received", func() error { _, err := LobbyInviteReceivedEventToPayload(nil); return err }},
		{"lobby invite revoked", func() error { _, err := LobbyInviteRevokedEventToPayload(nil); return err }},
		{"lobby application submitted", func() error { _, err := LobbyApplicationSubmittedEventToPayload(nil); return err }},
		{"lobby application approved", func() error { _, err := LobbyApplicationApprovedEventToPayload(nil); return err }},
		{"lobby application rejected", func() error { _, err := LobbyApplicationRejectedEventToPayload(nil); return err }},
		{"lobby membership removed", func() error { _, err := LobbyMembershipRemovedEventToPayload(nil); return err }},
		{"lobby membership blocked", func() error { _, err := LobbyMembershipBlockedEventToPayload(nil); return err }},
		{"lobby race name registered", func() error { _, err := LobbyRaceNameRegisteredEventToPayload(nil); return err }},
		{"lobby race name pending", func() error { _, err := LobbyRaceNamePendingEventToPayload(nil); return err }},
		{"lobby race name expired", func() error { _, err := LobbyRaceNameExpiredEventToPayload(nil); return err }},
		{"runtime image pull failed", func() error { _, err := RuntimeImagePullFailedEventToPayload(nil); return err }},
		{"runtime container start failed", func() error { _, err := RuntimeContainerStartFailedEventToPayload(nil); return err }},
		{"runtime start config invalid", func() error { _, err := RuntimeStartConfigInvalidEventToPayload(nil); return err }},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			if err := tt.call(); err == nil {
				t.Fatal("expected error")
			}
		})
	}
}

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

	tests := []struct {
		name string
		call func() error
	}{
		{"lobby invite received", func() error { _, err := PayloadToLobbyInviteReceivedEvent(nil); return err }},
		{"lobby invite revoked", func() error { _, err := PayloadToLobbyInviteRevokedEvent(nil); return err }},
		{"lobby application submitted", func() error { _, err := PayloadToLobbyApplicationSubmittedEvent(nil); return err }},
		{"lobby application approved", func() error { _, err := PayloadToLobbyApplicationApprovedEvent(nil); return err }},
		{"lobby application rejected", func() error { _, err := PayloadToLobbyApplicationRejectedEvent(nil); return err }},
		{"lobby membership removed", func() error { _, err := PayloadToLobbyMembershipRemovedEvent(nil); return err }},
		{"lobby membership blocked", func() error { _, err := PayloadToLobbyMembershipBlockedEvent(nil); return err }},
		{"lobby race name registered", func() error { _, err := PayloadToLobbyRaceNameRegisteredEvent(nil); return err }},
		{"lobby race name pending", func() error { _, err := PayloadToLobbyRaceNamePendingEvent(nil); return err }},
		{"lobby race name expired", func() error { _, err := PayloadToLobbyRaceNameExpiredEvent(nil); return err }},
		{"runtime image pull failed", func() error { _, err := PayloadToRuntimeImagePullFailedEvent(nil); return err }},
		{"runtime container start failed", func() error { _, err := PayloadToRuntimeContainerStartFailedEvent(nil); return err }},
		{"runtime start config invalid", func() error { _, err := PayloadToRuntimeStartConfigInvalidEvent(nil); return err }},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			if err := tt.call(); err == nil {
				t.Fatal("expected error")
			}
		})
	}
}

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

	tests := []struct {
		name string
		call func() error
		want string
	}{
		{
			name: "lobby invite received game_id",
			call: func() error { _, err := LobbyInviteReceivedEventToPayload(&LobbyInviteReceivedEvent{InviterUserID: testNotificationInviterID}); return err },
			want: "game_id is empty",
		},
		{
			name: "lobby invite received inviter",
			call: func() error { _, err := LobbyInviteReceivedEventToPayload(&LobbyInviteReceivedEvent{GameID: testNotificationGameID}); return err },
			want: "inviter_user_id is empty",
		},
		{
			name: "lobby application submitted game_id",
			call: func() error { _, err := LobbyApplicationSubmittedEventToPayload(&LobbyApplicationSubmittedEvent{ApplicationID: testNotificationApplicationID}); return err },
			want: "game_id is empty",
		},
		{
			name: "lobby application submitted application_id",
			call: func() error { _, err := LobbyApplicationSubmittedEventToPayload(&LobbyApplicationSubmittedEvent{GameID: testNotificationGameID}); return err },
			want: "application_id is empty",
		},
		{
			name: "runtime image pull failed game_id",
			call: func() error { _, err := RuntimeImagePullFailedEventToPayload(&RuntimeImagePullFailedEvent{ImageRef: "x"}); return err },
			want: "game_id is empty",
		},
	}

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

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

	tests := []struct {
		name    string
		payload func() []byte
		decode  func([]byte) error
		want    string
	}{
		{
			name: "lobby invite received game_id",
			payload: func() []byte {
				builder := flatbuffers.NewBuilder(32)
				notificationfbs.LobbyInviteReceivedEventStart(builder)
				offset := notificationfbs.LobbyInviteReceivedEventEnd(builder)
				notificationfbs.FinishLobbyInviteReceivedEventBuffer(builder, offset)
				return builder.FinishedBytes()
			},
			decode: func(data []byte) error { _, err := PayloadToLobbyInviteReceivedEvent(data); return err },
			want:   "game_id is missing",
		},
		{
			name: "lobby invite received inviter",
			payload: func() []byte {
				builder := flatbuffers.NewBuilder(32)
				notificationfbs.LobbyInviteReceivedEventStart(builder)
				notificationfbs.LobbyInviteReceivedEventAddGameId(builder, commonfbs.CreateUUID(builder, 1, 2))
				offset := notificationfbs.LobbyInviteReceivedEventEnd(builder)
				notificationfbs.FinishLobbyInviteReceivedEventBuffer(builder, offset)
				return builder.FinishedBytes()
			},
			decode: func(data []byte) error { _, err := PayloadToLobbyInviteReceivedEvent(data); return err },
			want:   "inviter_user_id is missing",
		},
		{
			name: "lobby application submitted application_id",
			payload: func() []byte {
				builder := flatbuffers.NewBuilder(32)
				notificationfbs.LobbyApplicationSubmittedEventStart(builder)
				notificationfbs.LobbyApplicationSubmittedEventAddGameId(builder, commonfbs.CreateUUID(builder, 1, 2))
				offset := notificationfbs.LobbyApplicationSubmittedEventEnd(builder)
				notificationfbs.FinishLobbyApplicationSubmittedEventBuffer(builder, offset)
				return builder.FinishedBytes()
			},
			decode: func(data []byte) error { _, err := PayloadToLobbyApplicationSubmittedEvent(data); return err },
			want:   "application_id is missing",
		},
	}

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