package domain_test

import (
	"fmt"
	"math"
	"slices"
	"testing"

	"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 TestServers_Close(t *testing.T) {
	t.Parallel()

	open := domain.BaseServers{
		domaintest.NewBaseServer(t),
		domaintest.NewBaseServer(t),
		domaintest.NewBaseServer(t),
	}

	servers := domain.Servers{
		domaintest.NewServer(t, func(cfg *domaintest.ServerConfig) {
			cfg.Key = open[0].Key()
		}),
		domaintest.NewServer(t, func(cfg *domaintest.ServerConfig) {
			cfg.Key = open[2].Key()
		}),
		domaintest.NewServer(t),
		domaintest.NewServer(t, func(cfg *domaintest.ServerConfig) {
			cfg.Open = false
		}),
	}

	res, err := servers.Close(open)
	require.NoError(t, err)
	assert.NotEmpty(t, res)
	for _, s := range servers {
		expected := domaintest.NewBaseServer(t, func(cfg *domaintest.BaseServerConfig) {
			cfg.Key = s.Key()
			cfg.URL = s.URL()
			cfg.Open = false
		})

		if !s.Open() || slices.ContainsFunc(open, func(server domain.BaseServer) bool {
			return server.Key() == s.Key()
		}) {
			assert.NotContains(t, res, expected)
			continue
		}

		assert.Contains(t, res, expected)
	}
}

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

	validVersion := domaintest.NewVersion(t)
	validBaseServer := domaintest.NewBaseServer(t)

	type args struct {
		servers     domain.BaseServers
		versionCode string
	}

	type test struct {
		name        string
		args        args
		expectedErr error
	}

	tests := []test{
		{
			name: "OK",
			args: args{
				versionCode: validVersion.Code(),
				servers: domain.BaseServers{
					validBaseServer,
				},
			},
		},
	}

	for _, versionCodeTest := range newVersionCodeValidationTests() {
		tests = append(tests, test{
			name: versionCodeTest.name,
			args: args{
				versionCode: versionCodeTest.code,
				servers: domain.BaseServers{
					validBaseServer,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "CreateServerParams",
				Field: "versionCode",
				Err:   versionCodeTest.expectedErr,
			},
		})
	}

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

			res, err := domain.NewCreateServerParams(tt.args.servers, tt.args.versionCode)
			require.ErrorIs(t, err, tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			require.Len(t, res, len(tt.args.servers))
			for i, b := range tt.args.servers {
				assert.Equal(t, b, res[i].Base())
				assert.Equal(t, tt.args.versionCode, res[i].VersionCode())
			}
		})
	}
}

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

	type args struct {
		numTribes domain.NullInt
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				numTribes: domain.NullInt{
					V:     gofakeit.IntRange(0, math.MaxInt),
					Valid: true,
				},
			},
		},
		{
			name: "OK: null value",
			args: args{
				numTribes: domain.NullInt{
					Valid: false,
				},
			},
		},
		{
			name: "ERR: numTribes < 0",
			args: args{
				numTribes: domain.NullInt{
					V:     -1,
					Valid: true,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "UpdateServerParams",
				Field: "numTribes",
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
	}

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

			var params domain.UpdateServerParams

			require.ErrorIs(t, params.SetNumTribes(tt.args.numTribes), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.numTribes, params.NumTribes())
		})
	}
}

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

	type args struct {
		numPlayers domain.NullInt
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				numPlayers: domain.NullInt{
					V:     gofakeit.IntRange(0, math.MaxInt),
					Valid: true,
				},
			},
		},
		{
			name: "OK: null value",
			args: args{
				numPlayers: domain.NullInt{
					Valid: false,
				},
			},
		},
		{
			name: "ERR: numPlayers < 0",
			args: args{
				numPlayers: domain.NullInt{
					V:     -1,
					Valid: true,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "UpdateServerParams",
				Field: "numPlayers",
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
	}

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

			var params domain.UpdateServerParams

			require.ErrorIs(t, params.SetNumPlayers(tt.args.numPlayers), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.numPlayers, params.NumPlayers())
		})
	}
}

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

	type args struct {
		numVillages domain.NullInt
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				numVillages: domain.NullInt{
					V:     gofakeit.IntRange(0, math.MaxInt),
					Valid: true,
				},
			},
		},
		{
			name: "OK: null value",
			args: args{
				numVillages: domain.NullInt{
					Valid: false,
				},
			},
		},
		{
			name: "ERR: numVillages < 0",
			args: args{
				numVillages: domain.NullInt{
					V:     -1,
					Valid: true,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "UpdateServerParams",
				Field: "numVillages",
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
	}

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

			var params domain.UpdateServerParams

			require.ErrorIs(t, params.SetNumVillages(tt.args.numVillages), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.numVillages, params.NumVillages())
		})
	}
}

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

	type args struct {
		numPlayerVillages domain.NullInt
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				numPlayerVillages: domain.NullInt{
					V:     gofakeit.IntRange(0, math.MaxInt),
					Valid: true,
				},
			},
		},
		{
			name: "OK: null value",
			args: args{
				numPlayerVillages: domain.NullInt{
					Valid: false,
				},
			},
		},
		{
			name: "ERR: numPlayerVillages < 0",
			args: args{
				numPlayerVillages: domain.NullInt{
					V:     -1,
					Valid: true,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "UpdateServerParams",
				Field: "numPlayerVillages",
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
	}

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

			var params domain.UpdateServerParams

			require.ErrorIs(t, params.SetNumPlayerVillages(tt.args.numPlayerVillages), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.numPlayerVillages, params.NumPlayerVillages())
		})
	}
}

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

	type args struct {
		numBarbarianVillages domain.NullInt
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				numBarbarianVillages: domain.NullInt{
					V:     gofakeit.IntRange(0, math.MaxInt),
					Valid: true,
				},
			},
		},
		{
			name: "OK: null value",
			args: args{
				numBarbarianVillages: domain.NullInt{
					Valid: false,
				},
			},
		},
		{
			name: "ERR: numBarbarianVillages < 0",
			args: args{
				numBarbarianVillages: domain.NullInt{
					V:     -1,
					Valid: true,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "UpdateServerParams",
				Field: "numBarbarianVillages",
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
	}

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

			var params domain.UpdateServerParams

			require.ErrorIs(t, params.SetNumBarbarianVillages(tt.args.numBarbarianVillages), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.numBarbarianVillages, params.NumBarbarianVillages())
		})
	}
}

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

	type args struct {
		numBonusVillages domain.NullInt
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				numBonusVillages: domain.NullInt{
					V:     gofakeit.IntRange(0, math.MaxInt),
					Valid: true,
				},
			},
		},
		{
			name: "OK: null value",
			args: args{
				numBonusVillages: domain.NullInt{
					Valid: false,
				},
			},
		},
		{
			name: "ERR: numBonusVillages < 0",
			args: args{
				numBonusVillages: domain.NullInt{
					V:     -1,
					Valid: true,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "UpdateServerParams",
				Field: "numBonusVillages",
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
	}

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

			var params domain.UpdateServerParams

			require.ErrorIs(t, params.SetNumBonusVillages(tt.args.numBonusVillages), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.numBonusVillages, params.NumBonusVillages())
		})
	}
}

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

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

	tests := []struct {
		name        string
		args        args
		expectedRes bool
	}{
		{
			name: "OK: key:ASC open:ASC",
			args: args{
				sorts: [2]domain.ServerSort{domain.ServerSortKeyASC, domain.ServerSortOpenASC},
			},
			expectedRes: false,
		},
		{
			name: "OK: key:ASC open:DESC",
			args: args{
				sorts: [2]domain.ServerSort{domain.ServerSortKeyASC, domain.ServerSortOpenDESC},
			},
			expectedRes: false,
		},
		{
			name: "OK: key:DESC open:ASC",
			args: args{
				sorts: [2]domain.ServerSort{domain.ServerSortKeyDESC, domain.ServerSortOpenASC},
			},
			expectedRes: false,
		},
		{
			name: "OK: key:ASC key:DESC",
			args: args{
				sorts: [2]domain.ServerSort{domain.ServerSortKeyASC, domain.ServerSortKeyDESC},
			},
			expectedRes: true,
		},
		{
			name: "OK: open:DESC open:ASC",
			args: args{
				sorts: [2]domain.ServerSort{domain.ServerSortOpenDESC, domain.ServerSortOpenASC},
			},
			expectedRes: true,
		},
		{
			name: "OK: open:DESC open:DESC",
			args: args{
				sorts: [2]domain.ServerSort{domain.ServerSortOpenDESC, domain.ServerSortOpenDESC},
			},
			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 TestNewServerCursor(t *testing.T) {
	t.Parallel()

	validServerCursor := domaintest.NewServerCursor(t)

	type args struct {
		key  string
		open bool
	}

	type test struct {
		name        string
		args        args
		expectedErr error
	}

	tests := []test{
		{
			name: "OK",
			args: args{
				key:  validServerCursor.Key(),
				open: validServerCursor.Open(),
			},
			expectedErr: nil,
		},
	}

	for _, serverKeyTest := range newServerKeyValidationTests() {
		tests = append(tests, test{
			name: serverKeyTest.name,
			args: args{
				key: serverKeyTest.key,
			},
			expectedErr: domain.ValidationError{
				Model: "ServerCursor",
				Field: "key",
				Err:   serverKeyTest.expectedErr,
			},
		})
	}

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

			sc, err := domain.NewServerCursor(tt.args.key, tt.args.open)
			require.ErrorIs(t, err, tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.key, sc.Key())
			assert.Equal(t, tt.args.open, sc.Open())
			assert.NotEmpty(t, sc.Encode())
		})
	}
}

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

	type args struct {
		sort []domain.ServerSort
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				sort: []domain.ServerSort{
					domain.ServerSortKeyASC,
				},
			},
		},
		{
			name: "ERR: len(sort) < 1",
			args: args{
				sort: nil,
			},
			expectedErr: domain.ValidationError{
				Model: "ListServersParams",
				Field: "sort",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     2,
					Current: 0,
				},
			},
		},
		{
			name: "ERR: len(sort) > 2",
			args: args{
				sort: []domain.ServerSort{
					domain.ServerSortKeyASC,
					domain.ServerSortKeyDESC,
					domain.ServerSortOpenASC,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "ListServersParams",
				Field: "sort",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     2,
					Current: 3,
				},
			},
		},
		{
			name: "ERR: conflict",
			args: args{
				sort: []domain.ServerSort{
					domain.ServerSortKeyASC,
					domain.ServerSortKeyDESC,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "ListServersParams",
				Field: "sort",
				Err: domain.SortConflictError{
					Sort: [2]string{domain.ServerSortKeyASC.String(), domain.ServerSortKeyDESC.String()},
				},
			},
		},
	}

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

			params := domain.NewListServersParams()

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

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

	validCursor := domaintest.NewServerCursor(t)

	type args struct {
		cursor string
	}

	tests := []struct {
		name           string
		args           args
		expectedCursor domain.ServerCursor
		expectedErr    error
	}{
		{
			name: "OK",
			args: args{
				cursor: validCursor.Encode(),
			},
			expectedCursor: validCursor,
		},
		{
			name: "ERR: len(cursor) < 1",
			args: args{
				cursor: "",
			},
			expectedErr: domain.ValidationError{
				Model: "ListServersParams",
				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: "ListServersParams",
				Field: "cursor",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     1000,
					Current: 1001,
				},
			},
		},
		{
			name: "ERR: malformed base64",
			args: args{
				cursor: "112345",
			},
			expectedErr: domain.ValidationError{
				Model: "ListServersParams",
				Field: "cursor",
				Err:   domain.ErrInvalidCursor,
			},
		},
	}

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

			params := domain.NewListServersParams()

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

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

	type args struct {
		keys []string
	}

	type test struct {
		name        string
		args        args
		expectedErr error
	}

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

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

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

			params := domain.NewListServersParams()

			require.ErrorIs(t, params.SetKeys(tt.args.keys), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.keys, params.Keys())
		})
	}
}

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

	type args struct {
		versionCodes []string
	}

	type test struct {
		name        string
		args        args
		expectedErr error
	}

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

	for _, versionCodeTest := range newVersionCodeValidationTests() {
		tests = append(tests, test{
			name: versionCodeTest.name,
			args: args{
				versionCodes: []string{versionCodeTest.code},
			},
			expectedErr: domain.SliceElementValidationError{
				Model: "ListServersParams",
				Field: "versionCodes",
				Index: 0,
				Err:   versionCodeTest.expectedErr,
			},
		})
	}

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

			params := domain.NewListServersParams()

			require.ErrorIs(t, params.SetVersionCodes(tt.args.versionCodes), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.versionCodes, params.VersionCodes())
		})
	}
}

