package domain_test

import (
	"cmp"
	"fmt"
	"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 TestVillages_Delete(t *testing.T) {
	t.Parallel()

	server := domaintest.NewServer(t)

	active := domain.BaseVillages{
		domaintest.NewBaseVillage(t),
		domaintest.NewBaseVillage(t),
		domaintest.NewBaseVillage(t),
	}

	villages := domain.Villages{
		domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
			cfg.ID = active[0].ID()
			cfg.ServerKey = server.Key()
		}),
		domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
			cfg.ServerKey = server.Key()
		}),
		domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
			cfg.ID = active[1].ID()
			cfg.ServerKey = server.Key()
		}),
		domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
			cfg.ID = active[2].ID()
			cfg.ServerKey = domaintest.RandServerKey()
		}),
	}

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

	slices.Sort(expectedIDs)

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

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

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

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

	server := domaintest.NewServer(t)

	villages := domain.BaseVillages{
		domaintest.NewBaseVillage(t),
		domaintest.NewBaseVillage(t),
		domaintest.NewBaseVillage(t),
	}
	slices.SortFunc(villages, func(a, b domain.BaseVillage) int {
		return cmp.Compare(a.ID(), b.ID())
	})

	res, err := domain.NewCreateVillageParams(server.Key(), villages)
	require.NoError(t, err)
	assert.Len(t, res, len(villages))
	for i, v := range villages {
		idx := slices.IndexFunc(res, func(params domain.CreateVillageParams) bool {
			return params.Base().ID() == v.ID() && params.ServerKey() == server.Key()
		})
		require.GreaterOrEqualf(t, idx, 0, "villages[%d] not found", i)

		params := res[idx]

		assert.Equalf(t, v, params.Base(), "villages[%d]", i)
	}
}

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

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

	tests := []struct {
		name        string
		args        args
		expectedRes bool
	}{
		{
			name: "OK: id:ASC serverKey:ASC",
			args: args{
				sorts: [2]domain.VillageSort{domain.VillageSortIDASC, domain.VillageSortServerKeyASC},
			},
			expectedRes: false,
		},
		{
			name: "OK: id:DESC serverKey:ASC",
			args: args{
				sorts: [2]domain.VillageSort{domain.VillageSortIDDESC, domain.VillageSortServerKeyASC},
			},
			expectedRes: false,
		},
		{
			name: "OK: id:ASC id:ASC",
			args: args{
				sorts: [2]domain.VillageSort{domain.VillageSortIDASC, domain.VillageSortIDASC},
			},
			expectedRes: true,
		},
		{
			name: "OK: id:ASC id:DESC",
			args: args{
				sorts: [2]domain.VillageSort{domain.VillageSortIDASC, domain.VillageSortIDDESC},
			},
			expectedRes: true,
		},
		{
			name: "OK: serverKey:DESC serverKey:ASC",
			args: args{
				sorts: [2]domain.VillageSort{domain.VillageSortServerKeyDESC, domain.VillageSortServerKeyASC},
			},
			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 TestNewVillageCursor(t *testing.T) {
	t.Parallel()

	validVillageCursor := domaintest.NewVillageCursor(t)

	type args struct {
		id        int
		serverKey string
	}

	type test struct {
		name        string
		args        args
		expectedErr error
	}

	tests := []test{
		{
			name: "OK",
			args: args{
				id:        validVillageCursor.ID(),
				serverKey: validVillageCursor.ServerKey(),
			},
			expectedErr: nil,
		},
		{
			name: "ERR: id < 1",
			args: args{
				id:        0,
				serverKey: validVillageCursor.ServerKey(),
			},
			expectedErr: domain.ValidationError{
				Model: "VillageCursor",
				Field: "id",
				Err: domain.MinGreaterEqualError{
					Min:     1,
					Current: 0,
				},
			},
		},
	}

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

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

			vc, err := domain.NewVillageCursor(
				tt.args.id,
				tt.args.serverKey,
			)
			require.ErrorIs(t, err, tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.id, vc.ID())
			assert.Equal(t, tt.args.serverKey, vc.ServerKey())
			assert.NotEmpty(t, vc.Encode())
		})
	}
}

