package backendclient_test

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"

	"galaxy/gateway/internal/backendclient"
	diplomailmodel "galaxy/model/diplomail"
	commonfbs "galaxy/schema/fbs/common"
	fbs "galaxy/schema/fbs/diplomail"

	flatbuffers "github.com/google/flatbuffers/go"
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

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

	gameID := uuid.MustParse("11111111-2222-3333-4444-555555555555")
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/mail/inbox", r.URL.Path)
		require.Equal(t, "user-1", r.Header.Get(backendclient.HeaderUserID))
		writeJSON(t, w, http.StatusOK, map[string]any{
			"items": []map[string]any{
				{
					"message_id":          "00000000-0000-0000-0000-000000000001",
					"game_id":             gameID.String(),
					"kind":                "personal",
					"sender_kind":         "player",
					"sender_user_id":      "00000000-0000-0000-0000-000000000010",
					"sender_username":     "alice",
					"sender_race_name":    "AliceRace",
					"subject":             "hi",
					"body":                "hello there",
					"body_lang":           "en",
					"broadcast_scope":     "single",
					"created_at":          "2026-05-15T12:00:00Z",
					"recipient_user_id":   "00000000-0000-0000-0000-000000000020",
					"recipient_user_name": "bob",
					"recipient_race_name": "BobRace",
				},
			},
		})
	}))
	t.Cleanup(server.Close)

	client := newRESTClient(t, server)
	payload := buildInboxRequest(gameID)
	cmd := newAuthCommand(t, diplomailmodel.MessageTypeUserGamesMailInbox, payload)
	result, err := client.ExecuteMailCommand(context.Background(), cmd)
	require.NoError(t, err)
	assert.Equal(t, "ok", result.ResultCode)

	resp := fbs.GetRootAsInboxResponse(result.PayloadBytes, 0)
	require.Equal(t, 1, resp.ItemsLength())
	var item fbs.MailMessage
	require.True(t, resp.Items(&item, 0))
	assert.Equal(t, "00000000-0000-0000-0000-000000000001", string(item.MessageId()))
	assert.Equal(t, "AliceRace", string(item.SenderRaceName()))
	assert.Equal(t, "BobRace", string(item.RecipientRaceName()))
}

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

	gameID := uuid.MustParse("22222222-3333-4444-5555-666666666666")
	var captured struct {
		Body              string
		RecipientUserID   string
		RecipientRaceName string
	}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPost, r.Method)
		require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/mail/messages", r.URL.Path)
		var req map[string]any
		raw, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		require.NoError(t, json.Unmarshal(raw, &req))
		if v, ok := req["body"].(string); ok {
			captured.Body = v
		}
		if v, ok := req["recipient_user_id"].(string); ok {
			captured.RecipientUserID = v
		}
		if v, ok := req["recipient_race_name"].(string); ok {
			captured.RecipientRaceName = v
		}
		writeJSON(t, w, http.StatusCreated, map[string]any{
			"message_id":          "00000000-0000-0000-0000-000000000099",
			"game_id":             gameID.String(),
			"kind":                "personal",
			"sender_kind":         "player",
			"sender_user_id":      "00000000-0000-0000-0000-000000000010",
			"sender_race_name":    "Senders",
			"body":                captured.Body,
			"body_lang":           "en",
			"broadcast_scope":     "single",
			"created_at":          "2026-05-15T12:00:00Z",
			"recipient_user_id":   "00000000-0000-0000-0000-000000000020",
			"recipient_race_name": "Receivers",
		})
	}))
	t.Cleanup(server.Close)

	client := newRESTClient(t, server)
	payload := buildSendRequestByRaceName(gameID, "Receivers", "let us talk")
	cmd := newAuthCommand(t, diplomailmodel.MessageTypeUserGamesMailSend, payload)
	result, err := client.ExecuteMailCommand(context.Background(), cmd)
	require.NoError(t, err)
	assert.Equal(t, "ok", result.ResultCode)

	resp := fbs.GetRootAsSendResponse(result.PayloadBytes, 0)
	require.NotNil(t, resp.Message(nil))
	msg := resp.Message(nil)
	assert.Equal(t, "let us talk", string(msg.Body()))
	assert.Equal(t, "Senders", string(msg.SenderRaceName()))
	assert.Equal(t, "Receivers", string(msg.RecipientRaceName()))

	assert.Empty(t, captured.RecipientUserID)
	assert.Equal(t, "Receivers", captured.RecipientRaceName)
	assert.Equal(t, "let us talk", captured.Body)
}

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

	gameID := uuid.MustParse("33333333-4444-5555-6666-777777777777")
	messageID := uuid.MustParse("00000000-0000-0000-0000-0000000000aa")
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPost, r.Method)
		require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/mail/messages/"+messageID.String()+"/read", r.URL.Path)
		writeJSON(t, w, http.StatusOK, map[string]any{
			"message_id": messageID.String(),
			"read_at":    "2026-05-15T12:34:56Z",
		})
	}))
	t.Cleanup(server.Close)

	client := newRESTClient(t, server)
	payload := buildReadRequest(gameID, messageID)
	cmd := newAuthCommand(t, diplomailmodel.MessageTypeUserGamesMailRead, payload)
	result, err := client.ExecuteMailCommand(context.Background(), cmd)
	require.NoError(t, err)

	resp := fbs.GetRootAsReadResponse(result.PayloadBytes, 0)
	state := resp.State(nil)
	require.NotNil(t, state)
	assert.Equal(t, messageID.String(), string(state.MessageId()))
	assert.NotZero(t, state.ReadAtMs())
}

