package main

import "testing"

func gp(r, c int) GoPos { return GoPos{Row: r, Col: c} }

func TestNewGoGameEmpty(t *testing.T) {
	g := NewGoGame(9)
	if g.Size != 9 {
		t.Fatalf("ожидался размер доски 9, получено %d", g.Size)
	}
	if g.Turn != GoBlack {
		t.Errorf("первый ход должен быть за чёрными")
	}
	for r := 0; r < 9; r++ {
		for c := 0; c < 9; c++ {
			if g.Board[r][c] != GoEmpty {
				t.Fatalf("новая доска должна быть полностью пустой")
			}
		}
	}
}

func TestPlaySwitchesTurn(t *testing.T) {
	g := NewGoGame(9)
	if err := g.Play(gp(4, 4)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Board[4][4] != GoBlack {
		t.Errorf("камень должен был встать в (4,4) как чёрный")
	}
	if g.Turn != GoWhite {
		t.Errorf("ход должен был перейти к белым")
	}
}

func TestCannotPlayOnOccupiedPoint(t *testing.T) {
	g := NewGoGame(9)
	g.Play(gp(4, 4))
	if err := g.Play(gp(4, 4)); err != ErrGoOccupied {
		t.Errorf("ожидалась ошибка занятой точки, получено: %v", err)
	}
}

func TestCannotPlayOutOfBounds(t *testing.T) {
	g := NewGoGame(9)
	if err := g.Play(gp(-1, 0)); err != ErrGoOutOfBounds {
		t.Errorf("ожидалась ошибка выхода за границы, получено: %v", err)
	}
	if err := g.Play(gp(9, 0)); err != ErrGoOutOfBounds {
		t.Errorf("ожидалась ошибка выхода за границы, получено: %v", err)
	}
}

func TestSingleStoneCapture(t *testing.T) {
	g := NewGoGame(9)
	g.Board[3][4] = GoBlack
	g.Board[5][4] = GoBlack
	g.Board[4][3] = GoBlack
	g.Board[4][4] = GoWhite
	g.Turn = GoBlack

	if err := g.Play(gp(4, 5)); err != nil {
		t.Fatalf("неожиданная ошибка взятия: %v", err)
	}
	if g.Board[4][4] != GoEmpty {
		t.Errorf("белый камень должен был быть взят")
	}
	if g.CapturedByBlack != 1 {
		t.Errorf("ожидалось 1 взятие у чёрных, получено %d", g.CapturedByBlack)
	}
}

func TestGroupCaptureRemovesWholeGroup(t *testing.T) {
	g := NewGoGame(9)
	g.Board[4][4] = GoWhite
	g.Board[4][5] = GoWhite
	g.Board[3][4] = GoBlack
	g.Board[3][5] = GoBlack
	g.Board[5][4] = GoBlack
	g.Board[5][5] = GoBlack
	g.Board[4][3] = GoBlack
	g.Turn = GoBlack

	if err := g.Play(gp(4, 6)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Board[4][4] != GoEmpty || g.Board[4][5] != GoEmpty {
		t.Errorf("вся группа должна была быть взята целиком")
	}
	if g.CapturedByBlack != 2 {
		t.Errorf("ожидалось 2 взятия, получено %d", g.CapturedByBlack)
	}
}

func TestSuicideMoveIsIllegal(t *testing.T) {
	g := NewGoGame(9)
	g.Board[3][4] = GoWhite
	g.Board[5][4] = GoWhite
	g.Board[4][3] = GoWhite
	g.Board[4][5] = GoWhite
	g.Turn = GoBlack

	if err := g.Play(gp(4, 4)); err != ErrGoSuicide {
		t.Errorf("ожидалась ошибка самоубийственного хода, получено: %v", err)
	}
	if g.Board[4][4] != GoEmpty {
		t.Errorf("доска не должна была измениться после отклонённого хода")
	}
}

func TestSuicideAllowedIfItCaptures(t *testing.T) {
	g := NewGoGame(9)
	// белый камень (4,4) окружён чёрными с трёх сторон, единственная
	// свобода — (4,5); чёрные играют туда — это взятие, а не
	// самоубийство их собственного камня
	g.Board[3][4] = GoBlack
	g.Board[5][4] = GoBlack
	g.Board[4][3] = GoBlack
	g.Board[4][4] = GoWhite
	g.Turn = GoBlack

	if err := g.Play(gp(4, 5)); err != nil {
		t.Fatalf("ход должен был пройти как взятие, а не самоубийство: %v", err)
	}
	if g.Board[4][4] != GoEmpty {
		t.Errorf("белый камень должен был быть взят")
	}
	if g.Board[4][5] != GoBlack {
		t.Errorf("чёрный камень должен был остаться на месте после взятия")
	}
}

func TestKoRuleForbidsImmediateRecapture(t *testing.T) {
	g := NewGoGame(9)
	// классическая форма ко (координаты row,col):
	//   (0,1)=W (0,2)=B
	//   (1,0)=W (1,1)=. (1,2)=W (1,3)=B   <- белые только что взяли
	//                                        одинокого чёрного в (1,1),
	//                                        сыграв в (1,2)
	//   (2,1)=W (2,2)=B
	// белый камень (1,2) сам стоит всего с одной свободой — (1,1) —
	// и чёрные не могут немедленно отыграть его обратно
	g.Board[0][1] = GoWhite
	g.Board[0][2] = GoBlack
	g.Board[1][0] = GoWhite
	g.Board[1][2] = GoWhite
	g.Board[1][3] = GoBlack
	g.Board[2][1] = GoWhite
	g.Board[2][2] = GoBlack

	// позиция ДО хода белых: на месте (1,2) было пусто, а на (1,1)
	// стоял тот самый одинокий чёрный камень, которого взяли
	before := cloneGoBoard(g.Board)
	before[1][1] = GoBlack
	before[1][2] = GoEmpty
	g.prevBoardBeforeOpponentMove = before
	g.Turn = GoBlack

	if err := g.Play(gp(1, 1)); err != ErrGoKoViolation {
		t.Errorf("ожидалась ошибка нарушения ко, получено: %v", err)
	}
}

func TestKoAllowedAfterPlayingElsewhere(t *testing.T) {
	g := NewGoGame(9)
	g.Board[0][1] = GoWhite
	g.Board[0][2] = GoBlack
	g.Board[1][0] = GoWhite
	g.Board[1][2] = GoWhite
	g.Board[1][3] = GoBlack
	g.Board[2][1] = GoWhite
	g.Board[2][2] = GoBlack
	before := cloneGoBoard(g.Board)
	before[1][1] = GoBlack
	before[1][2] = GoEmpty
	g.prevBoardBeforeOpponentMove = before
	g.Turn = GoBlack

	// чёрные играют "ходом ко" в другое место
	if err := g.Play(gp(7, 7)); err != nil {
		t.Fatalf("неожиданная ошибка хода в сторону: %v", err)
	}
	// теперь белые тоже играют куда-то ещё
	if err := g.Play(gp(0, 7)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	// а теперь чёрные вновь могут взять в (1,1), т.к. позиция уже не та
	if err := g.Play(gp(1, 1)); err != nil {
		t.Fatalf("после ходов в сторону взятие должно быть разрешено: %v", err)
	}
}

func TestPassTwiceEndsGame(t *testing.T) {
	g := NewGoGame(9)
	if err := g.Pass(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Result != nil {
		t.Fatalf("после одного паса партия не должна завершаться")
	}
	if err := g.Pass(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Result == nil {
		t.Fatalf("после двух пасов подряд партия должна была завершиться")
	}
}

func TestPassResetByPlay(t *testing.T) {
	g := NewGoGame(9)
	g.Pass()
	if err := g.Play(gp(4, 4)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Passes != 0 {
		t.Errorf("счётчик пасов должен был сброситься после реального хода")
	}
}

func TestScoringEmptyBoardKomiDecides(t *testing.T) {
	g := NewGoGame(9)
	g.Pass()
	g.Pass()
	if g.Result == nil {
		t.Fatalf("партия должна была завершиться")
	}
	if g.Result.BlackScore != 0 {
		t.Errorf("на пустой доске у чёрных должно быть 0 очков, получено %v", g.Result.BlackScore)
	}
	if g.Result.WhiteScore != GoKomi {
		t.Errorf("у белых должно быть ровно коми (%v), получено %v", GoKomi, g.Result.WhiteScore)
	}
	if g.Result.Winner != GoWhite {
		t.Errorf("на пустой доске белые должны побеждать за счёт коми")
	}
}

func TestScoringCountsStonesAndTerritory(t *testing.T) {
	g := NewGoGame(9)
	// угловая точка (0,0) на доске имеет всего два соседа — (0,1) и
	// (1,0); если оба чёрные, то сама точка (0,0), оставшись пустой,
	// полностью замкнута исключительно чёрным цветом
	g.Board[0][1] = GoBlack
	g.Board[1][0] = GoBlack
	g.Board[8][8] = GoWhite

	g.Pass()
	g.Pass()

	if g.Result == nil {
		t.Fatalf("партия должна была завершиться")
	}
	// у чёрных: 2 камня + 1 замкнутая пустая точка (0,0) = 3
	if g.Result.BlackScore != 3 {
		t.Errorf("ожидалось 3 очка у чёрных, получено %v", g.Result.BlackScore)
	}
}

func TestNeutralTerritoryNotScoredToEither(t *testing.T) {
	g := NewGoGame(9)
	g.Board[4][4] = GoBlack
	g.Board[4][5] = GoWhite

	g.Pass()
	g.Pass()

	if g.Result == nil {
		t.Fatalf("партия должна была завершиться")
	}
	if g.Result.BlackScore != 1 {
		t.Errorf("ожидалось 1 очко у чёрных (только свой камень), получено %v", g.Result.BlackScore)
	}
	if g.Result.WhiteScore != 1+GoKomi {
		t.Errorf("ожидалось 1+коми у белых, получено %v", g.Result.WhiteScore)
	}
}

func TestIsLegalDoesNotMutateState(t *testing.T) {
	g := NewGoGame(9)
	g.Board[3][4] = GoWhite
	g.Board[5][4] = GoWhite
	g.Board[4][3] = GoWhite
	g.Board[4][5] = GoWhite
	g.Turn = GoBlack

	if g.IsLegal(gp(4, 4)) {
		t.Errorf("самоубийственный ход не должен считаться легальным")
	}
	if g.Board[4][4] != GoEmpty {
		t.Errorf("IsLegal не должен был изменить реальную доску")
	}
	if g.Turn != GoBlack {
		t.Errorf("IsLegal не должен был менять очередь хода")
	}
}

func TestPlayAfterGameOverReturnsError(t *testing.T) {
	g := NewGoGame(9)
	g.Pass()
	g.Pass()
	if err := g.Play(gp(0, 0)); err != ErrGoGameOver {
		t.Errorf("ожидалась ErrGoGameOver, получено: %v", err)
	}
	if err := g.Pass(); err != ErrGoGameOver {
		t.Errorf("ожидалась ErrGoGameOver при пасе после конца игры, получено: %v", err)
	}
}

func TestBoardSizesSupported(t *testing.T) {
	for _, size := range []int{9, 13, 19} {
		g := NewGoGame(size)
		if g.Size != size || len(g.Board) != size || len(g.Board[0]) != size {
			t.Errorf("некорректная доска для размера %d", size)
		}
	}
}
