package main

import (
	"math/rand"
	"testing"
)

// goRandomLegalMove находит случайную легальную точку для текущего
// игрока (или сигнализирует, что стоит пасовать, если ничего не
// нашлось за разумное число попыток).
func goRandomLegalMove(g *GoGameState, rnd *rand.Rand) (GoPos, bool) {
	size := g.Size
	order := rnd.Perm(size * size)
	for _, idx := range order {
		pos := GoPos{Row: idx / size, Col: idx % size}
		if g.IsLegal(pos) {
			return pos, true
		}
	}
	return GoPos{}, false
}

func TestGoRandomGamesNoCorruption(t *testing.T) {
	rnd := rand.New(rand.NewSource(1))

	for _, size := range []int{9, 13, 19} {
		for game := 0; game < 10; game++ {
			g := NewGoGame(size)
			const maxMoves = 500
			moves := 0
			for ; moves < maxMoves && g.Result == nil; moves++ {
				pos, ok := goRandomLegalMove(g, rnd)
				if !ok {
					if err := g.Pass(); err != nil {
						t.Fatalf("размер %d, игра %d: неожиданная ошибка паса: %v", size, game, err)
					}
					continue
				}
				if err := g.Play(pos); err != nil {
					t.Fatalf("размер %d, игра %d, ход %d: неожиданная ошибка легального хода %v: %v",
						size, game, moves, pos, err)
				}
			}
			if g.Result == nil {
				g.Pass()
				g.Pass()
			}
			if g.Result == nil {
				t.Fatalf("размер %d, игра %d: партия так и не завершилась", size, game)
			}
			if g.Result.BlackScore < 0 || g.Result.WhiteScore < GoKomi {
				t.Errorf("размер %d, игра %d: подозрительный счёт %+v", size, game, g.Result)
			}
		}
	}
}

func TestGoLegalMoveEnumerationPerformance19x19(t *testing.T) {
	g := NewGoGame(19)
	rnd := rand.New(rand.NewSource(2))
	for i := 0; i < 60; i++ {
		pos, ok := goRandomLegalMove(g, rnd)
		if !ok {
			break
		}
		if err := g.Play(pos); err != nil {
			t.Fatalf("шаг %d: неожиданная ошибка: %v", i, err)
		}
	}
}

// TestGoBotFullGames прогоняет несколько полных партий бота против
// самого себя на каждом размере доски и уровне сложности, проверяя
// отсутствие паник, зависаний и корректность итогового результата.
// TestGoPlayedStonePersistsAcrossManyGames — по мотивам реального
// наблюдения пользователя ("бот съел свой камень, когда окружил"):
// после ЛЮБОГО успешного (не отклонённого как самоубийство или
// нарушение ко) хода поставленный камень обязан остаться на доске
// своим цветом — если бы он тут же исчез, это означало бы, что либо
// сам ход был на самом деле самоубийственным (баг проверки суицида),
// либо взятие каким-то образом задело группу самого ходящего вместо
// вражеской. Прогоняет много партий бот-против-бота на всех
// размерах доски и уровнях сложности, проверяя инвариант после
// КАЖДОГО хода.
func TestGoPlayedStonePersistsAcrossManyGames(t *testing.T) {
	rnd := rand.New(rand.NewSource(7))

	for _, size := range []int{9, 13, 19} {
		for _, diff := range []GoDifficulty{GoDifficultyEasy, GoDifficultyMedium, GoDifficultyHard} {
			bot := GoBot{Difficulty: diff}
			g := NewGoGame(size)

			const maxMoves = 300
			for move := 0; move < maxMoves && g.Result == nil; move++ {
				mover := g.Turn
				pos, ok := bot.DecideMove(g, rnd)
				if !ok {
					if err := g.Pass(); err != nil {
						t.Fatalf("размер %d, сложность %d, ход %d: неожиданная ошибка паса: %v", size, diff, move, err)
					}
					continue
				}
				if err := g.Play(pos); err != nil {
					t.Fatalf("размер %d, сложность %d, ход %d: неожиданная ошибка хода в %v: %v", size, diff, move, pos, err)
				}
				if g.Board[pos.Row][pos.Col] != mover {
					t.Fatalf("размер %d, сложность %d, ход %d: после успешного хода цвета %v в точку %v там оказалось %v — камень исчез или сменил цвет сразу после собственного хода",
						size, diff, move, mover, pos, g.Board[pos.Row][pos.Col])
				}
			}
		}
	}
}

func TestGoBotFullGames(t *testing.T) {
	rnd := rand.New(rand.NewSource(3))

	for _, size := range []int{9, 13, 19} {
		for _, diff := range []GoDifficulty{GoDifficultyEasy, GoDifficultyMedium, GoDifficultyHard} {
			bot := GoBot{Difficulty: diff}
			g := NewGoGame(size)

			const maxMoves = 800
			moves := 0
			for ; moves < maxMoves && g.Result == nil; moves++ {
				if _, err := bot.PlayFullTurn(g, rnd); err != nil {
					t.Fatalf("размер %d, сложность %d, ход %d: неожиданная ошибка: %v", size, diff, moves, err)
				}
			}
			if g.Result == nil {
				t.Fatalf("размер %d, сложность %d: партия не завершилась за %d ходов", size, diff, maxMoves)
			}
			if g.Result.WhiteScore < GoKomi {
				t.Errorf("размер %d, сложность %d: подозрительный счёт белых %v (меньше коми)", size, diff, g.Result.WhiteScore)
			}
		}
	}
}
