package transcoder

import (
	"reflect"
	"strconv"
	"strings"
	"testing"
	"time"

	usermodel "galaxy/model/user"
	userfbs "galaxy/schema/fbs/user"

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

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

	getPayload, err := GetMyAccountRequestToPayload(&usermodel.GetMyAccountRequest{})
	if err != nil {
		t.Fatalf("encode get my account request: %v", err)
	}

	getDecoded, err := PayloadToGetMyAccountRequest(getPayload)
	if err != nil {
		t.Fatalf("decode get my account request: %v", err)
	}
	if !reflect.DeepEqual(&usermodel.GetMyAccountRequest{}, getDecoded) {
		t.Fatalf("get my account request mismatch: %#v", getDecoded)
	}

	profileSource := &usermodel.UpdateMyProfileRequest{DisplayName: "NovaPrime"}
	profilePayload, err := UpdateMyProfileRequestToPayload(profileSource)
	if err != nil {
		t.Fatalf("encode update my profile request: %v", err)
	}

	profileDecoded, err := PayloadToUpdateMyProfileRequest(profilePayload)
	if err != nil {
		t.Fatalf("decode update my profile request: %v", err)
	}
	if !reflect.DeepEqual(profileSource, profileDecoded) {
		t.Fatalf("update my profile request mismatch\nsource: %#v\ndecoded:%#v", profileSource, profileDecoded)
	}

	settingsSource := &usermodel.UpdateMySettingsRequest{
		PreferredLanguage: "en-US",
		TimeZone:          "Europe/Kaliningrad",
	}
	settingsPayload, err := UpdateMySettingsRequestToPayload(settingsSource)
	if err != nil {
		t.Fatalf("encode update my settings request: %v", err)
	}

	settingsDecoded, err := PayloadToUpdateMySettingsRequest(settingsPayload)
	if err != nil {
		t.Fatalf("decode update my settings request: %v", err)
	}
	if !reflect.DeepEqual(settingsSource, settingsDecoded) {
		t.Fatalf("update my settings request mismatch\nsource: %#v\ndecoded:%#v", settingsSource, settingsDecoded)
	}
}

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

	emptyList, err := ListMySessionsRequestToPayload(&usermodel.ListMySessionsRequest{})
	if err != nil {
		t.Fatalf("encode list-my-sessions request: %v", err)
	}
	if _, err := PayloadToListMySessionsRequest(emptyList); err != nil {
		t.Fatalf("decode list-my-sessions request: %v", err)
	}

	revokeAll, err := RevokeAllMySessionsRequestToPayload(&usermodel.RevokeAllMySessionsRequest{})
	if err != nil {
		t.Fatalf("encode revoke-all-my-sessions request: %v", err)
	}
	if _, err := PayloadToRevokeAllMySessionsRequest(revokeAll); err != nil {
		t.Fatalf("decode revoke-all-my-sessions request: %v", err)
	}

	revokeReq := &usermodel.RevokeMySessionRequest{DeviceSessionID: "device-7c8f"}
	revokePayload, err := RevokeMySessionRequestToPayload(revokeReq)
	if err != nil {
		t.Fatalf("encode revoke-my-session request: %v", err)
	}
	revokeDecoded, err := PayloadToRevokeMySessionRequest(revokePayload)
	if err != nil {
		t.Fatalf("decode revoke-my-session request: %v", err)
	}
	if !reflect.DeepEqual(revokeReq, revokeDecoded) {
		t.Fatalf("revoke-my-session request mismatch\nsource: %#v\ndecoded:%#v", revokeReq, revokeDecoded)
	}

	now := time.Date(2026, time.April, 9, 10, 0, 0, 0, time.UTC)
	revokedAt := now.Add(time.Minute)
	lastSeenAt := now.Add(time.Second)

	listResp := &usermodel.ListMySessionsResponse{
		Items: []usermodel.DeviceSession{
			{
				DeviceSessionID: "ds-1",
				UserID:          "user-1",
				Status:          "active",
				ClientPublicKey: "AAAAAAAAAAA=",
				CreatedAt:       now,
				LastSeenAt:      &lastSeenAt,
			},
			{
				DeviceSessionID: "ds-2",
				UserID:          "user-1",
				Status:          "revoked",
				CreatedAt:       now,
				RevokedAt:       &revokedAt,
			},
		},
	}
	listPayload, err := ListMySessionsResponseToPayload(listResp)
	if err != nil {
		t.Fatalf("encode list-my-sessions response: %v", err)
	}
	listDecoded, err := PayloadToListMySessionsResponse(listPayload)
	if err != nil {
		t.Fatalf("decode list-my-sessions response: %v", err)
	}
	if !reflect.DeepEqual(listResp, listDecoded) {
		t.Fatalf("list-my-sessions response mismatch\nsource: %#v\ndecoded:%#v", listResp, listDecoded)
	}

	revokeResp := &usermodel.RevokeMySessionResponse{
		Session: usermodel.DeviceSession{
			DeviceSessionID: "ds-1",
			UserID:          "user-1",
			Status:          "revoked",
			CreatedAt:       now,
			RevokedAt:       &revokedAt,
		},
	}
	revokeRespPayload, err := RevokeMySessionResponseToPayload(revokeResp)
	if err != nil {
		t.Fatalf("encode revoke-my-session response: %v", err)
	}
	revokeRespDecoded, err := PayloadToRevokeMySessionResponse(revokeRespPayload)
	if err != nil {
		t.Fatalf("decode revoke-my-session response: %v", err)
	}
	if !reflect.DeepEqual(revokeResp, revokeRespDecoded) {
		t.Fatalf("revoke-my-session response mismatch\nsource: %#v\ndecoded:%#v", revokeResp, revokeRespDecoded)
	}

	revokeAllResp := &usermodel.RevokeAllMySessionsResponse{
		Summary: usermodel.DeviceSessionRevocationSummary{
			UserID:       "user-1",
			RevokedCount: 3,
		},
	}
	revokeAllPayload, err := RevokeAllMySessionsResponseToPayload(revokeAllResp)
	if err != nil {
		t.Fatalf("encode revoke-all-my-sessions response: %v", err)
	}
	revokeAllDecoded, err := PayloadToRevokeAllMySessionsResponse(revokeAllPayload)
	if err != nil {
		t.Fatalf("decode revoke-all-my-sessions response: %v", err)
	}
	if !reflect.DeepEqual(revokeAllResp, revokeAllDecoded) {
		t.Fatalf("revoke-all-my-sessions response mismatch\nsource: %#v\ndecoded:%#v", revokeAllResp, revokeAllDecoded)
	}
}

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

	now := time.Date(2026, time.April, 9, 10, 0, 0, 0, time.UTC)
	expiresAt := now.Add(30 * 24 * time.Hour)
	limitExpiresAt := now.Add(90 * 24 * time.Hour)

	source := &usermodel.AccountResponse{
		Account: usermodel.Account{
			UserID:            "user-123",
			Email:             "pilot@example.com",
			UserName:          "player-abcdefgh",
			DisplayName:       "PilotNova",
			PreferredLanguage: "en",
			TimeZone:          "Europe/Kaliningrad",
			DeclaredCountry:   "DE",
			Entitlement: usermodel.EntitlementSnapshot{
				PlanCode:   "paid_monthly",
				IsPaid:     true,
				Source:     "billing",
				Actor:      usermodel.ActorRef{Type: "billing", ID: "invoice-1"},
				ReasonCode: "renewal",
				StartsAt:   now,
				EndsAt:     &expiresAt,
				UpdatedAt:  now,
			},
			ActiveSanctions: []usermodel.ActiveSanction{
				{
					SanctionCode: "profile_update_block",
					Scope:        "lobby",
					ReasonCode:   "manual_block",
					Actor:        usermodel.ActorRef{Type: "admin", ID: "admin-1"},
					AppliedAt:    now,
					ExpiresAt:    &expiresAt,
				},
			},
			ActiveLimits: []usermodel.ActiveLimit{
				{
					LimitCode:  "max_owned_private_games",
					Value:      3,
					ReasonCode: "manual_override",
					Actor:      usermodel.ActorRef{Type: "admin", ID: "admin-1"},
					AppliedAt:  now,
					ExpiresAt:  &limitExpiresAt,
				},
			},
			CreatedAt: now,
			UpdatedAt: now.Add(time.Hour),
		},
	}

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

	decoded, err := PayloadToAccountResponse(payload)
	if err != nil {
		t.Fatalf("decode account response: %v", err)
	}

	if !reflect.DeepEqual(source, decoded) {
		t.Fatalf("account response mismatch\nsource: %#v\ndecoded:%#v", source, decoded)
	}
}

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

	source := &usermodel.ErrorResponse{
		Error: usermodel.ErrorBody{
			Code:    "conflict",
			Message: "request conflicts with current state",
		},
	}

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

	decoded, err := PayloadToErrorResponse(payload)
	if err != nil {
		t.Fatalf("decode error response: %v", err)
	}

	if !reflect.DeepEqual(source, decoded) {
		t.Fatalf("error response mismatch\nsource: %#v\ndecoded:%#v", source, decoded)
	}
}

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

	tests := []struct {
		name string
		call func() error
	}{
		{
			name: "get my account request",
			call: func() error {
				_, err := GetMyAccountRequestToPayload(nil)
				return err
			},
		},
		{
			name: "update my profile request",
			call: func() error {
				_, err := UpdateMyProfileRequestToPayload(nil)
				return err
			},
		},
		{
			name: "update my settings request",
			call: func() error {
				_, err := UpdateMySettingsRequestToPayload(nil)
				return err
			},
		},
		{
			name: "account response",
			call: func() error {
				_, err := AccountResponseToPayload(nil)
				return err
			},
		},
		{
			name: "error response",
			call: func() error {
				_, err := ErrorResponseToPayload(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 TestUserPayloadDecodersRejectEmptyPayloads(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
		call func() error
	}{
		{
			name: "get my account request",
			call: func() error {
				_, err := PayloadToGetMyAccountRequest(nil)
				return err
			},
		},
		{
			name: "update my profile request",
			call: func() error {
				_, err := PayloadToUpdateMyProfileRequest(nil)
				return err
			},
		},
		{
			name: "update my settings request",
			call: func() error {
				_, err := PayloadToUpdateMySettingsRequest(nil)
				return err
			},
		},
		{
			name: "account response",
			call: func() error {
				_, err := PayloadToAccountResponse(nil)
				return err
			},
		},
		{
			name: "error response",
			call: func() error {
				_, err := PayloadToErrorResponse(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 TestUserPayloadDecodersRecoverFromGarbagePayloads(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
		call func() error
	}{
		{
			name: "get my account request",
			call: func() error {
				_, err := PayloadToGetMyAccountRequest([]byte{0x01, 0x02, 0x03})
				return err
			},
		},
		{
			name: "update my profile request",
			call: func() error {
				_, err := PayloadToUpdateMyProfileRequest([]byte{0x01, 0x02, 0x03})
				return err
			},
		},
		{
			name: "update my settings request",
			call: func() error {
				_, err := PayloadToUpdateMySettingsRequest([]byte{0x01, 0x02, 0x03})
				return err
			},
		},
		{
			name: "account response",
			call: func() error {
				_, err := PayloadToAccountResponse([]byte{0x01, 0x02, 0x03})
				return err
			},
		},
		{
			name: "error response",
			call: func() error {
				_, err := PayloadToErrorResponse([]byte{0x01, 0x02, 0x03})
				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 TestPayloadToAccountResponseRejectsMissingAccount(t *testing.T) {
	t.Parallel()

	builder := flatbuffers.NewBuilder(64)
	userfbs.AccountResponseStart(builder)
	offset := userfbs.AccountResponseEnd(builder)
	userfbs.FinishAccountResponseBuffer(builder, offset)

	_, err := PayloadToAccountResponse(builder.FinishedBytes())
	if err == nil {
		t.Fatal("expected error for missing account")
	}
	if !strings.Contains(err.Error(), "account is missing") {
		t.Fatalf("unexpected error: %v", err)
	}
}

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

	payload := buildAccountResponsePayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
		userID := builder.CreateString("user-123")
		email := builder.CreateString("pilot@example.com")
		userName := builder.CreateString("player-abcdefgh")
		preferredLanguage := builder.CreateString("en")
		timeZone := builder.CreateString("Europe/Kaliningrad")

		userfbs.AccountViewStart(builder)
		userfbs.AccountViewAddUserId(builder, userID)
		userfbs.AccountViewAddEmail(builder, email)
		userfbs.AccountViewAddUserName(builder, userName)
		userfbs.AccountViewAddPreferredLanguage(builder, preferredLanguage)
		userfbs.AccountViewAddTimeZone(builder, timeZone)
		userfbs.AccountViewAddCreatedAtMs(builder, 1)
		userfbs.AccountViewAddUpdatedAtMs(builder, 2)
		return userfbs.AccountViewEnd(builder)
	})

	_, err := PayloadToAccountResponse(payload)
	if err == nil {
		t.Fatal("expected error for missing entitlement")
	}
	if !strings.Contains(err.Error(), "entitlement is missing") {
		t.Fatalf("unexpected error: %v", err)
	}
}

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

	if strconv.IntSize == 64 {
		t.Skip("int overflow from int64 is not possible on 64-bit runtime")
	}

	maxInt := int64(int(^uint(0) >> 1))
	overflow := maxInt + 1
	nowMS := int64(1)

	payload := buildAccountResponsePayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
		actorType := builder.CreateString("admin")
		userfbs.ActorRefStart(builder)
		userfbs.ActorRefAddType(builder, actorType)
		actorOffset := userfbs.ActorRefEnd(builder)

		planCode := builder.CreateString("free")
		source := builder.CreateString("auth_registration")
		reasonCode := builder.CreateString("initial_free_entitlement")
		userfbs.EntitlementSnapshotStart(builder)
		userfbs.EntitlementSnapshotAddPlanCode(builder, planCode)
		userfbs.EntitlementSnapshotAddSource(builder, source)
		userfbs.EntitlementSnapshotAddActor(builder, actorOffset)
		userfbs.EntitlementSnapshotAddReasonCode(builder, reasonCode)
		userfbs.EntitlementSnapshotAddStartsAtMs(builder, nowMS)
		userfbs.EntitlementSnapshotAddUpdatedAtMs(builder, nowMS)
		entitlementOffset := userfbs.EntitlementSnapshotEnd(builder)

		limitCode := builder.CreateString("max_owned_private_games")
		limitReasonCode := builder.CreateString("manual_override")
		userfbs.ActiveLimitStart(builder)
		userfbs.ActiveLimitAddLimitCode(builder, limitCode)
		userfbs.ActiveLimitAddValue(builder, overflow)
		userfbs.ActiveLimitAddReasonCode(builder, limitReasonCode)
		userfbs.ActiveLimitAddActor(builder, actorOffset)
		userfbs.ActiveLimitAddAppliedAtMs(builder, nowMS)
		limitOffset := userfbs.ActiveLimitEnd(builder)

		userfbs.AccountViewStartActiveLimitsVector(builder, 1)
		builder.PrependUOffsetT(limitOffset)
		limitsVector := builder.EndVector(1)

		userID := builder.CreateString("user-123")
		email := builder.CreateString("pilot@example.com")
		userName := builder.CreateString("player-abcdefgh")
		preferredLanguage := builder.CreateString("en")
		timeZone := builder.CreateString("Europe/Kaliningrad")

		userfbs.AccountViewStart(builder)
		userfbs.AccountViewAddUserId(builder, userID)
		userfbs.AccountViewAddEmail(builder, email)
		userfbs.AccountViewAddUserName(builder, userName)
		userfbs.AccountViewAddPreferredLanguage(builder, preferredLanguage)
		userfbs.AccountViewAddTimeZone(builder, timeZone)
		userfbs.AccountViewAddEntitlement(builder, entitlementOffset)
		userfbs.AccountViewAddActiveLimits(builder, limitsVector)
		userfbs.AccountViewAddCreatedAtMs(builder, nowMS)
		userfbs.AccountViewAddUpdatedAtMs(builder, nowMS)
		return userfbs.AccountViewEnd(builder)
	})

	_, err := PayloadToAccountResponse(payload)
	if err == nil {
		t.Fatal("expected overflow error")
	}
	if !strings.Contains(err.Error(), "overflows int") {
		t.Fatalf("unexpected error: %v", err)
	}
}

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

	builder := flatbuffers.NewBuilder(64)
	userfbs.ErrorResponseStart(builder)
	offset := userfbs.ErrorResponseEnd(builder)
	userfbs.FinishErrorResponseBuffer(builder, offset)

	_, err := PayloadToErrorResponse(builder.FinishedBytes())
	if err == nil {
		t.Fatal("expected error for missing error body")
	}
	if !strings.Contains(err.Error(), "error is missing") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func buildAccountResponsePayload(accountBuilder func(*flatbuffers.Builder) flatbuffers.UOffsetT) []byte {
	builder := flatbuffers.NewBuilder(256)

	accountOffset := accountBuilder(builder)

	userfbs.AccountResponseStart(builder)
	userfbs.AccountResponseAddAccount(builder, accountOffset)
	responseOffset := userfbs.AccountResponseEnd(builder)
	userfbs.FinishAccountResponseBuffer(builder, responseOffset)

	return builder.FinishedBytes()
}
