package main

import (
	"math/rand"
	"testing"
)

func msp(r, c int) MinesweeperPos { return MinesweeperPos{Row: r, Col: c} }

func TestNewMinesweeperGameEmpty(t *testing.T) {
	g := NewMinesweeperGame(9, 9, 10)
	if g.Width != 9 || g.Height != 9 || g.MineCount != 10 {
		t.Fatalf("неверные параметры поля: %+v", g)
	}
	for r := 0; r < 9; r++ {
		for c := 0; c < 9; c++ {
			if g.Board[r][c].IsMine || g.Board[r][c].Revealed {
				t.Fatalf("новое поле не должно содержать мин или открытых клеток до первого хода")
			}
		}
	}
}

func TestMineCountClampedToLeaveOneSafeCell(t *testing.T) {
	g := NewMinesweeperGame(3, 3, 100)
	if g.MineCount != 8 {
		t.Errorf("ожидалось ограничение до 8 мин (9 клеток - 1), получено %d", g.MineCount)
	}
}

func TestFirstRevealNeverHitsMine(t *testing.T) {
	rnd := rand.New(rand.NewSource(1))
	for trial := 0; trial < 50; trial++ {
		g := NewMinesweeperGame(9, 9, 10)
		pos := msp(4, 4)
		g.placeMines(pos, rnd)
		if g.Board[pos.Row][pos.Col].IsMine {
			t.Fatalf("попытка %d: первая открытая клетка не должна быть миной", trial)
		}
		for _, n := range msNeighbors(pos) {
			if g.inBounds(n) && g.Board[n.Row][n.Col].IsMine {
				t.Fatalf("попытка %d: сосед первой клетки тоже не должен быть миной", trial)
			}
		}
	}
}

