package main

import "testing"

func dc(rank Rank, suit Suit) Card { return Card{Rank: rank, Suit: suit} }

func TestNewDurakGameDealsSixEach(t *testing.T) {
	g := NewDurakGame([]string{"A", "B", "C"}, -1)
	for i, p := range g.Players {
		if len(p.Hand) != 6 {
			t.Errorf("игрок %d: ожидалось 6 карт, получено %d", i, len(p.Hand))
		}
	}
	// 36 - 18 в руках - 1 козырь на виду = 17 в талоне (козырь ещё
	// формально считается в талоне, так как он его последняя карта)
	if len(g.Talon) != 36-18 {
		t.Fatalf("ожидалось %d карт в талоне, получено %d", 36-18, len(g.Talon))
	}
	if g.Talon[len(g.Talon)-1] != g.TrumpCard {
		t.Fatalf("последняя карта талона должна быть козырной картой")
	}
}

func TestNewDurakGameSixPlayersUnevenDeal(t *testing.T) {
	g := NewDurakGame([]string{"A", "B", "C", "D", "E", "F"}, -1)
	total := 0
	for _, p := range g.Players {
		total += len(p.Hand)
	}
	// 35 карт доступно для раздачи (36 минус зарезервированный
	// козырь), на 6 игроков по 6 не поделится ровно
	if total != 35 {
		t.Fatalf("ожидалось 35 розданных карт при 6 игроках, получено %d", total)
	}
	if len(g.Talon) != 1 {
		t.Fatalf("в талоне должна была остаться только козырная карта, получено %d карт", len(g.Talon))
	}
}

func TestInitialAttackerHasLowestTrump(t *testing.T) {
	g := NewDurakGame([]string{"A", "B", "C"}, -1)

	minTrumpRank := func(hand []Card) (int, bool) {
		best := 99
		found := false
		for _, c := range hand {
			if c.Suit == g.TrumpSuit && durakRankOrder[c.Rank] < best {
				best = durakRankOrder[c.Rank]
				found = true
			}
		}
		return best, found
	}

	attackerMin, attackerHasTrump := minTrumpRank(g.Players[g.AttackerIdx].Hand)
	if !attackerHasTrump {
		return // допускаем случай, когда ни у кого нет козыря
	}
	for i, p := range g.Players {
		if i == g.AttackerIdx {
			continue
		}
		if otherMin, ok := minTrumpRank(p.Hand); ok && otherMin < attackerMin {
			t.Errorf("у игрока %d козырь младше (%d), чем у выбранного атакующего (%d)", i, otherMin, attackerMin)
		}
	}
}

// setupBout — тестовый хелпер: вручную задаёт роли, руки и козырь,
// затем через startNewBout() корректно инициализирует attackCap,
// ThrowOrder и Phase (вместо ручного дублирования этой логики в
// каждом тесте).
func setupBout(g *DurakGameState, attackerIdx, defenderIdx int, trump Suit, hands map[int][]Card) {
	g.AttackerIdx = attackerIdx
	g.DefenderIdx = defenderIdx
	g.TrumpSuit = trump
	for idx, hand := range hands {
		g.Players[idx].Hand = hand
	}
	g.startNewBout()
}

