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/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

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

	server := domaintest.NewServer(t)

	active := domain.BasePlayers{
		domaintest.NewBasePlayer(t),
		domaintest.NewBasePlayer(t),
		domaintest.NewBasePlayer(t),
	}

	players := domain.Players{
		domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
			cfg.ID = active[0].ID()
			cfg.ServerKey = server.Key()
		}),
		domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) { // should be deleted
			cfg.ServerKey = server.Key()
			cfg.TribeID = 0
		}),
		domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) { // should be deleted + tribe change
			cfg.ServerKey = server.Key()
		}),
		domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
			cfg.ServerKey = server.Key()
			cfg.DeletedAt = time.Now()
		}),
		domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
			cfg.ID = active[1].ID()
			cfg.ServerKey = server.Key()
		}),
		domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
			cfg.ID = active[2].ID()
			cfg.ServerKey = domaintest.RandServerKey()
		}),
	}

	expectedIDs := []int{players[1].ID(), players[2].ID()}
	expectedCreateTribeChangeParams := []struct {
		serverKey  string
		playerID   int
		oldTribeID int
		newTribeID int
	}{
		{
			serverKey:  server.Key(),
			playerID:   players[2].ID(),
			oldTribeID: players[2].TribeID(),
			newTribeID: 0,
		},
	}

	slices.Sort(expectedIDs)

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

	slices.SortFunc(players, func(a, b domain.Player) int {
		if res := cmp.Compare(a.ServerKey(), b.ServerKey()); res != 0 {
			return res
		}
		return cmp.Compare(a.ID(), b.ID())
	})

	ids, tribeChangesParams, err := players.Delete(server.Key(), active)
	require.NoError(t, err)
	assert.Equal(t, expectedIDs, ids)
	assert.Len(t, tribeChangesParams, len(expectedCreateTribeChangeParams))
	for i, expected := range expectedCreateTribeChangeParams {
		idx := slices.IndexFunc(tribeChangesParams, func(params domain.CreateTribeChangeParams) bool {
			return params.PlayerID() == expected.playerID && params.ServerKey() == expected.serverKey
		})
		require.GreaterOrEqualf(t, idx, 0, "expectedCreateTribeChangeParams[%d] not found", i)

		params := tribeChangesParams[idx]

		assert.Equalf(t, expected.serverKey, params.ServerKey(), "expectedCreateTribeChangeParams[%d]", i)
		assert.Equalf(t, expected.playerID, params.PlayerID(), "expectedCreateTribeChangeParams[%d]", i)
		assert.Equalf(t, expected.newTribeID, params.NewTribeID(), "expectedCreateTribeChangeParams[%d]", i)
		assert.Equalf(t, expected.oldTribeID, params.OldTribeID(), "expectedCreateTribeChangeParams[%d]", i)
	}
}

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

	now := time.Now()

	server := domaintest.NewServer(t)

	players := domain.BasePlayers{
		domaintest.NewBasePlayer(t),
		domaintest.NewBasePlayer(t),
		domaintest.NewBasePlayer(t),
	}
	slices.SortFunc(players, func(a, b domain.BasePlayer) int {
		return cmp.Compare(a.ID(), b.ID())
	})

	storedPlayers := domain.Players{
		// server with random server key to verify that slice order matters
		domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
			cfg.ID = players[0].ID()
			cfg.ServerKey = domaintest.RandServerKey()
		}),
		// should update BestRank/MostPoints/MostVillages/LastActivityAt
		domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
			cfg.ID = players[0].ID()
			cfg.Points = players[0].Points() - 1
			cfg.NumVillages = players[0].NumVillages() - 1
			cfg.OD = players[0].OD()
			cfg.ServerKey = server.Key()
			cfg.BestRank = players[0].Rank() + 1
			cfg.BestRankAt = now.Add(-time.Hour)
			cfg.MostPoints = players[0].Points() - 1
			cfg.MostPointsAt = now.Add(-time.Hour)
			cfg.MostVillages = players[0].NumVillages() - 1
			cfg.MostVillagesAt = now.Add(-time.Hour)
			cfg.LastActivityAt = now.Add(-time.Hour)
		}),
		// shouldn't update BestRank/MostPoints/MostVillages/LastActivityAt
		domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
			cfg.ID = players[1].ID()
			cfg.Points = players[1].Points() + 1
			cfg.NumVillages = players[1].NumVillages() + 1
			cfg.OD = players[1].OD()
			cfg.ServerKey = server.Key()
			cfg.BestRank = players[1].Rank() - 1
			cfg.BestRankAt = now.Add(-time.Hour)
			cfg.MostPoints = players[1].Points() + 1
			cfg.MostPointsAt = now.Add(-time.Hour)
			cfg.MostVillages = players[1].NumVillages() + 1
			cfg.MostVillagesAt = now.Add(-time.Hour)
			cfg.LastActivityAt = now.Add(-time.Hour)
		}),
	}
	storedPlayersSorted := slices.Clone(storedPlayers)
	slices.SortFunc(storedPlayersSorted, func(a, b domain.Player) int {
		if res := cmp.Compare(a.ServerKey(), b.ServerKey()); res != 0 {
			return res
		}
		return cmp.Compare(a.ID(), b.ID())
	})

	expectedParams := []struct {
		base           domain.BasePlayer
		serverKey      string
		bestRank       int
		bestRankAt     time.Time
		mostPoints     int
		mostPointsAt   time.Time
		mostVillages   int
		mostVillagesAt time.Time
		lastActivityAt time.Time
	}{
		{
			base:           players[0],
			serverKey:      server.Key(),
			bestRank:       players[0].Rank(),
			bestRankAt:     now,
			mostPoints:     players[0].Points(),
			mostPointsAt:   now,
			mostVillages:   players[0].NumVillages(),
			mostVillagesAt: now,
			lastActivityAt: now,
		},
		{
			base:           players[1],
			serverKey:      server.Key(),
			bestRank:       storedPlayers[2].BestRank(),
			bestRankAt:     storedPlayers[2].BestRankAt(),
			mostPoints:     storedPlayers[2].MostPoints(),
			mostPointsAt:   storedPlayers[2].MostPointsAt(),
			mostVillages:   storedPlayers[2].MostVillages(),
			mostVillagesAt: storedPlayers[2].MostVillagesAt(),
			lastActivityAt: storedPlayers[2].LastActivityAt(),
		},
		{
			base:           players[2],
			serverKey:      server.Key(),
			bestRank:       players[2].Rank(),
			bestRankAt:     now,
			mostPoints:     players[2].Points(),
			mostPointsAt:   now,
			mostVillages:   players[2].NumVillages(),
			mostVillagesAt: now,
			lastActivityAt: now,
		},
	}

	res, err := domain.NewCreatePlayerParams(server.Key(), players, storedPlayersSorted)
	require.NoError(t, err)
	assert.Len(t, res, len(expectedParams))
	for i, expected := range expectedParams {
		idx := slices.IndexFunc(res, func(params domain.CreatePlayerParams) 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)
		assert.WithinDurationf(t, expected.lastActivityAt, params.LastActivityAt(), time.Minute, "expectedParams[%d]", i)
	}
}