func TestRevealPlacesMinesLazilyOnFirstCall(t *testing.T) {
	g := NewMinesweeperGame(9, 9, 10)
	if g.minesPlaced {
		t.Fatalf("мины не должны быть расставлены до первого Reveal")
	}
	if err := g.Reveal(msp(4, 4)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if !g.minesPlaced {
		t.Errorf("мины должны были расставиться после первого Reveal")
	}
}

func TestRevealMineEndsGameAndRevealsAllMines(t *testing.T) {
	g := NewMinesweeperGame(5, 5, 3)
	g.placeMines(msp(0, 0), rand.New(rand.NewSource(1)))
	g.minesPlaced = true

	var minePos MinesweeperPos
	found := false
	for r := 0; r < 5 && !found; r++ {
		for c := 0; c < 5 && !found; c++ {
			if g.Board[r][c].IsMine {
				minePos = msp(r, c)
				found = true
			}
		}
	}
	if !found {
		t.Fatalf("в тестовом поле должна была быть хотя бы одна мина")
	}

	if err := g.Reveal(minePos); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Result == nil || g.Result.Won {
		t.Fatalf("ожидалось поражение после открытия мины")
	}
	for r := 0; r < 5; r++ {
		for c := 0; c < 5; c++ {
			if g.Board[r][c].IsMine && !g.Board[r][c].Revealed {
				t.Errorf("все мины должны были раскрыться при поражении")
			}
		}
	}
}

func TestFloodRevealOpensZeroRegion(t *testing.T) {
	g := NewMinesweeperGame(5, 5, 1)
	g.Board[4][4].IsMine = true
	for r := 0; r < 5; r++ {
		for c := 0; c < 5; c++ {
			if g.Board[r][c].IsMine {
				continue
			}
			count := 0
			for _, n := range msNeighbors(msp(r, c)) {
				if g.inBounds(n) && g.Board[n.Row][n.Col].IsMine {
					count++
				}
			}
			g.Board[r][c].AdjacentMines = count
		}
	}
	g.minesPlaced = true

	if err := g.Reveal(msp(0, 0)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Board[0][0].AdjacentMines != 0 {
		t.Fatalf("тестовая настройка неверна: (0,0) должна иметь 0 соседних мин")
	}
	if !g.Board[0][2].Revealed {
		t.Errorf("каскад должен был открыть клетки вдалеке от мины")
	}
	if g.Board[4][4].Revealed {
		t.Errorf("сама мина не должна была открыться просто от каскада рядом")
	}
}

func TestCannotRevealAlreadyOpenCell(t *testing.T) {
	g := NewMinesweeperGame(9, 9, 10)
	g.Reveal(msp(4, 4))
	if err := g.Reveal(msp(4, 4)); err != ErrMinesweeperAlreadyOpen {
		t.Errorf("ожидалась ошибка повторного открытия, получено: %v", err)
	}
}

func TestCannotRevealFlaggedCell(t *testing.T) {
	g := NewMinesweeperGame(9, 9, 10)
	g.ToggleFlag(msp(4, 4))
	if err := g.Reveal(msp(4, 4)); err != ErrMinesweeperFlagged {
		t.Errorf("ожидалась ошибка флага, получено: %v", err)
	}
}

func TestToggleFlagOnAndOff(t *testing.T) {
	g := NewMinesweeperGame(9, 9, 10)
	if err := g.ToggleFlag(msp(4, 4)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if !g.Board[4][4].Flagged || g.FlagCount != 1 {
		t.Errorf("клетка должна была быть помечена флагом")
	}
	if err := g.ToggleFlag(msp(4, 4)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Board[4][4].Flagged || g.FlagCount != 0 {
		t.Errorf("флаг должен был сняться")
	}
}

func TestCannotFlagRevealedCell(t *testing.T) {
	g := NewMinesweeperGame(9, 9, 10)
	g.Reveal(msp(4, 4))
	if err := g.ToggleFlag(msp(4, 4)); err != ErrMinesweeperAlreadyOpen {
		t.Errorf("ожидалась ошибка попытки пометить открытую клетку, получено: %v", err)
	}
}

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

func TestWinConditionAllSafeCellsRevealed(t *testing.T) {
	g := NewMinesweeperGame(2, 2, 1)
	g.Board[1][1].IsMine = true
	g.Board[0][0].AdjacentMines = 1
	g.Board[0][1].AdjacentMines = 1
	g.Board[1][0].AdjacentMines = 1
	g.minesPlaced = true

	g.Reveal(msp(0, 0))
	if g.Result != nil {
		t.Fatalf("после одной открытой клетки из трёх безопасных победа наступить не должна")
	}
	g.Reveal(msp(0, 1))
	g.Reveal(msp(1, 0))
	if g.Result == nil || !g.Result.Won {
		t.Fatalf("после открытия всех безопасных клеток должна была наступить победа")
	}
}

func TestActionsAfterGameOverReturnError(t *testing.T) {
	g := NewMinesweeperGame(5, 5, 3)
	g.placeMines(msp(0, 0), rand.New(rand.NewSource(1)))
	g.minesPlaced = true
	for r := 0; r < 5; r++ {
		for c := 0; c < 5; c++ {
			if g.Board[r][c].IsMine {
				g.Reveal(msp(r, c))
				break
			}
		}
		if g.Result != nil {
			break
		}
	}
	if g.Result == nil {
		t.Fatalf("партия должна была завершиться поражением")
	}
	if err := g.Reveal(msp(0, 0)); err != ErrMinesweeperGameOver {
		t.Errorf("ожидалась ErrMinesweeperGameOver, получено: %v", err)
	}
	if err := g.ToggleFlag(msp(0, 0)); err != ErrMinesweeperGameOver {
		t.Errorf("ожидалась ErrMinesweeperGameOver при флаге, получено: %v", err)
	}
}

func TestChordOpensNeighborsWhenFlagsMatch(t *testing.T) {
	g := NewMinesweeperGame(5, 5, 2)
	g.minesPlaced = true
	g.Board[0][1].IsMine = true
	g.Board[1][0].IsMine = true
	for r := 0; r < 5; r++ {
		for c := 0; c < 5; c++ {
			if g.Board[r][c].IsMine {
				continue
			}
			n := 0
			for _, nb := range msNeighbors(msp(r, c)) {
				if g.inBounds(nb) && g.Board[nb.Row][nb.Col].IsMine {
					n++
				}
			}
			g.Board[r][c].AdjacentMines = n
		}
	}
	g.Board[0][0].Revealed = true
	g.RevealedCount++

	g.ToggleFlag(msp(0, 1))
	g.ToggleFlag(msp(1, 0))

	if err := g.Chord(msp(0, 0)); err != nil {
		t.Fatalf("неожиданная ошибка хорды: %v", err)
	}
	if !g.Board[1][1].Revealed {
		t.Errorf("хорда должна была открыть оставшегося непомеченного соседа (1,1)")
	}
}

func TestChordDoesNothingWhenFlagsDontMatch(t *testing.T) {
	g := NewMinesweeperGame(5, 5, 2)
	g.minesPlaced = true
	g.Board[0][1].IsMine = true
	g.Board[1][0].IsMine = true
	for r := 0; r < 5; r++ {
		for c := 0; c < 5; c++ {
			if g.Board[r][c].IsMine {
				continue
			}
			n := 0
			for _, nb := range msNeighbors(msp(r, c)) {
				if g.inBounds(nb) && g.Board[nb.Row][nb.Col].IsMine {
					n++
				}
			}
			g.Board[r][c].AdjacentMines = n
		}
	}
	g.Board[0][0].Revealed = true
	g.RevealedCount++
	// флаг только один, а мин рядом две — хорда не должна сработать
	g.ToggleFlag(msp(0, 1))

	if err := g.Chord(msp(0, 0)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Board[1][1].Revealed {
		t.Errorf("хорда не должна была ничего открыть при несовпадении числа флагов")
	}
}

func TestChordRequiresRevealedCell(t *testing.T) {
	g := NewMinesweeperGame(5, 5, 2)
	g.minesPlaced = true
	if err := g.Chord(msp(0, 0)); err != ErrMinesweeperNotRevealed {
		t.Errorf("ожидалась ErrMinesweeperNotRevealed, получено: %v", err)
	}
}

func TestChordCanTriggerLoss(t *testing.T) {
	g := NewMinesweeperGame(5, 5, 1)
	g.minesPlaced = true
	g.Board[0][1].IsMine = true
	for r := 0; r < 5; r++ {
		for c := 0; c < 5; c++ {
			if g.Board[r][c].IsMine {
				continue
			}
			n := 0
			for _, nb := range msNeighbors(msp(r, c)) {
				if g.inBounds(nb) && g.Board[nb.Row][nb.Col].IsMine {
					n++
				}
			}
			g.Board[r][c].AdjacentMines = n
		}
	}
	g.Board[0][0].Revealed = true
	g.RevealedCount++
	// неверно ставим флаг НЕ на мину, а число соседних мин у (0,0)
	// равно 1 — хорда решит, что "мина учтена", и откроет саму мину
	g.ToggleFlag(msp(1, 1))

	if err := g.Chord(msp(0, 0)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Result == nil || g.Result.Won {
		t.Fatalf("неверно поставленный флаг при хорде должен был привести к проигрышу")
	}
}