func TestListServersParams_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.ServerListMaxLimit,
			},
		},
		{
			name: "ERR: limit < 1",
			args: args{
				limit: 0,
			},
			expectedErr: domain.ValidationError{
				Model: "ListServersParams",
				Field: "limit",
				Err: domain.MinGreaterEqualError{
					Min:     1,
					Current: 0,
				},
			},
		},
		{
			name: fmt.Sprintf("ERR: limit > %d", domain.ServerListMaxLimit),
			args: args{
				limit: domain.ServerListMaxLimit + 1,
			},
			expectedErr: domain.ValidationError{
				Model: "ListServersParams",
				Field: "limit",
				Err: domain.MaxLessEqualError{
					Max:     domain.ServerListMaxLimit,
					Current: domain.ServerListMaxLimit + 1,
				},
			},
		},
	}

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

			params := domain.NewListServersParams()

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

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

	servers := domain.Servers{
		domaintest.NewServer(t),
		domaintest.NewServer(t),
		domaintest.NewServer(t),
	}
	next := domaintest.NewServer(t)

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

		res, err := domain.NewListServersResult(servers, next)
		require.NoError(t, err)
		assert.Equal(t, servers, res.Servers())
		assert.Equal(t, servers[0].Key(), res.Self().Key())
		assert.Equal(t, servers[0].Open(), res.Self().Open())
		assert.Equal(t, next.Key(), res.Next().Key())
		assert.Equal(t, next.Open(), res.Next().Open())
	})

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

		res, err := domain.NewListServersResult(servers, domain.Server{})
		require.NoError(t, err)
		assert.Equal(t, servers, res.Servers())
		assert.Equal(t, servers[0].Key(), res.Self().Key())
		assert.Equal(t, servers[0].Open(), res.Self().Open())
		assert.True(t, res.Next().IsZero())
	})

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

		res, err := domain.NewListServersResult(nil, domain.Server{})
		require.NoError(t, err)
		assert.Zero(t, res.Servers())
		assert.True(t, res.Self().IsZero())
		assert.True(t, res.Next().IsZero())
	})
}

type serverKeyValidationTest struct {
	name        string
	key         string
	expectedErr error
}

func newServerKeyValidationTests() []serverKeyValidationTest {
	return []serverKeyValidationTest{
		{
			name: "ERR: server key length < 1",
			expectedErr: domain.LenOutOfRangeError{
				Min: 1,
				Max: 10,
			},
		},
		{
			name: "ERR: server key length > 10",
			key:  "keykeykeyke",
			expectedErr: domain.LenOutOfRangeError{
				Min:     1,
				Max:     10,
				Current: len("keykeykeyke"),
			},
		},
	}
}
