package main

import (
	"fmt"
	"strings"
	"testing"
)

func TestNewGameDealsCorrectly(t *testing.T) {
	g := NewGame([]string{"A", "B", "C"}, -1)
	if len(g.Players) != 3 {
		t.Fatalf("ожидалось 3 игрока, получено %d", len(g.Players))
	}
	for _, p := range g.Players {
		if len(p.Hand) != 5 {
			t.Errorf("у игрока %s ожидалось 5 карт, получено %d", p.Name, len(p.Hand))
		}
	}
	if _, ok := g.PeekDiscard(); !ok {
		t.Error("ожидалась вскрытая карта в сбросе после раздачи")
	}
	// 3*5 карт игрокам + 1 в сброс = 16, в колоде должно остаться 36
	if g.Deck.Len() != 52-15-1 {
		t.Errorf("неверное количество карт в колоде: %d", g.Deck.Len())
	}
}

func TestDrawDiscardCycle(t *testing.T) {
	g := NewGame([]string{"A", "B"}, -1)
	handBefore := len(g.current().Hand)

	if _, err := g.DrawFromDeck(); err != nil {
		t.Fatalf("неожиданная ошибка при взятии карты: %v", err)
	}
	if len(g.current().Hand) != handBefore+1 {
		t.Fatalf("рука должна была вырасти на 1 карту")
	}
	if g.Phase != PhaseAction {
		t.Fatalf("после взятия карты ожидалась фаза PhaseAction, получена %v", g.Phase)
	}

	card := g.current().Hand[0]
	if err := g.Discard(card); err != nil {
		t.Fatalf("неожиданная ошибка при сбросе: %v", err)
	}
	if g.CurrentPlayer != 1 {
		t.Fatalf("ход должен был перейти ко второму игроку, текущий: %d", g.CurrentPlayer)
	}
	if g.Phase != PhaseDraw {
		t.Fatalf("после сброса ожидалась фаза PhaseDraw, получена %v", g.Phase)
	}
}