func TestListVillagesParams_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: "ListVillagesParams",
				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.NewListVillagesParams()

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

func TestListVillagesParams_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: "ListVillagesParams",
				Field: "serverKeys",
				Index: 0,
				Err:   serverKeyTest.expectedErr,
			},
		})
	}

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

			params := domain.NewListVillagesParams()

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

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

	type args struct {
		coords []domain.Coords
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				coords: []domain.Coords{
					domaintest.NewVillage(t).Coords(),
					domaintest.NewVillage(t).Coords(),
					domaintest.NewVillage(t).Coords(),
				},
			},
		},
		{
			name: "ERR: len(coords) > 200",
			args: args{
				coords: func() []domain.Coords {
					coords := make([]domain.Coords, 201)
					for i := range coords {
						coords[i] = domaintest.NewVillage(t).Coords()
					}
					return coords
				}(),
			},
			expectedErr: domain.ValidationError{
				Model: "ListVillagesParams",
				Field: "coords",
				Err: domain.LenOutOfRangeError{
					Min:     0,
					Max:     200,
					Current: 201,
				},
			},
		},
	}

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

			params := domain.NewListVillagesParams()

			require.ErrorIs(t, params.SetCoords(tt.args.coords), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.coords, params.Coords())
		})
	}
}

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

	type args struct {
		coords []string
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				coords: []string{
					domaintest.NewVillage(t).Coords().String(),
					domaintest.NewVillage(t).Coords().String(),
					domaintest.NewVillage(t).Coords().String(),
				},
			},
		},
		{
			name: "ERR: len(coords) > 200",
			args: args{
				coords: func() []string {
					coords := make([]string, 201)
					for i := range coords {
						coords[i] = domaintest.NewVillage(t).Coords().String()
					}
					return coords
				}(),
			},
			expectedErr: domain.ValidationError{
				Model: "ListVillagesParams",
				Field: "coords",
				Err: domain.LenOutOfRangeError{
					Min:     0,
					Max:     200,
					Current: 201,
				},
			},
		},
		{
			name: "ERR: invalid coords string 1",
			args: args{
				coords: []string{
					domaintest.NewVillage(t).Coords().String(),
					domaintest.NewVillage(t).Coords().String(),
					"xxx|xxx",
				},
			},
			expectedErr: domain.SliceElementValidationError{
				Model: "ListVillagesParams",
				Field: "coords",
				Index: 2,
				Err:   domain.ErrInvalidCoordsString,
			},
		},
		{
			name: "ERR: invalid coords string 2",
			args: args{
				coords: []string{
					domaintest.NewVillage(t).Coords().String(),
					domaintest.NewVillage(t).Coords().String(),
					"xxx",
				},
			},
			expectedErr: domain.SliceElementValidationError{
				Model: "ListVillagesParams",
				Field: "coords",
				Index: 2,
				Err:   domain.ErrInvalidCoordsString,
			},
		},
		{
			name: "ERR: invalid coords string 3",
			args: args{
				coords: []string{
					domaintest.NewVillage(t).Coords().String(),
					domaintest.NewVillage(t).Coords().String(),
					domaintest.NewVillage(t).Coords().String() + domaintest.NewVillage(t).Coords().String(),
				},
			},
			expectedErr: domain.SliceElementValidationError{
				Model: "ListVillagesParams",
				Field: "coords",
				Index: 2,
				Err:   domain.ErrInvalidCoordsString,
			},
		},
	}

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

			params := domain.NewListVillagesParams()

			require.ErrorIs(t, params.SetCoordsString(tt.args.coords), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			require.Len(t, params.Coords(), len(tt.args.coords))
			for i, c := range tt.args.coords {
				assert.Equal(t, c, params.Coords()[i].String())
			}
		})
	}
}

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

	type args struct {
		sort []domain.VillageSort
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				sort: []domain.VillageSort{
					domain.VillageSortIDASC,
					domain.VillageSortServerKeyASC,
				},
			},
		},
		{
			name: "ERR: len(sort) < 1",
			args: args{
				sort: nil,
			},
			expectedErr: domain.ValidationError{
				Model: "ListVillagesParams",
				Field: "sort",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     2,
					Current: 0,
				},
			},
		},
		{
			name: "ERR: len(sort) > 2",
			args: args{
				sort: []domain.VillageSort{
					domain.VillageSortIDASC,
					domain.VillageSortServerKeyASC,
					domain.VillageSortServerKeyDESC,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "ListVillagesParams",
				Field: "sort",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     2,
					Current: 3,
				},
			},
		},
		{
			name: "ERR: conflict",
			args: args{
				sort: []domain.VillageSort{
					domain.VillageSortIDASC,
					domain.VillageSortIDDESC,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "ListVillagesParams",
				Field: "sort",
				Err: domain.SortConflictError{
					Sort: [2]string{domain.VillageSortIDASC.String(), domain.VillageSortIDDESC.String()},
				},
			},
		},
	}

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

			params := domain.NewListVillagesParams()

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

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

	validCursor := domaintest.NewVillageCursor(t)

	type args struct {
		cursor string
	}

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

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

			params := domain.NewListVillagesParams()

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

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

			params := domain.NewListVillagesParams()

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

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

	villages := domain.Villages{
		domaintest.NewVillage(t),
		domaintest.NewVillage(t),
		domaintest.NewVillage(t),
	}
	next := domaintest.NewVillage(t)

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

		res, err := domain.NewListVillagesResult(villages, next)
		require.NoError(t, err)
		assert.Equal(t, villages, res.Villages())
		assert.Equal(t, villages[0].ID(), res.Self().ID())
		assert.Equal(t, villages[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.NewListVillagesResult(villages, domain.Village{})
		require.NoError(t, err)
		assert.Equal(t, villages, res.Villages())
		assert.Equal(t, villages[0].ID(), res.Self().ID())
		assert.Equal(t, villages[0].ServerKey(), res.Self().ServerKey())
		assert.True(t, res.Next().IsZero())
	})

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

		res, err := domain.NewListVillagesResult(nil, domain.Village{})
		require.NoError(t, err)
		assert.Zero(t, res.Villages())
		assert.True(t, res.Self().IsZero())
		assert.True(t, res.Next().IsZero())
	})
}

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

	villages := domain.VillagesWithRelations{
		domaintest.NewVillageWithRelations(t),
		domaintest.NewVillageWithRelations(t),
		domaintest.NewVillageWithRelations(t),
	}
	next := domaintest.NewVillageWithRelations(t)

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

		res, err := domain.NewListVillagesWithRelationsResult(villages, next)
		require.NoError(t, err)
		assert.Equal(t, villages, res.Villages())
		assert.Equal(t, villages[0].Village().ID(), res.Self().ID())
		assert.Equal(t, villages[0].Village().ServerKey(), res.Self().ServerKey())
		assert.Equal(t, next.Village().ID(), res.Next().ID())
		assert.Equal(t, next.Village().ServerKey(), res.Next().ServerKey())
	})

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

		res, err := domain.NewListVillagesWithRelationsResult(villages, domain.VillageWithRelations{})
		require.NoError(t, err)
		assert.Equal(t, villages, res.Villages())
		assert.Equal(t, villages[0].Village().ID(), res.Self().ID())
		assert.Equal(t, villages[0].Village().ServerKey(), res.Self().ServerKey())
		assert.True(t, res.Next().IsZero())
	})

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

		res, err := domain.NewListVillagesWithRelationsResult(nil, domain.VillageWithRelations{})
		require.NoError(t, err)
		assert.Zero(t, res.Villages())
		assert.True(t, res.Self().IsZero())
		assert.True(t, res.Next().IsZero())
	})
}