func TestListPlayersParams_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 < 0",
			args: args{
				ids: []int{
					domaintest.RandID(),
					domaintest.RandID(),
					domaintest.RandID(),
					-1,
					domaintest.RandID(),
				},
			},
			expectedErr: domain.SliceElementValidationError{
				Model: "ListPlayersParams",
				Field: "ids",
				Index: 3,
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
	}

	for _, tt := range tests {
		tt := tt

		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			params := domain.NewListPlayersParams()

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

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

	type args struct {
		idGT domain.NullInt
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				idGT: domain.NullInt{
					Value: domaintest.RandID(),
					Valid: true,
				},
			},
		},
		{
			name: "ERR: value < 0",
			args: args{
				idGT: domain.NullInt{
					Value: -1,
					Valid: true,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "ListPlayersParams",
				Field: "idGT",
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
	}

	for _, tt := range tests {
		tt := tt

		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			params := domain.NewListPlayersParams()

			require.ErrorIs(t, params.SetIDGT(tt.args.idGT), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.idGT, params.IDGT())
		})
	}
}

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

	type args struct {
		sort []domain.PlayerSort
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				sort: []domain.PlayerSort{
					domain.PlayerSortIDASC,
					domain.PlayerSortServerKeyASC,
				},
			},
		},
		{
			name: "ERR: len(sort) < 1",
			args: args{
				sort: nil,
			},
			expectedErr: domain.ValidationError{
				Model: "ListPlayersParams",
				Field: "sort",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     2,
					Current: 0,
				},
			},
		},
		{
			name: "ERR: len(sort) > 2",
			args: args{
				sort: []domain.PlayerSort{
					domain.PlayerSortIDASC,
					domain.PlayerSortServerKeyASC,
					domain.PlayerSortServerKeyDESC,
				},
			},
			expectedErr: domain.ValidationError{
				Model: "ListPlayersParams",
				Field: "sort",
				Err: domain.LenOutOfRangeError{
					Min:     1,
					Max:     2,
					Current: 3,
				},
			},
		},
	}

	for _, tt := range tests {
		tt := tt

		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			params := domain.NewListPlayersParams()

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

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

	for _, tt := range tests {
		tt := tt

		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			params := domain.NewListPlayersParams()

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

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

	type args struct {
		offset int
	}

	tests := []struct {
		name        string
		args        args
		expectedErr error
	}{
		{
			name: "OK",
			args: args{
				offset: 100,
			},
		},
		{
			name: "ERR: offset < 0",
			args: args{
				offset: -1,
			},
			expectedErr: domain.ValidationError{
				Model: "ListPlayersParams",
				Field: "offset",
				Err: domain.MinGreaterEqualError{
					Min:     0,
					Current: -1,
				},
			},
		},
	}

	for _, tt := range tests {
		tt := tt

		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			params := domain.NewListPlayersParams()

			require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr)
			if tt.expectedErr != nil {
				return
			}
			assert.Equal(t, tt.args.offset, params.Offset())
		})
	}
}