func TestWrongPhaseRejected(t *testing.T) {
	g := NewGame([]string{"A", "B"}, -1)
	// ещё не брали карту — сброс должен быть запрещён
	if err := g.Discard(g.current().Hand[0]); err != ErrWrongPhase {
		t.Errorf("ожидалась ошибка ErrWrongPhase, получено: %v", err)
	}
	// повторное взятие карты в фазе PhaseDraw -> PhaseAction, затем
	// повторный DrawFromDeck должен быть отклонён
	if _, err := g.DrawFromDeck(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if _, err := g.DrawFromDeck(); err != ErrWrongPhase {
		t.Errorf("повторное взятие карты в фазе PhaseAction должно быть запрещено, получено: %v", err)
	}
}

func TestEndByEmptyHandTakesPot(t *testing.T) {
	g := NewGameWithStakes([]string{"A", "B", "C"}, -1, 100, 10, 2)
	// банк уже собран в NewGameWithStakes: 3 игрока * 10 = 30
	if g.Pot != 30 {
		t.Fatalf("ожидался банк 30 после сбора анте, получено %d", g.Pot)
	}
	for _, p := range g.Players {
		if p.Balance != 90 {
			t.Fatalf("ожидался капитал 90 после анте, получено %d у %s", p.Balance, p.Name)
		}
	}

	g.Players[0].Hand = []Card{c(Five, Hearts)}
	g.CurrentPlayer = 0
	g.Phase = PhaseAction

	if err := g.Discard(c(Five, Hearts)); err != nil {
		t.Fatalf("неожиданная ошибка при сбросе: %v", err)
	}
	if g.Phase != PhaseGameOver {
		t.Fatalf("раунд должен был завершиться победой игрока 0")
	}
	if g.Result.Winner != 0 {
		t.Fatalf("ожидался победитель 0, получен %d", g.Result.Winner)
	}
	if g.Result.Pot != 30 {
		t.Fatalf("ожидался банк раунда 30, получено %d", g.Result.Pot)
	}
	// победитель: было 90, получил банк 30 -> 120; итоговое изменение
	// относительно капитала ДО анте (100): +20
	if g.Players[0].Balance != 120 {
		t.Errorf("ожидался капитал победителя 120, получено %d", g.Players[0].Balance)
	}
	if g.Result.BalanceDeltas[0] != 20 {
		t.Errorf("ожидалось изменение капитала +20 у победителя, получено %d", g.Result.BalanceDeltas[0])
	}
	for i := 1; i < 3; i++ {
		if g.Players[i].Balance != 90 {
			t.Errorf("проигравший %d должен остаться с капиталом 90, получено %d", i, g.Players[i].Balance)
		}
		if g.Result.BalanceDeltas[i] != -10 {
			t.Errorf("проигравший %d должен потерять анте (-10), получено %d", i, g.Result.BalanceDeltas[i])
		}
	}
}

func TestDeadRoundRefundsAnte(t *testing.T) {
	g := NewGameWithStakes([]string{"A", "B"}, -1, 100, 10, 2)
	g.endDeadRound()
	if !g.Result.DeadRound {
		t.Fatalf("ожидался DeadRound=true")
	}
	for i, p := range g.Players {
		if p.Balance != 100 {
			t.Errorf("капитал игрока %d должен был вернуться к 100, получено %d", i, p.Balance)
		}
		if g.Result.BalanceDeltas[i] != 0 {
			t.Errorf("изменение капитала при мёртвом раунде должно быть 0, получено %d", g.Result.BalanceDeltas[i])
		}
	}
}

func TestDropSucceedsWhenStrictlyLowest(t *testing.T) {
	g := NewGameWithStakes([]string{"A", "B", "C"}, -1, 100, 10, 2)
	// банк = 30 (3*10), капитал каждого сейчас 90
	g.Players[0].Hand = []Card{c(Two, Clubs)}                      // вес 2 — строго ниже всех
	g.Players[1].Hand = []Card{c(King, Hearts), c(Nine, Diamonds)} // вес 14
	g.Players[2].Hand = []Card{c(Queen, Spades)}                   // вес 10
	g.CurrentPlayer = 0
	g.Phase = PhaseDraw

	res, err := g.Drop()
	if err != nil {
		t.Fatalf("неожиданная ошибка дропа: %v", err)
	}
	if !res.DropCall || res.DropCaught {
		t.Fatalf("ожидался успешный дроп: %+v", res)
	}
	if res.Winner != 0 {
		t.Errorf("ожидался победитель 0, получено %d", res.Winner)
	}
	// объявивший забирает банк целиком (без множителя): 90+30=120,
	// дельта +20 относительно стартовых 100
	if g.Players[0].Balance != 120 {
		t.Errorf("ожидался капитал 120 у объявившего, получено %d", g.Players[0].Balance)
	}
	if g.Players[1].Balance != 90 || g.Players[2].Balance != 90 {
		t.Errorf("остальные должны были просто остаться при своём анте (90), получено %d и %d",
			g.Players[1].Balance, g.Players[2].Balance)
	}
	if res.BalanceDeltas[0] != 20 || res.BalanceDeltas[1] != -10 || res.BalanceDeltas[2] != -10 {
		t.Errorf("неверный расчёт изменения капитала при успешном дропе: %v", res.BalanceDeltas)
	}
}

func TestDropCaughtByLowerOpponent(t *testing.T) {
	g := NewGameWithStakes([]string{"A", "B"}, -1, 100, 10, 2)
	// банк = 20, капитал каждого сейчас 90
	g.Players[0].Hand = []Card{c(King, Hearts)} // вес 4 — думает, что мало
	g.Players[1].Hand = []Card{c(Ace, Clubs)}   // вес 1 — реально ниже
	g.CurrentPlayer = 0
	g.Phase = PhaseDraw

	res, err := g.Drop()
	if err != nil {
		t.Fatalf("неожиданная ошибка дропа: %v", err)
	}
	if !res.DropCall || !res.DropCaught {
		t.Fatalf("ожидался неудачный дроп (попался): %+v", res)
	}
	if res.Winner != 1 {
		t.Errorf("ожидался победитель 1 (реально ниже), получено %d", res.Winner)
	}
	// игрок 1 получает банк (20) плюс штраф с объявившего (анте=10):
	// 90+20+10=120, дельта +20
	if g.Players[1].Balance != 120 {
		t.Errorf("ожидался капитал 120 у поймавшего, получено %d", g.Players[1].Balance)
	}
	// объявивший теряет анте (уже потрачено) и вдобавок платит штраф
	// в размере анте: 90-10=80, дельта -20 относительно стартовых 100
	if g.Players[0].Balance != 80 {
		t.Errorf("ожидался капитал 80 у объявившего (попался), получено %d", g.Players[0].Balance)
	}
	if res.BalanceDeltas[0] != -20 || res.BalanceDeltas[1] != 20 {
		t.Errorf("неверный расчёт изменения капитала при неудачном дропе: %v", res.BalanceDeltas)
	}
}

func TestDropTieWithAnotherPlayerCountsAsCaught(t *testing.T) {
	g := NewGameWithStakes([]string{"A", "B"}, -1, 100, 10, 2)
	g.Players[0].Hand = []Card{c(Ace, Clubs)}  // вес 1
	g.Players[1].Hand = []Card{c(Ace, Hearts)} // тоже вес 1 — ничья не в пользу объявившего
	g.CurrentPlayer = 0
	g.Phase = PhaseDraw

	res, err := g.Drop()
	if err != nil {
		t.Fatalf("неожиданная ошибка дропа: %v", err)
	}
	if !res.DropCaught {
		t.Errorf("при равенстве весов дроп должен считаться неудачным (нужно строго меньше всех)")
	}
	if res.Winner != 1 {
		t.Errorf("ожидался победитель 1, получено %d", res.Winner)
	}
}

func TestDropOnlyAllowedBeforeDrawing(t *testing.T) {
	g := NewGameWithStakes([]string{"A", "B"}, -1, 100, 10, 2)
	g.CurrentPlayer = 0
	g.Phase = PhaseAction // уже взяли карту — дропать поздно

	if _, err := g.Drop(); err != ErrWrongPhase {
		t.Errorf("ожидалась ошибка ErrWrongPhase, получено: %v", err)
	}
}

func TestDropZeroSumAcrossManyPlayers(t *testing.T) {
	g := NewGameWithStakes([]string{"A", "B", "C", "D"}, -1, 100, 7, 2)
	g.Players[0].Hand = []Card{c(Five, Clubs)}
	g.Players[1].Hand = []Card{c(Three, Hearts)}
	g.Players[2].Hand = []Card{c(Three, Spades)} // ничья с игроком 1 за самую низкую
	g.Players[3].Hand = []Card{c(King, Diamonds)}
	g.CurrentPlayer = 0
	g.Phase = PhaseDraw

	res, err := g.Drop()
	if err != nil {
		t.Fatalf("неожиданная ошибка дропа: %v", err)
	}
	sum := 0
	for _, d := range res.BalanceDeltas {
		sum += d
	}
	if sum != 0 {
		t.Errorf("сумма изменений капитала при дропе должна быть 0, получено %d (%v)", sum, res.BalanceDeltas)
	}
}

func TestDeclareTonk(t *testing.T) {
	g := NewGameWithStakes([]string{"A", "B"}, -1, 100, 10, 2)
	// банк = 20 (2 игрока * 10), капитал каждого сейчас 90
	g.Players[0].Hand = []Card{c(Ace, Hearts), c(Two, Clubs)} // вес 3 <= порога
	g.CurrentPlayer = 0
	g.Phase = PhaseAction

	res, err := g.DeclareTonk()
	if err != nil {
		t.Fatalf("неожиданная ошибка объявления Тонка: %v", err)
	}
	if !res.TonkCall || res.Winner != 0 {
		t.Fatalf("ожидался Тонк от игрока 0: %+v", res)
	}
	// банк ×2 = 40; победитель получает 40 (90+40=130, дельта +30
	// относительно стартовых 100); единственный проигравший
	// доплачивает разницу (extra=20) сверх уже отданного анте:
	// 90-20=70, дельта -30 относительно стартовых 100
	if g.Players[0].Balance != 130 {
		t.Errorf("ожидался капитал победителя 130, получено %d", g.Players[0].Balance)
	}
	if g.Players[1].Balance != 70 {
		t.Errorf("ожидался капитал проигравшего 70, получено %d", g.Players[1].Balance)
	}
	if res.BalanceDeltas[0] != 30 || res.BalanceDeltas[1] != -30 {
		t.Errorf("неверный расчёт изменения капитала при Тонке: %v", res.BalanceDeltas)
	}
}

func TestDeclareTonkRejectedAboveThreshold(t *testing.T) {
	g := NewGame([]string{"A", "B"}, -1)
	g.Players[0].Hand = []Card{c(King, Hearts), c(Nine, Clubs)} // вес 19
	g.CurrentPlayer = 0
	g.Phase = PhaseAction

	if _, err := g.DeclareTonk(); err != ErrTonkNotAllowed {
		t.Errorf("ожидалась ошибка ErrTonkNotAllowed, получено: %v", err)
	}
}

func TestLayNewMeldAndAddToMeld(t *testing.T) {
	g := NewGame([]string{"A", "B"}, -1)
	g.Players[0].Hand = []Card{c(Seven, Clubs), c(Seven, Hearts), c(Seven, Spades), c(Two, Diamonds)}
	g.CurrentPlayer = 0
	g.Phase = PhaseAction

	set := []Card{c(Seven, Clubs), c(Seven, Hearts), c(Seven, Spades)}
	if err := g.LayNewMeld(set); err != nil {
		t.Fatalf("неожиданная ошибка выкладки сета: %v", err)
	}
	if len(g.Table) != 1 {
		t.Fatalf("ожидалась 1 комбинация на столе, получено %d", len(g.Table))
	}
	if len(g.current().Hand) != 1 {
		t.Fatalf("после выкладки в руке должна остаться 1 карта, получено %d", len(g.current().Hand))
	}

	// второй игрок подкладывает четвёртую семёрку
	g.Players[1].Hand = []Card{c(Seven, Diamonds)}
	g.CurrentPlayer = 1
	g.Phase = PhaseAction
	if err := g.AddToMeld(0, c(Seven, Diamonds)); err != nil {
		t.Fatalf("неожиданная ошибка подкладки: %v", err)
	}
	if len(g.Table[0].Cards) != 4 {
		t.Errorf("ожидалось 4 карты в комбинации после подкладки, получено %d", len(g.Table[0].Cards))
	}
}

// TestFullBotGames прогоняет много полных партий силами ботов всех
// трёх уровней в разных сочетаниях и проверяет, что игра всегда
// корректно завершается (без паники и без зависания) и что очки
// нулевой суммы (кроме мёртвых раундов).
// allCardsAccountedFor проверяет, что все 52 карты стандартной
// колоды встречаются в игре РОВНО один раз суммарно по: колоде,
// сбросу, рукам игроков и комбинациям на столе. Возвращает список
// описаний найденных нарушений (пустой — всё в порядке).
func allCardsAccountedFor(g *GameState) []string {
	counts := map[Card]int{}
	locations := map[Card][]string{}

	record := func(c Card, where string) {
		counts[c]++
		locations[c] = append(locations[c], where)
	}

	for _, c := range g.Deck.cards {
		record(c, "колода")
	}
	for _, c := range g.DiscardPile {
		record(c, "сброс")
	}
	for i, p := range g.Players {
		for _, c := range p.Hand {
			record(c, fmt.Sprintf("рука игрока %d", i))
		}
	}
	for mi, meld := range g.Table {
		for _, c := range meld.Cards {
			record(c, fmt.Sprintf("комбинация №%d на столе", mi))
		}
	}

	var problems []string
	fullDeck := NewDeck()
	for _, c := range fullDeck.cards {
		switch counts[c] {
		case 1:
		case 0:
			problems = append(problems, fmt.Sprintf("карта %s пропала — нигде не найдена", c))
		default:
			problems = append(problems, fmt.Sprintf("карта %s встречается %d раз(а) — в: %v", c, counts[c], locations[c]))
		}
	}
	return problems
}

// TestCardIntegrityAcrossFullBotGames — по мотивам реального
// наблюдения пользователя (одна и та же карта якобы одновременно
// в сбросе и в комбинации на столе): прогоняет много полных партий
// разными сочетаниями ботов, проверяя после КАЖДОГО хода, что все
// 52 карты стандартной колоды учтены ровно по одному разу — нигде
// не потеряны и нигде не задублированы.
func TestCardIntegrityAcrossFullBotGames(t *testing.T) {
	configs := [][]BotStrategy{
		{SimpleBot{}, SimpleBot{}, SimpleBot{}, SimpleBot{}},
		{MediumBot{}, MediumBot{}, MediumBot{}, MediumBot{}},
		{TonkBot{}, TonkBot{}, TonkBot{}, TonkBot{}},
		{SimpleBot{}, MediumBot{}, TonkBot{}, SimpleBot{}},
	}

	for cfgIdx, bots := range configs {
		for game := 0; game < 50; game++ {
			names := make([]string, len(bots))
			for i := range names {
				names[i] = "Bot"
			}
			g := NewGame(names, -1)

			const maxTurns = 1000
			for turn := 0; turn < maxTurns && g.Phase != PhaseGameOver; turn++ {
				bot := bots[g.CurrentPlayer]
				if err := bot.PlayTurn(g); err != nil {
					t.Fatalf("конфиг %d, игра %d, ход %d: ошибка хода бота: %v", cfgIdx, game, turn, err)
				}
				if problems := allCardsAccountedFor(g); len(problems) > 0 {
					t.Fatalf("конфиг %d, игра %d, ход %d: нарушена целостность колоды:\n%s",
						cfgIdx, game, turn, strings.Join(problems, "\n"))
				}
			}

			if g.Phase == PhaseGameOver {
				g2 := g.NextRound()
				if problems := allCardsAccountedFor(g2); len(problems) > 0 {
					t.Fatalf("конфиг %d, игра %d: нарушена целостность колоды сразу после NextRound():\n%s",
						cfgIdx, game, strings.Join(problems, "\n"))
				}
			}
		}
	}
}

func TestFullBotGames(t *testing.T) {
	configs := [][]BotStrategy{
		{SimpleBot{}, SimpleBot{}, SimpleBot{}, SimpleBot{}},
		{MediumBot{}, MediumBot{}, MediumBot{}, MediumBot{}},
		{TonkBot{}, TonkBot{}, TonkBot{}, TonkBot{}},
		{SimpleBot{}, MediumBot{}, TonkBot{}, SimpleBot{}},
	}

	for cfgIdx, bots := range configs {
		for game := 0; game < 20; game++ {
			names := make([]string, len(bots))
			for i := range names {
				names[i] = "Bot"
			}
			g := NewGame(names, -1)

			const maxTurns = 1000
			turn := 0
			for ; turn < maxTurns && g.Phase != PhaseGameOver; turn++ {
				bot := bots[g.CurrentPlayer]
				if err := bot.PlayTurn(g); err != nil {
					t.Fatalf("конфиг %d, игра %d: ошибка на ходу бота %d: %v",
						cfgIdx, game, g.CurrentPlayer, err)
				}
			}

			if turn >= maxTurns {
				t.Fatalf("конфиг %d, игра %d: партия не завершилась за %d ходов (возможное зависание)",
					cfgIdx, game, maxTurns)
			}
			if g.Result == nil {
				t.Fatalf("конфиг %d, игра %d: партия завершилась без результата", cfgIdx, game)
			}
			sum := 0
			for _, d := range g.Result.BalanceDeltas {
				sum += d
			}
			if sum != 0 {
				t.Errorf("конфиг %d, игра %d: сумма изменений капитала должна быть 0, получено %d (%v)",
					cfgIdx, game, sum, g.Result.BalanceDeltas)
			}
		}
	}
}
