package main

import "testing"

func TestGoModelPlayStoneViaKey(t *testing.T) {
	m := NewGoModel(9, GoDifficultyEasy, ColorChoiceA)
	m.cursor = GoPos{Row: 4, Col: 4}

	next, _ := m.Update(key("enter"))
	m = next.(GoModel)
	if m.isError {
		t.Fatalf("неожиданная ошибка: %s", m.message)
	}
	if m.game.Board[4][4] != GoBlack {
		t.Fatalf("камень должен был встать в (4,4) как чёрный")
	}
}

func TestGoModelCursorMovement(t *testing.T) {
	m := NewGoModel(9, GoDifficultyEasy, ColorChoiceA)
	m.cursor = GoPos{Row: 4, Col: 4}

	next, _ := m.Update(key("up"))
	m = next.(GoModel)
	if m.cursor.Row != 3 {
		t.Errorf("курсор должен был сдвинуться вверх")
	}
	next, _ = m.Update(key("right"))
	m = next.(GoModel)
	if m.cursor.Col != 5 {
		t.Errorf("курсор должен был сдвинуться вправо")
	}
}

func TestGoModelCursorClampsAtEdges(t *testing.T) {
	m := NewGoModel(9, GoDifficultyEasy, ColorChoiceA)
	m.cursor = GoPos{Row: 0, Col: 0}

	next, _ := m.Update(key("up"))
	m = next.(GoModel)
	if m.cursor.Row != 0 {
		t.Errorf("курсор не должен был уйти за верхний край")
	}
	next, _ = m.Update(key("left"))
	m = next.(GoModel)
	if m.cursor.Col != 0 {
		t.Errorf("курсор не должен был уйти за левый край")
	}
}

func TestGoModelPassViaKey(t *testing.T) {
	m := NewGoModel(9, GoDifficultyEasy, ColorChoiceA)
	next, _ := m.Update(key("p"))
	m = next.(GoModel)
	if m.isError {
		t.Fatalf("неожиданная ошибка паса: %s", m.message)
	}
}

func TestGoModelInvalidMoveShowsError(t *testing.T) {
	m := NewGoModel(9, GoDifficultyEasy, ColorChoiceA)
	m.cursor = GoPos{Row: 0, Col: 0}
	m.game.Board[0][0] = GoBlack

	next, _ := m.Update(key("enter"))
	m = next.(GoModel)
	if !m.isError {
		t.Fatalf("ожидалась ошибка хода на занятую точку")
	}
}

// TestGoModelBotPassShowsDistinctMessage — по мотивам реального
// наблюдения пользователя: "бот делал ход, но камень не появлялся"
// — на самом деле бот пасовал, но интерфейс молча показывал то же
// сообщение "Бот сходил.", что и при обычном ходе, из-за чего
// казалось, будто ход просто не отрисовался.
func TestGoModelBotPassShowsDistinctMessage(t *testing.T) {
	m := NewGoModel(9, GoDifficultyEasy, ColorChoiceA)
	// заполняем всю доску без единой пустой клетки — тогда легальных
	// ходов гарантированно нет ни у кого, пас неизбежен
	for r := 0; r < 9; r++ {
		for c := 0; c < 9; c++ {
			if (r+c)%2 == 0 {
				m.game.Board[r][c] = GoBlack
			} else {
				m.game.Board[r][c] = GoWhite
			}
		}
	}
	m.game.Turn = GoWhite

	next, _ := m.Update(botMoveMsg{})
	m = next.(GoModel)
	if m.message != T("go.msg.bot_passed") {
		t.Errorf("ожидалось отдельное сообщение о пасе бота, получено: %q", m.message)
	}
	if m.message == T("go.msg.bot_moved") {
		t.Errorf("сообщение о пасе не должно совпадать с сообщением об обычном ходе")
	}
}

func TestGoModelQReturnsToMenu(t *testing.T) {
	m := NewGoModel(9, GoDifficultyEasy, ColorChoiceA)
	next, _ := m.Update(key("q"))
	m = next.(GoModel)
	if !m.backToMenu {
		t.Fatalf("ожидался флаг backToMenu после 'q'")
	}
	if m.quitting {
		t.Fatalf("'q' не должен закрывать всё приложение")
	}
}

func TestGoModelNewGameAfterResult(t *testing.T) {
	m := NewGoModel(9, GoDifficultyEasy, ColorChoiceA)
	m.game.Result = &GoResult{Winner: GoBlack, BlackScore: 10, WhiteScore: 7.5}

	next, _ := m.Update(key("n"))
	m = next.(GoModel)
	if m.game.Result != nil {
		t.Errorf("новая партия не должна была начаться с результатом")
	}
	if m.game.Size != 9 {
		t.Errorf("новая партия должна была сохранить размер доски")
	}
}

func TestGoModelSupportsAllBoardSizes(t *testing.T) {
	for _, size := range []int{9, 13, 19} {
		m := NewGoModel(size, GoDifficultyMedium, ColorChoiceA)
		if m.game.Size != size {
			t.Errorf("ожидался размер доски %d, получено %d", size, m.game.Size)
		}
	}
}

// TestGoModelFullAutoPlaythrough прогоняет TUI-модель от начала до
// конца на маленькой доске, где "человек" отыгрывает свои ходы
// через бота, проверяя, что весь путь через Update не паникует и
// партия завершается.
func TestGoModelFullAutoPlaythrough(t *testing.T) {
	m := NewGoModel(9, GoDifficultyEasy, ColorChoiceA)
	helperBot := GoBot{Difficulty: GoDifficultyEasy}
	rnd := m.rnd

	const maxSteps = 3000
	for i := 0; i < maxSteps && m.game.Result == nil; i++ {
		if m.game.Turn != m.human {
			next, _ := m.Update(botMoveMsg{})
			m = next.(GoModel)
			continue
		}
		pos, ok := helperBot.DecideMove(m.game, rnd)
		if !ok {
			next, _ := m.Update(key("p"))
			m = next.(GoModel)
			continue
		}
		m.cursor = pos
		next, _ := m.Update(key("enter"))
		m = next.(GoModel)
		if m.isError {
			t.Fatalf("неожиданная ошибка хода человеком: %s", m.message)
		}
	}
	if m.game.Result == nil {
		t.Fatalf("партия через TUI не завершилась за %d шагов", maxSteps)
	}
}

func TestNewGoModelRespectsColorChoiceBlack(t *testing.T) {
	m := NewGoModel(9, GoDifficultyEasy, ColorChoiceA)
	if m.human != GoBlack {
		t.Errorf("при выборе ColorChoiceA человек должен был играть чёрными, получено %v", m.human)
	}
}

func TestNewGoModelRespectsColorChoiceWhite(t *testing.T) {
	m := NewGoModel(9, GoDifficultyEasy, ColorChoiceB)
	if m.human != GoWhite {
		t.Errorf("при выборе ColorChoiceB человек должен был играть белыми, получено %v", m.human)
	}
}

func TestNewGoModelRandomColorGivesValidColor(t *testing.T) {
	seenBlack, seenWhite := false, false
	for i := 0; i < 50 && !(seenBlack && seenWhite); i++ {
		m := NewGoModel(9, GoDifficultyEasy, ColorChoiceRandom)
		if m.human == GoBlack {
			seenBlack = true
		} else {
			seenWhite = true
		}
	}
	if !seenBlack || !seenWhite {
		t.Errorf("случайный выбор цвета должен был давать оба варианта за 50 попыток: чёрные=%v белые=%v", seenBlack, seenWhite)
	}
}
