package transcoder

import (
	"reflect"
	"testing"
	"time"

	lobbymodel "galaxy/model/lobby"
)

// fixedTimes returns deterministic UTC timestamps for round-trip
// fixtures. Millisecond precision matches the FlatBuffers `*Ms` ints,
// which is what the transcoder preserves.
func fixedTimes() (created, updated, ends time.Time) {
	created = time.Date(2026, time.May, 7, 9, 30, 15, 123_000_000, time.UTC)
	updated = created.Add(2 * time.Minute)
	ends = created.Add(48 * time.Hour)
	return
}

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

	created, updated, ends := fixedTimes()

	source := &lobbymodel.MyGamesListResponse{
		Items: []lobbymodel.GameSummary{
			{
				GameID:           "game-private-7c8f",
				GameName:         "First Contact",
				GameType:         "private",
				Status:           "running",
				OwnerUserID:      "user-9912",
				MinPlayers:       2,
				MaxPlayers:       8,
				EnrollmentEndsAt: ends,
				CreatedAt:        created,
				UpdatedAt:        updated,
				CurrentTurn:      7,
			},
			{
				GameID:           "game-public-aabb",
				GameName:         "Open Lobby",
				GameType:         "public",
				Status:           "enrollment_open",
				OwnerUserID:      "",
				MinPlayers:       4,
				MaxPlayers:       12,
				EnrollmentEndsAt: ends,
				CreatedAt:        created,
				UpdatedAt:        updated,
				CurrentTurn:      0,
			},
		},
	}

	payload, err := MyGamesListResponseToPayload(source)
	if err != nil {
		t.Fatalf("encode: %v", err)
	}

	decoded, err := PayloadToMyGamesListResponse(payload)
	if err != nil {
		t.Fatalf("decode: %v", err)
	}
	if !reflect.DeepEqual(source, decoded) {
		t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded)
	}

	requestBytes, err := MyGamesListRequestToPayload(&lobbymodel.MyGamesListRequest{})
	if err != nil {
		t.Fatalf("encode request: %v", err)
	}
	if _, err := PayloadToMyGamesListRequest(requestBytes); err != nil {
		t.Fatalf("decode request: %v", err)
	}
}

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

	payload, err := MyGamesListResponseToPayload(&lobbymodel.MyGamesListResponse{Items: nil})
	if err != nil {
		t.Fatalf("encode: %v", err)
	}
	decoded, err := PayloadToMyGamesListResponse(payload)
	if err != nil {
		t.Fatalf("decode: %v", err)
	}
	if got := len(decoded.Items); got != 0 {
		t.Fatalf("expected empty items, got %d", got)
	}
}

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

	created, updated, ends := fixedTimes()

	requestSource := &lobbymodel.PublicGamesListRequest{Page: 3, PageSize: 25}
	requestBytes, err := PublicGamesListRequestToPayload(requestSource)
	if err != nil {
		t.Fatalf("encode request: %v", err)
	}
	requestDecoded, err := PayloadToPublicGamesListRequest(requestBytes)
	if err != nil {
		t.Fatalf("decode request: %v", err)
	}
	if !reflect.DeepEqual(requestSource, requestDecoded) {
		t.Fatalf("request round-trip mismatch\nsource: %#v\ndecoded:%#v", requestSource, requestDecoded)
	}

	responseSource := &lobbymodel.PublicGamesListResponse{
		Items: []lobbymodel.GameSummary{
			{
				GameID:           "game-public-aabb",
				GameName:         "Open Lobby",
				GameType:         "public",
				Status:           "enrollment_open",
				OwnerUserID:      "",
				MinPlayers:       4,
				MaxPlayers:       12,
				EnrollmentEndsAt: ends,
				CreatedAt:        created,
				UpdatedAt:        updated,
			},
		},
		Page:     3,
		PageSize: 25,
		Total:    51,
	}
	responseBytes, err := PublicGamesListResponseToPayload(responseSource)
	if err != nil {
		t.Fatalf("encode response: %v", err)
	}
	responseDecoded, err := PayloadToPublicGamesListResponse(responseBytes)
	if err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if !reflect.DeepEqual(responseSource, responseDecoded) {
		t.Fatalf("response round-trip mismatch\nsource: %#v\ndecoded:%#v", responseSource, responseDecoded)
	}
}

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

	created, _, _ := fixedTimes()
	decided := created.Add(2 * time.Hour)

	requestBytes, err := MyApplicationsListRequestToPayload(&lobbymodel.MyApplicationsListRequest{})
	if err != nil {
		t.Fatalf("encode request: %v", err)
	}
	if _, err := PayloadToMyApplicationsListRequest(requestBytes); err != nil {
		t.Fatalf("decode request: %v", err)
	}

	source := &lobbymodel.MyApplicationsListResponse{
		Items: []lobbymodel.ApplicationSummary{
			{
				ApplicationID:   "app-1",
				GameID:          "game-public-aabb",
				ApplicantUserID: "user-9912",
				RaceName:        "Vegan Federation",
				Status:          "pending",
				CreatedAt:       created,
				DecidedAt:       nil,
			},
			{
				ApplicationID:   "app-2",
				GameID:          "game-public-ccdd",
				ApplicantUserID: "user-9912",
				RaceName:        "Lithic Compact",
				Status:          "approved",
				CreatedAt:       created,
				DecidedAt:       &decided,
			},
		},
	}
	payload, err := MyApplicationsListResponseToPayload(source)
	if err != nil {
		t.Fatalf("encode: %v", err)
	}
	decoded, err := PayloadToMyApplicationsListResponse(payload)
	if err != nil {
		t.Fatalf("decode: %v", err)
	}
	if !reflect.DeepEqual(source, decoded) {
		t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded)
	}
}

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

	created, _, _ := fixedTimes()
	expires := created.Add(72 * time.Hour)
	decided := created.Add(1 * time.Hour)

	requestBytes, err := MyInvitesListRequestToPayload(&lobbymodel.MyInvitesListRequest{})
	if err != nil {
		t.Fatalf("encode request: %v", err)
	}
	if _, err := PayloadToMyInvitesListRequest(requestBytes); err != nil {
		t.Fatalf("decode request: %v", err)
	}

	source := &lobbymodel.MyInvitesListResponse{
		Items: []lobbymodel.InviteSummary{
			{
				InviteID:      "invite-user-bound",
				GameID:        "game-private-7c8f",
				InviterUserID: "user-1111",
				InvitedUserID: "user-9912",
				Code:          "",
				RaceName:      "Vegan Federation",
				Status:        "pending",
				CreatedAt:     created,
				ExpiresAt:     expires,
				DecidedAt:     nil,
			},
			{
				InviteID:      "invite-code-based",
				GameID:        "game-private-3322",
				InviterUserID: "user-1111",
				InvitedUserID: "",
				Code:          "ABCDEF12",
				RaceName:      "Lithic Compact",
				Status:        "accepted",
				CreatedAt:     created,
				ExpiresAt:     expires,
				DecidedAt:     &decided,
			},
		},
	}
	payload, err := MyInvitesListResponseToPayload(source)
	if err != nil {
		t.Fatalf("encode: %v", err)
	}
	decoded, err := PayloadToMyInvitesListResponse(payload)
	if err != nil {
		t.Fatalf("decode: %v", err)
	}
	if !reflect.DeepEqual(source, decoded) {
		t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded)
	}
}

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

	requestSource := &lobbymodel.OpenEnrollmentRequest{GameID: "game-private-7c8f"}
	requestBytes, err := OpenEnrollmentRequestToPayload(requestSource)
	if err != nil {
		t.Fatalf("encode request: %v", err)
	}
	requestDecoded, err := PayloadToOpenEnrollmentRequest(requestBytes)
	if err != nil {
		t.Fatalf("decode request: %v", err)
	}
	if !reflect.DeepEqual(requestSource, requestDecoded) {
		t.Fatalf("request round-trip mismatch\nsource: %#v\ndecoded:%#v", requestSource, requestDecoded)
	}

	responseSource := &lobbymodel.OpenEnrollmentResponse{GameID: "game-private-7c8f", Status: "enrollment_open"}
	responseBytes, err := OpenEnrollmentResponseToPayload(responseSource)
	if err != nil {
		t.Fatalf("encode response: %v", err)
	}
	responseDecoded, err := PayloadToOpenEnrollmentResponse(responseBytes)
	if err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if !reflect.DeepEqual(responseSource, responseDecoded) {
		t.Fatalf("response round-trip mismatch\nsource: %#v\ndecoded:%#v", responseSource, responseDecoded)
	}
}

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

	_, _, ends := fixedTimes()

	requestSource := &lobbymodel.GameCreateRequest{
		GameName:            "First Contact",
		Description:         "First Phase 8 sandbox game",
		MinPlayers:          2,
		MaxPlayers:          8,
		StartGapHours:       24,
		StartGapPlayers:     2,
		EnrollmentEndsAt:    ends,
		TurnSchedule:        "0 0 * * *",
		TargetEngineVersion: "v1",
	}
	requestBytes, err := GameCreateRequestToPayload(requestSource)
	if err != nil {
		t.Fatalf("encode request: %v", err)
	}
	requestDecoded, err := PayloadToGameCreateRequest(requestBytes)
	if err != nil {
		t.Fatalf("decode request: %v", err)
	}
	if !reflect.DeepEqual(requestSource, requestDecoded) {
		t.Fatalf("request round-trip mismatch\nsource: %#v\ndecoded:%#v", requestSource, requestDecoded)
	}

	created, updated, _ := fixedTimes()
	responseSource := &lobbymodel.GameCreateResponse{
		Game: lobbymodel.GameSummary{
			GameID:           "game-private-newly-created",
			GameName:         "First Contact",
			GameType:         "private",
			Status:           "draft",
			OwnerUserID:      "user-9912",
			MinPlayers:       2,
			MaxPlayers:       8,
			EnrollmentEndsAt: ends,
			CreatedAt:        created,
			UpdatedAt:        updated,
		},
	}
	responseBytes, err := GameCreateResponseToPayload(responseSource)
	if err != nil {
		t.Fatalf("encode response: %v", err)
	}
	responseDecoded, err := PayloadToGameCreateResponse(responseBytes)
	if err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if !reflect.DeepEqual(responseSource, responseDecoded) {
		t.Fatalf("response round-trip mismatch\nsource: %#v\ndecoded:%#v", responseSource, responseDecoded)
	}
}

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

	requestSource := &lobbymodel.ApplicationSubmitRequest{GameID: "game-public-aabb", RaceName: "Vegan Federation"}
	requestBytes, err := ApplicationSubmitRequestToPayload(requestSource)
	if err != nil {
		t.Fatalf("encode request: %v", err)
	}
	requestDecoded, err := PayloadToApplicationSubmitRequest(requestBytes)
	if err != nil {
		t.Fatalf("decode request: %v", err)
	}
	if !reflect.DeepEqual(requestSource, requestDecoded) {
		t.Fatalf("request round-trip mismatch\nsource: %#v\ndecoded:%#v", requestSource, requestDecoded)
	}

	created, _, _ := fixedTimes()
	responseSource := &lobbymodel.ApplicationSubmitResponse{
		Application: lobbymodel.ApplicationSummary{
			ApplicationID:   "app-3",
			GameID:          "game-public-aabb",
			ApplicantUserID: "user-9912",
			RaceName:        "Vegan Federation",
			Status:          "pending",
			CreatedAt:       created,
			DecidedAt:       nil,
		},
	}
	responseBytes, err := ApplicationSubmitResponseToPayload(responseSource)
	if err != nil {
		t.Fatalf("encode response: %v", err)
	}
	responseDecoded, err := PayloadToApplicationSubmitResponse(responseBytes)
	if err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if !reflect.DeepEqual(responseSource, responseDecoded) {
		t.Fatalf("response round-trip mismatch\nsource: %#v\ndecoded:%#v", responseSource, responseDecoded)
	}
}

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

	created, _, _ := fixedTimes()
	expires := created.Add(72 * time.Hour)
	decided := created.Add(1 * time.Hour)

	redeemReq := &lobbymodel.InviteRedeemRequest{GameID: "game-private-7c8f", InviteID: "invite-user-bound"}
	redeemBytes, err := InviteRedeemRequestToPayload(redeemReq)
	if err != nil {
		t.Fatalf("encode redeem request: %v", err)
	}
	redeemDecoded, err := PayloadToInviteRedeemRequest(redeemBytes)
	if err != nil {
		t.Fatalf("decode redeem request: %v", err)
	}
	if !reflect.DeepEqual(redeemReq, redeemDecoded) {
		t.Fatalf("redeem request mismatch")
	}

	redeemResp := &lobbymodel.InviteRedeemResponse{
		Invite: lobbymodel.InviteSummary{
			InviteID:      "invite-user-bound",
			GameID:        "game-private-7c8f",
			InviterUserID: "user-1111",
			InvitedUserID: "user-9912",
			Code:          "",
			RaceName:      "Vegan Federation",
			Status:        "accepted",
			CreatedAt:     created,
			ExpiresAt:     expires,
			DecidedAt:     &decided,
		},
	}
	redeemRespBytes, err := InviteRedeemResponseToPayload(redeemResp)
	if err != nil {
		t.Fatalf("encode redeem response: %v", err)
	}
	redeemRespDecoded, err := PayloadToInviteRedeemResponse(redeemRespBytes)
	if err != nil {
		t.Fatalf("decode redeem response: %v", err)
	}
	if !reflect.DeepEqual(redeemResp, redeemRespDecoded) {
		t.Fatalf("redeem response mismatch")
	}

	declineReq := &lobbymodel.InviteDeclineRequest{GameID: "game-private-7c8f", InviteID: "invite-user-bound"}
	declineBytes, err := InviteDeclineRequestToPayload(declineReq)
	if err != nil {
		t.Fatalf("encode decline request: %v", err)
	}
	declineDecoded, err := PayloadToInviteDeclineRequest(declineBytes)
	if err != nil {
		t.Fatalf("decode decline request: %v", err)
	}
	if !reflect.DeepEqual(declineReq, declineDecoded) {
		t.Fatalf("decline request mismatch")
	}

	declineResp := &lobbymodel.InviteDeclineResponse{
		Invite: lobbymodel.InviteSummary{
			InviteID:      "invite-user-bound",
			GameID:        "game-private-7c8f",
			InviterUserID: "user-1111",
			InvitedUserID: "user-9912",
			Code:          "",
			RaceName:      "Vegan Federation",
			Status:        "declined",
			CreatedAt:     created,
			ExpiresAt:     expires,
			DecidedAt:     &decided,
		},
	}
	declineRespBytes, err := InviteDeclineResponseToPayload(declineResp)
	if err != nil {
		t.Fatalf("encode decline response: %v", err)
	}
	declineRespDecoded, err := PayloadToInviteDeclineResponse(declineRespBytes)
	if err != nil {
		t.Fatalf("decode decline response: %v", err)
	}
	if !reflect.DeepEqual(declineResp, declineRespDecoded) {
		t.Fatalf("decline response mismatch")
	}
}

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

	source := &lobbymodel.ErrorResponse{
		Error: lobbymodel.ErrorBody{
			Code:    "conflict",
			Message: "request conflicts with current state",
		},
	}
	payload, err := LobbyErrorResponseToPayload(source)
	if err != nil {
		t.Fatalf("encode: %v", err)
	}
	decoded, err := PayloadToLobbyErrorResponse(payload)
	if err != nil {
		t.Fatalf("decode: %v", err)
	}
	if !reflect.DeepEqual(source, decoded) {
		t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded)
	}
}

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

	garbage := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}

	if _, err := PayloadToMyGamesListResponse(garbage); err == nil {
		t.Fatal("expected error decoding corrupt my games payload")
	}
	if _, err := PayloadToPublicGamesListResponse(garbage); err == nil {
		t.Fatal("expected error decoding corrupt public games payload")
	}
	if _, err := PayloadToApplicationSubmitResponse(garbage); err == nil {
		t.Fatal("expected error decoding corrupt application submit response")
	}
	if _, err := PayloadToInviteRedeemResponse(garbage); err == nil {
		t.Fatal("expected error decoding corrupt invite redeem response")
	}
	if _, err := PayloadToLobbyErrorResponse(garbage); err == nil {
		t.Fatal("expected error decoding corrupt error payload")
	}
}

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

	if _, err := PayloadToMyGamesListRequest(nil); err == nil {
		t.Fatal("expected error decoding nil request")
	}
	if _, err := PayloadToPublicGamesListResponse(nil); err == nil {
		t.Fatal("expected error decoding nil response")
	}
	if _, err := PayloadToInviteDeclineRequest(nil); err == nil {
		t.Fatal("expected error decoding nil decline request")
	}
}