// buildInboxRequest emits a FlatBuffers InboxRequest envelope with
// the supplied game_id.
func buildInboxRequest(gameID uuid.UUID) []byte {
	builder := flatbuffers.NewBuilder(64)
	hi, lo := uuidToHiLo(gameID)
	fbs.InboxRequestStart(builder)
	fbs.InboxRequestAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
	root := fbs.InboxRequestEnd(builder)
	fbs.FinishInboxRequestBuffer(builder, root)
	return builder.FinishedBytes()
}

// buildSendRequestByRaceName emits a FlatBuffers SendRequest that
// addresses the recipient by race name rather than user_id.
func buildSendRequestByRaceName(gameID uuid.UUID, raceName, body string) []byte {
	builder := flatbuffers.NewBuilder(128)
	raceOff := builder.CreateString(raceName)
	bodyOff := builder.CreateString(body)
	hi, lo := uuidToHiLo(gameID)
	fbs.SendRequestStart(builder)
	fbs.SendRequestAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
	fbs.SendRequestAddRecipientRaceName(builder, raceOff)
	fbs.SendRequestAddBody(builder, bodyOff)
	root := fbs.SendRequestEnd(builder)
	fbs.FinishSendRequestBuffer(builder, root)
	return builder.FinishedBytes()
}

// buildReadRequest emits a FlatBuffers ReadRequest envelope.
func buildReadRequest(gameID, messageID uuid.UUID) []byte {
	builder := flatbuffers.NewBuilder(64)
	gameHi, gameLo := uuidToHiLo(gameID)
	msgHi, msgLo := uuidToHiLo(messageID)
	fbs.ReadRequestStart(builder)
	fbs.ReadRequestAddGameId(builder, commonfbs.CreateUUID(builder, gameHi, gameLo))
	fbs.ReadRequestAddMessageId(builder, commonfbs.CreateUUID(builder, msgHi, msgLo))
	root := fbs.ReadRequestEnd(builder)
	fbs.FinishReadRequestBuffer(builder, root)
	return builder.FinishedBytes()
}

// uuidToHiLo splits a 16-byte UUID into the two big-endian uint64
// halves the common.UUID struct uses.
func uuidToHiLo(u uuid.UUID) (uint64, uint64) {
	var hi, lo uint64
	for i := 0; i < 8; i++ {
		hi = (hi << 8) | uint64(u[i])
		lo = (lo << 8) | uint64(u[i+8])
	}
	return hi, lo
}