func TestThrowInAndDefendEntersThrowInPhase(t *testing.T) {
	// 3 игрока и запас карт у защищающегося (cap=3), чтобы после
	// первой защиты лимит атаки ещё не был исчерпан и раунд ушёл в
	// фазу подкидывания, а не сразу в отбой
	g := NewDurakGame([]string{"A", "B", "C"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs), dc(Nine, Hearts)},
		1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)},
		2: {dc(Eight, Diamonds)},
	})

	if err := g.ThrowIn(dc(Six, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка подкидывания: %v", err)
	}
	if g.Phase != DurakPhaseDefend {
		t.Fatalf("ожидалась фаза защиты")
	}
	if err := g.Defend(dc(Seven, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка защиты: %v", err)
	}
	if g.Phase != DurakPhaseThrowIn {
		t.Fatalf("после успешной защиты (лимит атаки ещё не исчерпан) ожидалась фаза подкидывания, получено %v", g.Phase)
	}
	if len(g.Table) != 1 || g.Table[0].Defend == nil {
		t.Fatalf("карта на столе должна быть отбита")
	}
	// первый в очереди подкидывающих — атакующий раунда (игрок 0)
	if g.CurrentThrower() != 0 {
		t.Errorf("ожидался первый подкидывающий 0 (атакующий), получено %d", g.CurrentThrower())
	}
}

func TestOtherPlayerCanThrowIn(t *testing.T) {
	// ключевая проверка полноценных правил: подкинуть карту может не
	// только атакующий, но и третий игрок (не защищающийся)
	g := NewDurakGame([]string{"A", "B", "C"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs)},
		1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)},
		2: {dc(Six, Hearts)}, // тот же ранг, что уже на столе
	})
	if err := g.ThrowIn(dc(Six, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Seven, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Phase != DurakPhaseThrowIn {
		t.Fatalf("ожидалась фаза подкидывания")
	}

	// игрок 0 (атакующий) пасует — очередь переходит к игроку 2
	if err := g.Pass(); err != nil {
		t.Fatalf("неожиданная ошибка паса: %v", err)
	}
	if g.CurrentThrower() != 2 {
		t.Fatalf("ожидался подкидывающий 2, получено %d", g.CurrentThrower())
	}

	// игрок 2 (не атакующий и не защищающийся) подкидывает шестёрку червей
	if err := g.ThrowIn(dc(Six, Hearts)); err != nil {
		t.Fatalf("игрок 2 должен был иметь возможность подкинуть карту: %v", err)
	}
	if g.Phase != DurakPhaseDefend {
		t.Fatalf("ожидалась фаза защиты после подкидывания игроком 2")
	}
	if len(g.Table) != 2 {
		t.Fatalf("ожидалось 2 карты на столе, получено %d", len(g.Table))
	}
}

func TestDefendRejectsWeakerCard(t *testing.T) {
	g := NewDurakGame([]string{"A", "B"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Eight, Clubs)},
		1: {dc(Six, Clubs)}, // младше по той же масти
	})
	if err := g.ThrowIn(dc(Eight, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Six, Clubs)); err != ErrDurakInvalidDefend {
		t.Errorf("ожидалась ошибка ErrDurakInvalidDefend, получено: %v", err)
	}
}

func TestTrumpBeatsNonTrump(t *testing.T) {
	g := NewDurakGame([]string{"A", "B"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Ace, Clubs)},
		1: {dc(Six, Spades)}, // младший козырь бьёт туза не в масть
	})
	if err := g.ThrowIn(dc(Ace, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Six, Spades)); err != nil {
		t.Errorf("младший козырь должен побить некозырного туза: %v", err)
	}
}

func TestNonTrumpCannotBeatTrump(t *testing.T) {
	g := NewDurakGame([]string{"A", "B"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Spades)}, // козырная атака
		1: {dc(Ace, Clubs)},
	})
	if err := g.ThrowIn(dc(Six, Spades)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Ace, Clubs)); err != ErrDurakInvalidDefend {
		t.Errorf("некозырная карта не должна бить козырь, получено: %v", err)
	}
}

func TestThrowInRejectsMismatchedRank(t *testing.T) {
	g := NewDurakGame([]string{"A", "B"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs), dc(Nine, Hearts)},
		1: {dc(Seven, Clubs), dc(Ten, Diamonds)}, // 2 карты -> cap=2, не исчерпан после 1 карты
	})

	if err := g.ThrowIn(dc(Six, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Seven, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Phase != DurakPhaseThrowIn {
		t.Fatalf("ожидалась фаза подкидывания, получено %v", g.Phase)
	}
	// девятка не совпадает по рангу ни с чем на столе (там только 6 и 7)
	if err := g.ThrowIn(dc(Nine, Hearts)); err != ErrDurakInvalidThrowIn {
		t.Errorf("ожидалась ошибка ErrDurakInvalidThrowIn, получено: %v", err)
	}
}

func TestHasThrowableCardFalseWithEmptyHand(t *testing.T) {
	g := NewDurakGame([]string{"A", "B", "C"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs)},
		1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)},
		2: {}, // пустая рука — подкидывать нечем чисто физически
	})
	if err := g.ThrowIn(dc(Six, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Seven, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.hasThrowableCard(2) {
		t.Errorf("у игрока с пустой рукой не может быть карты для подкидывания")
	}
}

func TestHasThrowableCardFalseWithNoMatchingRank(t *testing.T) {
	g := NewDurakGame([]string{"A", "B", "C"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs)},
		1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)},
		2: {dc(Eight, Diamonds), dc(Nine, Hearts)}, // ни 8, ни 9 не совпадают с 6/7 на столе
	})
	if err := g.ThrowIn(dc(Six, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Seven, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.hasThrowableCard(2) {
		t.Errorf("ни одна карта в руке не подходит по рангу — подкидывать нечего")
	}
}

func TestHasThrowableCardTrueWithMatchingRank(t *testing.T) {
	g := NewDurakGame([]string{"A", "B", "C"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs)},
		1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)},
		2: {dc(Six, Hearts), dc(Nine, Hearts)}, // шестёрка совпадает по рангу с 6♣ на столе
	})
	if err := g.ThrowIn(dc(Six, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Seven, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if !g.hasThrowableCard(2) {
		t.Errorf("шестёрка в руке должна была засчитаться как подходящая для подкидывания")
	}
}

func TestHasThrowableCardFalseWhenAttackCapReached(t *testing.T) {
	g := NewDurakGame([]string{"A", "B", "C"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs)},
		1: {dc(Seven, Clubs), dc(Jack, Diamonds)},
		2: {dc(Six, Hearts)}, // шестёрка подошла бы по рангу, но лимит атаки уже исчерпан
	})
	defendCard := dc(Seven, Clubs)
	g.Table = []durakTablePair{{Attack: dc(Six, Clubs), Defend: &defendCard}}
	g.attackCap = 1 // имитируем уже достигнутый лимит атаки напрямую
	if g.hasThrowableCard(2) {
		t.Errorf("при достигнутом лимите атаки подкидывать уже некуда, даже если ранг подходит")
	}
}

