package domain_test

import (
	"cmp"
	"fmt"
	"slices"
	"testing"
	"time"

	"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
	"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
	"github.com/brianvoe/gofakeit/v7"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

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

	server := domaintest.NewServer(t)

	active := domain.BaseTribes{
		domaintest.NewBaseTribe(t),
		domaintest.NewBaseTribe(t),
		domaintest.NewBaseTribe(t),
	}

	tribes := domain.Tribes{
		domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
			cfg.ID = active[0].ID()
			cfg.ServerKey = server.Key()
		}),
		domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
			cfg.ServerKey = server.Key()
		}),
		domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
			cfg.ServerKey = server.Key()
			cfg.DeletedAt = time.Now()
		}),
		domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
			cfg.ID = active[1].ID()
			cfg.ServerKey = server.Key()
		}),
		domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
			cfg.ID = active[2].ID()
			cfg.ServerKey = domaintest.RandServerKey()
		}),
	}

	expectedIDs := []int{tribes[1].ID()}

	slices.Sort(expectedIDs)

	slices.SortFunc(active, func(a, b domain.BaseTribe) int {
		return cmp.Compare(a.ID(), b.ID())
	})

	slices.SortFunc(tribes, func(a, b domain.Tribe) int {
		return cmp.Or(
			cmp.Compare(a.ServerKey(), b.ServerKey()),
			cmp.Compare(a.ID(), b.ID()),
		)
	})

	assert.Equal(t, expectedIDs, tribes.Delete(server.Key(), active))
}

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

	now := time.Now()

	server := domaintest.NewServer(t)

	tribes := domain.BaseTribes{
		domaintest.NewBaseTribe(t),
		domaintest.NewBaseTribe(t),
		domaintest.NewBaseTribe(t),
	}

	storedTribes := domain.Tribes{
		domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
			cfg.ID = tribes[0].ID()
			cfg.ServerKey = domaintest.RandServerKey()
		}),
		domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
			cfg.ID = tribes[0].ID()
			cfg.ServerKey = server.Key()
			cfg.BestRank = tribes[0].Rank() + 1
			cfg.BestRankAt = now.Add(-time.Hour)
			cfg.MostPoints = tribes[0].AllPoints() - 1
			cfg.MostPointsAt = now.Add(-time.Hour)
			cfg.MostVillages = tribes[0].NumVillages() - 1
			cfg.MostVillagesAt = now.Add(-time.Hour)
		}),
		domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
			cfg.ID = tribes[1].ID()
			cfg.ServerKey = server.Key()
			cfg.BestRank = tribes[1].Rank() - 1
			cfg.BestRankAt = now.Add(-time.Hour)
			cfg.MostPoints = tribes[1].AllPoints() + 1
			cfg.MostPointsAt = now.Add(-time.Hour)
			cfg.MostVillages = tribes[1].NumVillages() + 1
			cfg.MostVillagesAt = now.Add(-time.Hour)
		}),
	}

	expectedParams := []struct {
		base           domain.BaseTribe
		serverKey      string
		bestRank       int
		bestRankAt     time.Time
		mostPoints     int
		mostPointsAt   time.Time
		mostVillages   int
		mostVillagesAt time.Time
	}{
		{
			base:           tribes[0],
			serverKey:      server.Key(),
			bestRank:       tribes[0].Rank(),
			bestRankAt:     now,
			mostPoints:     tribes[0].AllPoints(),
			mostPointsAt:   now,
			mostVillages:   tribes[0].NumVillages(),
			mostVillagesAt: now,
		},
		{
			base:           tribes[1],
			serverKey:      server.Key(),
			bestRank:       storedTribes[2].BestRank(),
			bestRankAt:     storedTribes[2].BestRankAt(),
			mostPoints:     storedTribes[2].MostPoints(),
			mostPointsAt:   storedTribes[2].MostPointsAt(),
			mostVillages:   storedTribes[2].MostVillages(),
			mostVillagesAt: storedTribes[2].MostVillagesAt(),
		},
		{
			base:           tribes[2],
			serverKey:      server.Key(),
			bestRank:       tribes[2].Rank(),
			bestRankAt:     now,
			mostPoints:     tribes[2].AllPoints(),
			mostPointsAt:   now,
			mostVillages:   tribes[2].NumVillages(),
			mostVillagesAt: now,
		},
	}

	slices.SortFunc(tribes, func(a, b domain.BaseTribe) int {
		return cmp.Compare(a.ID(), b.ID())
	})

	slices.SortFunc(storedTribes, func(a, b domain.Tribe) int {
		return cmp.Or(
			cmp.Compare(a.ServerKey(), b.ServerKey()),
			cmp.Compare(a.ID(), b.ID()),
		)
	})

	res, err := domain.NewCreateTribeParams(server.Key(), tribes, storedTribes)
	require.NoError(t, err)
	assert.Len(t, res, len(expectedParams))
	for i, expected := range expectedParams {
		idx := slices.IndexFunc(res, func(params domain.CreateTribeParams) bool {
			return params.Base().ID() == expected.base.ID() && params.ServerKey() == expected.serverKey
		})
		require.GreaterOrEqualf(t, idx, 0, "expectedParams[%d] not found", i)

		params := res[idx]

		assert.Equalf(t, expected.base, params.Base(), "expectedParams[%d]", i)
		assert.Equalf(t, expected.serverKey, params.ServerKey(), "expectedParams[%d]", i)
		assert.Equalf(t, expected.bestRank, params.BestRank(), "expectedParams[%d]", i)
		assert.WithinDurationf(t, expected.bestRankAt, params.BestRankAt(), time.Minute, "expectedParams[%d]", i)
		assert.Equalf(t, expected.mostPoints, params.MostPoints(), "expectedParams[%d]", i)
		assert.WithinDurationf(t, expected.mostPointsAt, params.MostPointsAt(), time.Minute, "expectedParams[%d]", i)
		assert.Equalf(t, expected.mostVillages, params.MostVillages(), "expectedParams[%d]", i)
		assert.WithinDurationf(t, expected.mostVillagesAt, params.MostVillagesAt(), time.Minute, "expectedParams[%d]", i)
	}
}

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

	type args struct {
		sorts [2]domain.TribeSort
	}

	tests := []struct {
		name        string
		args        args
		expectedRes bool
	}{
		{
			name: "OK: id:ASC serverKey:ASC",
			args: args{
				sorts: [2]domain.TribeSort{domain.TribeSortIDASC, domain.TribeSortServerKeyASC},
			},
			expectedRes: false,
		},
		{
			name: "OK: id:DESC serverKey:ASC",
			args: args{
				sorts: [2]domain.TribeSort{domain.TribeSortIDDESC, domain.TribeSortServerKeyASC},
			},
			expectedRes: false,
		},
		{
			name: "OK: id:ASC id:ASC",
			args: args{
				sorts: [2]domain.TribeSort{domain.TribeSortIDASC, domain.TribeSortIDASC},
			},
			expectedRes: true,
		},
		{
			name: "OK: id:ASC id:DESC",
			args: args{
				sorts: [2]domain.TribeSort{domain.TribeSortIDASC, domain.TribeSortIDDESC},
			},
			expectedRes: true,
		},
		{
			name: "OK: serverKey:DESC serverKey:ASC",
			args: args{
				sorts: [2]domain.TribeSort{domain.TribeSortServerKeyDESC, domain.TribeSortServerKeyASC},
			},
			expectedRes: true,
		},
		{
			name: "OK: odScoreAtt:ASC odScoreAtt:DESC",
			args: args{
				sorts: [2]domain.TribeSort{domain.TribeSortODScoreAttASC, domain.TribeSortODScoreAttDESC},
			},
			expectedRes: true,
		},
		{
			name: "OK: odScoreDef:ASC odScoreDef:DESC",
			args: args{
				sorts: [2]domain.TribeSort{domain.TribeSortODScoreDefASC, domain.TribeSortODScoreDefDESC},
			},
			expectedRes: true,
		},
		{
			name: "OK: odScoreTotal:ASC odScoreTotal:DESC",
			args: args{
				sorts: [2]domain.TribeSort{domain.TribeSortODScoreTotalASC, domain.TribeSortODScoreTotalDESC},
			},
			expectedRes: true,
		},
		{
			name: "OK: points:ASC points:DESC",
			args: args{
				sorts: [2]domain.TribeSort{domain.TribeSortPointsASC, domain.TribeSortPointsDESC},
			},
			expectedRes: true,
		},
		{
			name: "OK: dominance:ASC dominance:DESC",
			args: args{
				sorts: [2]domain.TribeSort{domain.TribeSortDominanceASC, domain.TribeSortDominanceDESC},
			},
			expectedRes: true,
		},
		{
			name: "OK: deletedAt:ASC deletedAt:DESC",
			args: args{
				sorts: [2]domain.TribeSort{domain.TribeSortDeletedAtASC, domain.TribeSortDeletedAtDESC},
			},
			expectedRes: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			assert.Equal(t, tt.expectedRes, tt.args.sorts[0].IsInConflict(tt.args.sorts[1]))
		})
	}
}

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

	validTribeCursor := domaintest.NewTribeCursor(t)

	type args struct {
		id           int
		serverKey    string
		odScoreAtt   int
		odScoreDef   int
		odScoreTotal int
		points       int
		dominance    float64
		deletedAt    time.Time
	}

	type test struct {
		name        string
		args        args
		expectedErr error
	}

	tests := []test{
		{
			name: "OK",
			args: args{
				id:           validTribeCursor.ID(),
				serverKey:    validTribeCursor.ServerKey(),
				odScoreAtt:   validTribeCursor.ODScoreAtt(),
				odScoreDef:   validTribeCursor.ODScoreDef(),
				odScoreTotal: validTribeCursor.ODScoreTotal(),
				points:       validTribeCursor.Points(),
				dominance:    validTribeCursor.Dominance(),
				deletedAt:    validTribeCursor.DeletedAt(),
			},
			expectedErr: nil,
		},
		{
			name: "ERR: id < 1",
			args: args{
				id:           0,
				serverKey:    validTribeCursor.ServerKey(),
				odScoreAtt:   validTribeCursor.ODScoreAtt(),
				odScoreDef:   validTribeCursor.ODScoreDef(),
				odScoreTotal: validTribeCursor.ODScoreTotal(),
				points:       validTribeCursor.Points(),
				dominance:    validTribeCursor.Dominance(),
				deletedAt:    validTribeCursor.DeletedAt(),
			},
			expectedErr: domain.ValidationError{
				Model: "TribeCursor",
				Field: "id",
				Err: domain.MinGreaterEqualError{
					Min:     1,
					Current: 0,
				},
			},
		},
		{
			name: "ERR: odScoreAtt < 0",
			args: args{
				id:           validTribeCursor.ID(),
				serverKey:    validTribeCursor.ServerKey(),
				odScoreAtt:   -1,
				odScoreDef:   validTribeCursor.ODScoreDef(),
				odScoreTotal: validTribeCursor.ODScoreTotal(),
				points:       validTribeCursor.Points(),
				dominance:    validTribeCursor.Dominance(),
				deletedAt:    validTribeCursor.DeletedAt(),
			},
			expectedErr: domain.ValidationError{
				Model: "TribeCursor",
				Field: "odScoreAtt",
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
		{
			name: "ERR: odScoreDef < 0",
			args: args{
				id:           validTribeCursor.ID(),
				serverKey:    validTribeCursor.ServerKey(),
				odScoreAtt:   validTribeCursor.ODScoreAtt(),
				odScoreDef:   -1,
				odScoreTotal: validTribeCursor.ODScoreTotal(),
				points:       validTribeCursor.Points(),
				dominance:    validTribeCursor.Dominance(),
				deletedAt:    validTribeCursor.DeletedAt(),
			},
			expectedErr: domain.ValidationError{
				Model: "TribeCursor",
				Field: "odScoreDef",
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
		{
			name: "ERR: odScoreTotal < 0",
			args: args{
				id:           validTribeCursor.ID(),
				serverKey:    validTribeCursor.ServerKey(),
				odScoreAtt:   validTribeCursor.ODScoreAtt(),
				odScoreDef:   validTribeCursor.ODScoreDef(),
				odScoreTotal: -1,
				points:       validTribeCursor.Points(),
				dominance:    validTribeCursor.Dominance(),
				deletedAt:    validTribeCursor.DeletedAt(),
			},
			expectedErr: domain.ValidationError{
				Model: "TribeCursor",
				Field: "odScoreTotal",
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
		{
			name: "ERR: points < 0",
			args: args{
				id:           validTribeCursor.ID(),
				serverKey:    validTribeCursor.ServerKey(),
				odScoreAtt:   validTribeCursor.ODScoreAtt(),
				odScoreDef:   validTribeCursor.ODScoreDef(),
				odScoreTotal: validTribeCursor.ODScoreTotal(),
				points:       -1,
				dominance:    validTribeCursor.Dominance(),
				deletedAt:    validTribeCursor.DeletedAt(),
			},
			expectedErr: domain.ValidationError{
				Model: "TribeCursor",
				Field: "points",
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
	}

	for _, serverKeyTest := range newServerKeyValidationTests() {
		tests = append(tests, test{
			name: serverKeyTest.name,
			args: args{
				id:           validTribeCursor.ID(),
				serverKey:    serverKeyTest.key,
				odScoreAtt:   validTribeCursor.ODScoreAtt(),
				odScoreDef:   validTribeCursor.ODScoreDef(),
				odScoreTotal: validTribeCursor.ODScoreTotal(),
				points:       validTribeCursor.Points(),
				dominance:    validTribeCursor.Dominance(),
				deletedAt:    validTribeCursor.DeletedAt(),
			},
			expectedErr: domain.ValidationError{
				Model: "TribeCursor",
				Field: "serverKey",
				Err:   serverKeyTest.expectedErr,
			},
		})
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			tc, err := domain.NewTribeCursor(
				tt.args.id,
				tt.args.serverKey,
				tt.args.odScoreAtt,
				tt.args.odScoreDef,
				tt.args.odScoreTotal,
				tt.args.points,
				tt.args.dominance,
				tt.args.deletedAt,
			)
			require.ErrorIs(t, err, tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.id, tc.ID())
			assert.Equal(t, tt.args.serverKey, tc.ServerKey())
			assert.Equal(t, tt.args.odScoreAtt, tc.ODScoreAtt())
			assert.Equal(t, tt.args.odScoreDef, tc.ODScoreDef())
			assert.Equal(t, tt.args.odScoreTotal, tc.ODScoreTotal())
			assert.Equal(t, tt.args.points, tc.Points())
			assert.InDelta(t, tt.args.dominance, tc.Dominance(), 0.001)
			assert.Equal(t, tt.args.deletedAt, tc.DeletedAt())
			assert.NotEmpty(t, tc.Encode())
		})
	}
}

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

	type args struct {
		ids []int
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				ids: []int{
					domaintest.RandID(),
					domaintest.RandID(),
					domaintest.RandID(),
				},
			},
		},
		{
			name: "ERR: value < 1",
			args: args{
				ids: []int{
					domaintest.RandID(),
					domaintest.RandID(),
					domaintest.RandID(),
					0,
					domaintest.RandID(),
				},
			},
			expectedErr: domain.SliceElementValidationError{
				Model: "ListTribesParams",
				Field: "ids",
				Index: 3,
				Err: domain.MinGreaterEqualError{
					Min:     1,
					Current: 0,
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			params := domain.NewListTribesParams()

			require.ErrorIs(t, params.SetIDs(tt.args.ids), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.ids, params.IDs())
		})
	}
}

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

	type args struct {
		serverKeys []string
	}

	type test struct {
		name        string
		args        args
		expectedErr error
	}

	tests := []test{
		{
			name: "OK",
			args: args{
				serverKeys: []string{
					domaintest.RandServerKey(),
				},
			},
		},
	}

	for _, serverKeyTest := range newServerKeyValidationTests() {
		tests = append(tests, test{
			name: serverKeyTest.name,
			args: args{
				serverKeys: []string{serverKeyTest.key},
			},
			expectedErr: domain.SliceElementValidationError{
				Model: "ListTribesParams",
				Field: "serverKeys",
				Index: 0,
				Err:   serverKeyTest.expectedErr,
			},
		})
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			params := domain.NewListTribesParams()

			require.ErrorIs(t, params.SetServerKeys(tt.args.serverKeys), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.serverKeys, params.ServerKeys())
		})
	}
}

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

	type args struct {
		tags []string
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				tags: []string{
					domaintest.RandTribeTag(),
					domaintest.RandTribeTag(),
					domaintest.RandTribeTag(),
					domaintest.RandTribeTag(),
					domaintest.RandTribeTag(),
				},
			},
		},
		{
			name: "ERR: len(tags) < 1",
			args: args{
				tags: nil,
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "tags",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     100,
					Current: 0,
				},
			},
		},
		{
			name: "ERR: len(tags) > 100",
			args: args{
				tags: func() []string {
					tags := make([]string, 101)
					for i := range tags {
						tags[i] = domaintest.RandTribeTag()
					}
					return tags
				}(),
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "tags",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     100,
					Current: 101,
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			params := domain.NewListTribesParams()

			require.ErrorIs(t, params.SetTags(tt.args.tags), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.tags, params.Tags())
		})
	}
}

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

	type args struct {
		sort []domain.TribeSort
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				sort: []domain.TribeSort{
					domain.TribeSortPointsASC,
					domain.TribeSortIDASC,
					domain.TribeSortServerKeyASC,
				},
			},
		},
		{
			name: "ERR: len(sort) < 1",
			args: args{
				sort: nil,
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "sort",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     3,
					Current: 0,
				},
			},
		},
		{
			name: "ERR: len(sort) > 3",
			args: args{
				sort: []domain.TribeSort{
					domain.TribeSortDominanceASC,
					domain.TribeSortPointsASC,
					domain.TribeSortIDASC,
					domain.TribeSortServerKeyASC,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "sort",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     3,
					Current: 4,
				},
			},
		},
		{
			name: "ERR: conflict",
			args: args{
				sort: []domain.TribeSort{
					domain.TribeSortIDASC,
					domain.TribeSortIDDESC,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "sort",
				Err: domain.SortConflictError{
					Sort: [2]string{domain.TribeSortIDASC.String(), domain.TribeSortIDDESC.String()},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			params := domain.NewListTribesParams()

			require.ErrorIs(t, params.SetSort(tt.args.sort), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.sort, params.Sort())
		})
	}
}

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

	defaultNewParams := func(t *testing.T) domain.ListTribesParams {
		t.Helper()
		return domain.ListTribesParams{}
	}

	type args struct {
		sort []string
	}

	tests := []struct {
		name         string
		newParams    func(t *testing.T) domain.ListTribesParams
		args         args
		expectedSort []domain.TribeSort
		expectedErr  error
	}{
		{
			name: "OK: [odScoreAtt:ASC, odScoreDef:ASC, odScoreTotal:ASC]",
			args: args{
				sort: []string{
					"odScoreAtt:ASC",
					"odScoreDef:ASC",
					"odScoreTotal:ASC",
				},
			},
			expectedSort: []domain.TribeSort{
				domain.TribeSortODScoreAttASC,
				domain.TribeSortODScoreDefASC,
				domain.TribeSortODScoreTotalASC,
			},
		},
		{
			name: "OK: [odScoreAtt:DESC, odScoreDef:DESC, odScoreTotal:DESC]",
			args: args{
				sort: []string{
					"odScoreAtt:DESC",
					"odScoreDef:DESC",
					"odScoreTotal:DESC",
				},
			},
			expectedSort: []domain.TribeSort{
				domain.TribeSortODScoreAttDESC,
				domain.TribeSortODScoreDefDESC,
				domain.TribeSortODScoreTotalDESC,
			},
		},
		{
			name: "OK: [points:ASC, dominance:ASC, deletedAt:ASC]",
			args: args{
				sort: []string{
					"points:ASC",
					"dominance:ASC",
					"deletedAt:ASC",
				},
			},
			expectedSort: []domain.TribeSort{
				domain.TribeSortPointsASC,
				domain.TribeSortDominanceASC,
				domain.TribeSortDeletedAtASC,
			},
		},
		{
			name: "OK: [points:DESC, dominance:DESC, deletedAt:DESC]",
			args: args{
				sort: []string{
					"points:DESC",
					"dominance:DESC",
					"deletedAt:DESC",
				},
			},
			expectedSort: []domain.TribeSort{
				domain.TribeSortPointsDESC,
				domain.TribeSortDominanceDESC,
				domain.TribeSortDeletedAtDESC,
			},
		},
		{
			name: "ERR: len(sort) < 1",
			args: args{
				sort: nil,
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "sort",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     3,
					Current: 0,
				},
			},
		},
		{
			name: "ERR: len(sort) > 3",
			args: args{
				sort: []string{
					"odScoreAtt:ASC",
					"odScoreDef:ASC",
					"odScoreTotal:ASC",
					"points:ASC",
				},
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "sort",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     3,
					Current: 4,
				},
			},
		},
		{
			name: "ERR: custom params + len(sort) > 2",
			newParams: func(t *testing.T) domain.ListTribesParams {
				t.Helper()
				params := domain.NewListTribesParams()
				require.NoError(t, params.SetSort([]domain.TribeSort{domain.TribeSortIDASC}))
				return params
			},
			args: args{
				sort: []string{
					"odScoreAtt:ASC",
					"odScoreDef:ASC",
					"odScoreTotal:ASC",
				},
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "sort",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     2,
					Current: 3,
				},
			},
		},
		{
			name:      "ERR: unsupported sort string",
			newParams: defaultNewParams,
			args: args{
				sort: []string{
					"odScoreAtt:ASC",
					"odScoreDef:ASC",
					"odScoreTotal:",
				},
			},
			expectedErr: domain.SliceElementValidationError{
				Model: "ListTribesParams",
				Field: "sort",
				Index: 2,
				Err: domain.UnsupportedSortStringError{
					Sort: "odScoreTotal:",
				},
			},
		},
		{
			name: "ERR: conflict",
			args: args{
				sort: []string{
					"odScoreAtt:ASC",
					"odScoreAtt:DESC",
				},
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "sort",
				Err: domain.SortConflictError{
					Sort: [2]string{domain.TribeSortODScoreAttASC.String(), domain.TribeSortODScoreAttDESC.String()},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			newParams := defaultNewParams
			if tt.newParams != nil {
				newParams = tt.newParams
			}
			params := newParams(t)

			require.ErrorIs(t, params.PrependSortString(tt.args.sort), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.expectedSort, params.Sort())
		})
	}
}

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

	validCursor := domaintest.NewTribeCursor(t)

	type args struct {
		cursor string
	}

	tests := []struct {
		name           string
		args           args
		expectedCursor domain.TribeCursor
		expectedErr    error
	}{
		{
			name: "OK",
			args: args{
				cursor: validCursor.Encode(),
			},
			expectedCursor: validCursor,
		},
		{
			name: "ERR: len(cursor) < 1",
			args: args{
				cursor: "",
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "cursor",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     1000,
					Current: 0,
				},
			},
		},
		{
			name: "ERR: len(cursor) > 1000",
			args: args{
				cursor: gofakeit.LetterN(1001),
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "cursor",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     1000,
					Current: 1001,
				},
			},
		},
		{
			name: "ERR: malformed base64",
			args: args{
				cursor: "112345",
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "cursor",
				Err:   domain.ErrInvalidCursor,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			params := domain.NewListTribesParams()

			require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.expectedCursor, params.Cursor())
			assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
		})
	}
}

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

	type args struct {
		limit int
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				limit: domain.TribeListMaxLimit,
			},
		},
		{
			name: "ERR: limit < 1",
			args: args{
				limit: 0,
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "limit",
				Err: domain.MinGreaterEqualError{
					Min:     1,
					Current: 0,
				},
			},
		},
		{
			name: fmt.Sprintf("ERR: limit > %d", domain.TribeListMaxLimit),
			args: args{
				limit: domain.TribeListMaxLimit + 1,
			},
			expectedErr: domain.ValidationError{
				Model: "ListTribesParams",
				Field: "limit",
				Err: domain.MaxLessEqualError{
					Max:     domain.TribeListMaxLimit,
					Current: domain.TribeListMaxLimit + 1,
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			params := domain.NewListTribesParams()

			require.ErrorIs(t, params.SetLimit(tt.args.limit), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.limit, params.Limit())
		})
	}
}

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

	tribes := domain.Tribes{
		domaintest.NewTribe(t),
		domaintest.NewTribe(t),
		domaintest.NewTribe(t),
	}
	next := domaintest.NewTribe(t)

	t.Run("OK: with next", func(t *testing.T) {
		t.Parallel()

		res, err := domain.NewListTribesResult(tribes, next)
		require.NoError(t, err)
		assert.Equal(t, tribes, res.Tribes())
		assert.Equal(t, tribes[0].ID(), res.Self().ID())
		assert.Equal(t, tribes[0].ServerKey(), res.Self().ServerKey())
		assert.Equal(t, next.ID(), res.Next().ID())
		assert.Equal(t, next.ServerKey(), res.Next().ServerKey())
	})

	t.Run("OK: without next", func(t *testing.T) {
		t.Parallel()

		res, err := domain.NewListTribesResult(tribes, domain.Tribe{})
		require.NoError(t, err)
		assert.Equal(t, tribes, res.Tribes())
		assert.Equal(t, tribes[0].ID(), res.Self().ID())
		assert.Equal(t, tribes[0].ServerKey(), res.Self().ServerKey())
		assert.True(t, res.Next().IsZero())
	})

	t.Run("OK: 0 tribes", func(t *testing.T) {
		t.Parallel()

		res, err := domain.NewListTribesResult(nil, domain.Tribe{})
		require.NoError(t, err)
		assert.Zero(t, res.Tribes())
		assert.True(t, res.Self().IsZero())
		assert.True(t, res.Next().IsZero())
	})
}
