package main

import (
	"math/rand"
	"testing"
)

func TestEasyBotAlwaysMovesToEmptyCell(t *testing.T) {
	rnd := rand.New(rand.NewSource(1))
	bot := TicTacToeBot{Difficulty: TicTacToeDifficultyEasy}
	g := NewTicTacToeGame(TicTacToeO) // человек — нолики, бот ходит крестиками первым
	for i := 0; i < 9 && g.Phase == TicTacToePhasePlay; i++ {
		pos := bot.DecideMove(g, rnd)
		if pos < 0 || pos > 8 || g.Board[pos] != TicTacToeEmpty {
			t.Fatalf("бот выбрал недопустимую клетку %d при доске %v", pos, g.Board)
		}
		g.PlaceMark(pos)
	}
}

func TestMediumBotTakesImmediateWin(t *testing.T) {
	rnd := rand.New(rand.NewSource(1))
	bot := TicTacToeBot{Difficulty: TicTacToeDifficultyMedium}
	g := NewTicTacToeGame(TicTacToeO)
	// X уже имеет две подряд (0,1) — ход X, победный ход очевиден: клетка 2
	g.Board[0] = TicTacToeX
	g.Board[1] = TicTacToeX
	g.Board[3] = TicTacToeO
	g.Board[4] = TicTacToeO
	g.CurrentTurn = TicTacToeX
	g.MovesPlayed = 4

	pos := bot.DecideMove(g, rnd)
	if pos != 2 {
		t.Errorf("средний уровень должен был взять очевидную победу в клетке 2, получено %d", pos)
	}
}

func TestMediumBotBlocksImmediateLoss(t *testing.T) {
	rnd := rand.New(rand.NewSource(1))
	bot := TicTacToeBot{Difficulty: TicTacToeDifficultyMedium}
	g := NewTicTacToeGame(TicTacToeX)
	// O имеет две подряд (3,4) и вот-вот выиграет в клетке 5; ходит O (бот)
	g.Board[3] = TicTacToeO
	g.Board[4] = TicTacToeO
	g.Board[0] = TicTacToeX
	g.Board[1] = TicTacToeX
	g.CurrentTurn = TicTacToeO
	g.MovesPlayed = 4

	pos := bot.DecideMove(g, rnd)
	if pos != 5 {
		t.Errorf("средний уровень должен был заблокировать угрозу в клетке 5, получено %d", pos)
	}
}

// playTicTacToeToEnd доигрывает партию до конца, где botX ходит за
// крестики, а botO — за нолики.
func playTicTacToeToEnd(t *testing.T, g *TicTacToeGameState, rnd *rand.Rand, botX, botO TicTacToeBot) {
	t.Helper()
	for i := 0; i < 9 && g.Phase == TicTacToePhasePlay; i++ {
		var pos int
		if g.CurrentTurn == TicTacToeX {
			pos = botX.DecideMove(g, rnd)
		} else {
			pos = botO.DecideMove(g, rnd)
		}
		if pos < 0 {
			t.Fatalf("бот вернул недопустимый ход -1 при незавершённой игре, доска: %v", g.Board)
		}
		if err := g.PlaceMark(pos); err != nil {
			t.Fatalf("неожиданная ошибка хода: %v (доска: %v, позиция: %d)", err, g.Board, pos)
		}
	}
	if g.Phase != TicTacToePhaseOver {
		t.Fatalf("партия не завершилась за 9 ходов — это невозможно для крестиков-ноликов, доска: %v", g.Board)
	}
}

func TestHardBotNeverLosesAgainstAnyOpponent(t *testing.T) {
	hard := TicTacToeBot{Difficulty: TicTacToeDifficultyHard}

	// Средний и непобедимый уровни не используют rnd вообще (их
	// решение полностью детерминировано доской) — при одинаковой
	// стартовой раскладке разные seed дали бы совершенно одинаковую
	// партию, так что достаточно одного прогона на каждую раскладку.
	deterministic := []TicTacToeBot{
		{Difficulty: TicTacToeDifficultyMedium},
		{Difficulty: TicTacToeDifficultyHard},
	}
	for _, opponent := range deterministic {
		rnd := rand.New(rand.NewSource(1))

		g := NewTicTacToeGame(TicTacToeO)
		playTicTacToeToEnd(t, g, rnd, hard, opponent)
		if g.Result.Winner == TicTacToeO {
			t.Fatalf("непобедимый уровень (крестики) проиграл сопернику %v", opponent.Difficulty)
		}

		g2 := NewTicTacToeGame(TicTacToeX)
		playTicTacToeToEnd(t, g2, rnd, opponent, hard)
		if g2.Result.Winner == TicTacToeX {
			t.Fatalf("непобедимый уровень (нолики) проиграл сопернику %v", opponent.Difficulty)
		}
	}

	// Лёгкий уровень — единственный, где ход действительно случаен,
	// так что здесь имеет смысл перебрать много разных seed.
	easy := TicTacToeBot{Difficulty: TicTacToeDifficultyEasy}
	for seed := int64(0); seed < 500; seed++ {
		rnd := rand.New(rand.NewSource(seed))

		g := NewTicTacToeGame(TicTacToeO)
		playTicTacToeToEnd(t, g, rnd, hard, easy)
		if g.Result.Winner == TicTacToeO {
			t.Fatalf("непобедимый уровень (крестики) проиграл случайному сопернику на seed %d", seed)
		}

		g2 := NewTicTacToeGame(TicTacToeX)
		playTicTacToeToEnd(t, g2, rnd, easy, hard)
		if g2.Result.Winner == TicTacToeX {
			t.Fatalf("непобедимый уровень (нолики) проиграл случайному сопернику на seed %d", seed)
		}
	}
}

func TestHardVsHardAlwaysDraws(t *testing.T) {
	hard := TicTacToeBot{Difficulty: TicTacToeDifficultyHard}
	rnd := rand.New(rand.NewSource(1))
	g := NewTicTacToeGame(TicTacToeO) // оба хода в этом тесте делают боты, HumanMark тут не участвует
	playTicTacToeToEnd(t, g, rnd, hard, hard)
	if !g.Result.Draw {
		t.Fatalf("два непобедимых бота (полностью детерминированных) должны были сыграть вничью, получено Result=%+v", g.Result)
	}
}

func TestPlayFullTurnOnlyActsOnBotsTurn(t *testing.T) {
	rnd := rand.New(rand.NewSource(1))
	bot := TicTacToeBot{Difficulty: TicTacToeDifficultyHard}
	g := NewTicTacToeGame(TicTacToeX) // человек играет крестиками — сейчас его ход
	if err := bot.PlayFullTurn(g, rnd); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.MovesPlayed != 0 {
		t.Errorf("бот не должен был ходить, пока сейчас ход человека")
	}
}

func TestPlayFullTurnActsWhenItsBotsTurn(t *testing.T) {
	rnd := rand.New(rand.NewSource(1))
	bot := TicTacToeBot{Difficulty: TicTacToeDifficultyHard}
	g := NewTicTacToeGame(TicTacToeO) // человек — нолики, бот (крестики) ходит первым
	if err := bot.PlayFullTurn(g, rnd); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.MovesPlayed != 1 {
		t.Errorf("бот должен был сделать один ход, MovesPlayed=%d", g.MovesPlayed)
	}
}