func TestPassingResolvesBito(t *testing.T) {
	g := NewDurakGame([]string{"A", "B", "C"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs)},
		1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)},
		2: {dc(Eight, Diamonds)}, // не подходит по рангу для подкидывания
	})
	if err := g.ThrowIn(dc(Six, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Seven, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Phase != DurakPhaseThrowIn {
		t.Fatalf("ожидалась фаза подкидывания")
	}

	// оба потенциальных подкидывающих (0 и 2) пасуют по очереди
	if err := g.Pass(); err != nil {
		t.Fatalf("неожиданная ошибка первого паса: %v", err)
	}
	if g.Phase == DurakPhaseGameOver {
		t.Fatalf("партия не должна была завершиться")
	}
	if len(g.Table) == 0 {
		t.Fatalf("после первого паса стол ещё не должен был очиститься (ждём второй пас)")
	}
	if err := g.Pass(); err != nil {
		t.Fatalf("неожиданная ошибка второго паса: %v", err)
	}
	if len(g.Table) != 0 {
		t.Errorf("после второго паса стол должен был очиститься (бито)")
	}
}

func TestAutoResolvesWhenNoOneElseCanThrow(t *testing.T) {
	// 2 игрока: после защиты подкидывать может только сам атакующий;
	// если у него нет карт подходящего ранга — единственный пас в
	// ThrowOrder сразу должен разрешить раунд бито
	g := NewDurakGame([]string{"A", "B"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs), dc(Nine, Hearts)}, // девятка не подходит по рангу
		1: {dc(Seven, Clubs), dc(Ten, Diamonds)},
	})
	if err := g.ThrowIn(dc(Six, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Seven, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Phase != DurakPhaseThrowIn {
		t.Fatalf("ожидалась фаза подкидывания")
	}
	if err := g.Pass(); err != nil {
		t.Fatalf("неожиданная ошибка паса: %v", err)
	}
	if len(g.Table) != 0 {
		t.Errorf("после единственного паса (2 игрока) стол должен был очиститься")
	}
}

func TestBitoRotatesRolesAndRefills(t *testing.T) {
	g := NewDurakGame([]string{"A", "B", "C"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs)},
		1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)},
		2: {dc(Eight, Diamonds)},
	})
	g.Talon = []Card{dc(Two, Hearts), dc(Three, Hearts)} // карты-заглушки, важен только факт добора

	if err := g.ThrowIn(dc(Six, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Seven, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Pass(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Pass(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Phase == DurakPhaseGameOver {
		t.Fatalf("партия не должна была завершиться в этом сценарии, результат: %+v", g.Result)
	}
	if len(g.Table) != 0 {
		t.Errorf("стол должен был очиститься после бито")
	}
	// атакующим должен стать следующий после старого защищающегося (1) -> 2
	if g.AttackerIdx != 2 {
		t.Errorf("ожидался новый атакующий 2, получено %d", g.AttackerIdx)
	}
	if g.DefenderIdx != 0 {
		t.Errorf("ожидался новый защищающийся 0, получено %d", g.DefenderIdx)
	}
	if len(g.Talon) != 0 {
		t.Errorf("талон должен был опустеть, осталось: %v", g.Talon)
	}
}

func TestTakeGivesTableCardsToDefenderAndRotates(t *testing.T) {
	g := NewDurakGame([]string{"A", "B", "C"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Eight, Clubs), dc(Ten, Diamonds)}, // запасная карта, чтобы не выбыть при пустом талоне
		1: {dc(Six, Hearts)},                     // не бьёт и не козырь
		2: {dc(Nine, Diamonds)},
	})
	g.Talon = nil

	if err := g.ThrowIn(dc(Eight, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Take(); err != nil {
		t.Fatalf("неожиданная ошибка взятия: %v", err)
	}
	if g.Phase == DurakPhaseGameOver {
		t.Fatalf("партия не должна была завершиться в этом сценарии, результат: %+v", g.Result)
	}
	wantHand := []Card{dc(Six, Hearts), dc(Eight, Clubs)}
	if len(g.Players[1].Hand) != len(wantHand) {
		t.Fatalf("защищающийся должен был получить карту со стола вдобавок к своей, рука: %v", g.Players[1].Hand)
	}
	for _, c := range wantHand {
		if !hasCard(g.Players[1].Hand, c) {
			t.Errorf("в руке защищающегося должна была быть карта %v, рука: %v", c, g.Players[1].Hand)
		}
	}
	if len(g.Table) != 0 {
		t.Errorf("стол должен был очиститься после взятия")
	}
	// новым атакующим должен стать следующий после защищающегося (1) -> 2
	if g.AttackerIdx != 2 {
		t.Errorf("ожидался новый атакующий 2, получено %d", g.AttackerIdx)
	}
	if g.DefenderIdx != 0 {
		t.Errorf("ожидался новый защищающийся 0, получено %d", g.DefenderIdx)
	}
}

func TestGameOverWhenOnePlayerLeftWithCards(t *testing.T) {
	g := NewDurakGame([]string{"A", "B"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs)},
		1: {dc(Seven, Clubs), dc(Ten, Diamonds)}, // одна карта останется после защиты
	})
	g.Talon = nil

	if err := g.ThrowIn(dc(Six, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Seven, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Pass(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Phase != DurakPhaseGameOver {
		t.Fatalf("игрок 0 избавился от последней карты при пустом талоне — партия должна была завершиться")
	}
	if g.Result == nil || g.Result.NoLoser {
		t.Fatalf("ожидался единственный проигравший (игрок 1), получено: %+v", g.Result)
	}
	if g.Result.Loser != 1 {
		t.Errorf("ожидался проигравший 1 (у него осталась карта), получено %d", g.Result.Loser)
	}
}

func TestAttackCapLimitsThrowIn(t *testing.T) {
	g := NewDurakGame([]string{"A", "B"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs), dc(Six, Hearts), dc(Six, Diamonds)},
		1: {dc(Seven, Clubs)}, // всего 1 карта -> cap = 1
	})
	if g.attackCap != 1 {
		t.Fatalf("ожидался лимит атаки 1, получено %d", g.attackCap)
	}

	if err := g.ThrowIn(dc(Six, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Seven, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	// лимит атаки (1) уже достигнут -> раунд должен был автоматически
	// разрешиться бито, не дожидаясь паса
	if len(g.Table) != 0 {
		t.Errorf("при исчерпанном лимите атаки стол должен был очиститься сразу после защиты")
	}
}

func TestCapReachedResolvesBitoWithoutWaitingForPass(t *testing.T) {
	g := NewDurakGame([]string{"A", "B", "C"}, -1)
	setupBout(g, 0, 1, Spades, map[int][]Card{
		0: {dc(Six, Clubs)},
		1: {dc(Seven, Clubs)}, // 1 карта -> cap=1
		2: {dc(Six, Hearts)},
	})
	if g.attackCap != 1 {
		t.Fatalf("ожидался лимит атаки 1, получено %d", g.attackCap)
	}
	if err := g.ThrowIn(dc(Six, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Defend(dc(Seven, Clubs)); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Phase == DurakPhaseThrowIn {
		t.Fatalf("при исчерпанном лимите атаки фаза подкидывания не должна была наступить")
	}
}
