package main

import (
	"testing"
	"time"
)

func csT(offsetSec int) time.Time {
	base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	return base.Add(time.Duration(offsetSec) * time.Second)
}

func TestNewCorpseStarchGameInitialState(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	if g.Starch != 0 || g.TotalEarned != 0 {
		t.Fatalf("новая партия должна начинаться с нулевого запаса")
	}
	if g.ClickYield != 1 {
		t.Errorf("ожидался базовый доход клика 1, получено %v", g.ClickYield)
	}
	if len(g.ServitorCounts) != len(corpseStarchServitorDefs) {
		t.Errorf("количество счётчиков сервиторов должно совпадать с числом тиров")
	}
}

func TestRequisitionAddsClickYield(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.Requisition(csT(0))
	if g.Starch != 1 || g.TotalEarned != 1 {
		t.Errorf("ожидался запас 1 после одной реквизиции, получено %v/%v", g.Starch, g.TotalEarned)
	}
	g.Requisition(csT(1))
	if g.Starch != 2 {
		t.Errorf("ожидался запас 2 после двух реквизиций, получено %v", g.Starch)
	}
}

func TestTickAccruesPassiveIncome(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.ServitorCounts[0] = 2 // 2 сервочерепа по 0.5/сек = 1/сек
	g.Tick(csT(10))
	if g.Starch != 10 {
		t.Errorf("ожидался запас 10 после 10 секунд по 1/сек, получено %v", g.Starch)
	}
	if g.TotalEarned != 10 {
		t.Errorf("TotalEarned тоже должен был вырасти на 10, получено %v", g.TotalEarned)
	}
}

func TestTickWithNoElapsedTimeDoesNothing(t *testing.T) {
	g := NewCorpseStarchGame(csT(5))
	g.ServitorCounts[0] = 5
	g.Tick(csT(5))
	if g.Starch != 0 {
		t.Errorf("без прошедшего времени доход не должен начисляться, получено %v", g.Starch)
	}
}

func TestSectionsUnlockProgressively(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	if g.ServitorsUnlocked() || g.ShopUnlocked() || g.SumpUnlocked() || g.MissionsUnlocked() {
		t.Fatalf("в начале партии ни один раздел не должен быть открыт")
	}

	g.TotalEarned = corpseStarchUnlockServitors
	if !g.ServitorsUnlocked() {
		t.Errorf("сервиторы должны открыться на пороге %v", corpseStarchUnlockServitors)
	}
	if g.ShopUnlocked() {
		t.Errorf("магазин не должен быть открыт раньше своего порога")
	}

	g.TotalEarned = corpseStarchUnlockMissions
	if !g.ShopUnlocked() || !g.SumpUnlocked() || !g.MissionsUnlocked() {
		t.Errorf("на пороге зачисток все более ранние разделы тоже должны быть открыты")
	}
}

func TestBuyServitorRespectsUnlockAndCost(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.Starch = 5
	if g.BuyServitor(0, csT(0)) {
		t.Fatalf("покупка не должна была пройти при нехватке средств")
	}
	g.Starch = 10
	if !g.BuyServitor(0, csT(0)) {
		t.Fatalf("покупка должна была пройти при ровно достаточных средствах")
	}
	if g.ServitorCounts[0] != 1 {
		t.Errorf("счётчик тира 0 должен был увеличиться")
	}
	if g.Starch != 0 {
		t.Errorf("стоимость должна была списаться полностью, получено %v", g.Starch)
	}

	g.Starch = 1000
	if g.BuyServitor(1, csT(0)) {
		t.Errorf("тир 1 не должен быть доступен без достаточного TotalEarned")
	}
}

func TestServitorCostScalesUp(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	firstCost := g.servitorCost(0)
	g.Starch = firstCost
	g.BuyServitor(0, csT(0))
	secondCost := g.servitorCost(0)
	if secondCost <= firstCost {
		t.Errorf("стоимость следующей покупки должна расти, было %v стало %v", firstCost, secondCost)
	}
}

func TestBuyClickUpgrade(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.Starch = corpseStarchClickUpgradeBaseCost
	before := g.ClickYield
	if !g.BuyClickUpgrade() {
		t.Fatalf("апгрейд должен был пройти при достаточных средствах")
	}
	if g.ClickYield <= before {
		t.Errorf("доход клика должен был вырасти")
	}
	if g.Starch != 0 {
		t.Errorf("стоимость апгрейда должна была списаться полностью")
	}
}

func TestDipChainswordBonusOutcome(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	before := g.Starch
	outcome := g.DipChainsword(csT(0), 0.1)
	if outcome != DipBonusStarch {
		t.Fatalf("ожидался исход DipBonusStarch при малом roll, получено %v", outcome)
	}
	if g.Starch <= before {
		t.Errorf("запас должен был вырасти при бонусном исходе")
	}
}

func TestDipChainswordBlessingGrantsTemporaryMultiplier(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	outcome := g.DipChainsword(csT(0), 0.6)
	if outcome != DipMachineSpiritBlessing {
		t.Fatalf("ожидался исход DipMachineSpiritBlessing, получено %v", outcome)
	}
	if g.currentClickYield(csT(0)) != g.ClickYield*2 {
		t.Errorf("во время благословения доход клика должен удваиваться")
	}
	if g.currentClickYield(csT(31)) != g.ClickYield {
		t.Errorf("после истечения 30 секунд бонус должен был закончиться")
	}
}

func TestDipChainswordNothingOutcome(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	before := g.Starch
	outcome := g.DipChainsword(csT(0), 0.8)
	if outcome != DipNothing {
		t.Fatalf("ожидался исход DipNothing, получено %v", outcome)
	}
	if g.Starch != before {
		t.Errorf("запас не должен был измениться при пустом исходе")
	}
}

func TestDipChainswordCorruptionOutcome(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.Starch = 100
	outcome := g.DipChainsword(csT(0), 0.95)
	if outcome != DipCorruption {
		t.Fatalf("ожидался исход DipCorruption, получено %v", outcome)
	}
	if g.Corruption != 1 {
		t.Errorf("скверна должна была увеличиться на 1")
	}
	if g.Starch >= 100 {
		t.Errorf("запас должен был уменьшиться от порчи, получено %v", g.Starch)
	}
}

func TestWarpBreachTriggersAtCorruptionThreshold(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.Starch = 1000
	g.TotalEarned = 1000
	g.ServitorCounts[0] = 10
	g.ServitorCounts[1] = 4

	// два неудачных окунания — Скверна 1, потом 2, выброс ещё не должен случиться
	g.DipChainsword(csT(0), 0.95)
	g.DipChainsword(csT(1), 0.95)
	if g.Corruption != 2 {
		t.Fatalf("ожидалась Скверна 2 после двух неудач, получено %d", g.Corruption)
	}
	if g.InChaosIncursion(csT(1)) {
		t.Fatalf("вторжение не должно было начаться раньше порога")
	}

	// третье неудачное окунание достигает порога — должен случиться выброс
	starchBefore := g.Starch
	g.DipChainsword(csT(2), 0.95)

	if g.Corruption != 0 {
		t.Errorf("Скверна должна была сброситься в 0 после выброса, получено %d", g.Corruption)
	}
	if !g.InChaosIncursion(csT(2)) {
		t.Fatalf("вторжение хаоситов должно было начаться сразу после выброса")
	}
	// после выброса запас должен был уменьшиться примерно вдвое от
	// того, что было ДО штрафа за само неудачное окунание (10%), —
	// проверяем лишь то, что итог заметно меньше половины исходного
	// запаса до всей цепочки событий, без привязки к точной формуле
	if g.Starch >= starchBefore {
		t.Errorf("запас должен был заметно уменьшиться при выбросе, было %v, стало %v", starchBefore, g.Starch)
	}
	if g.ServitorCounts[0] != 5 || g.ServitorCounts[1] != 2 {
		t.Errorf("количество сервиторов должно было уполовиниться (10->5, 4->2), получено %v", g.ServitorCounts)
	}
}

func TestWarpBreachDoesNotLowerServitorPrice(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.Starch = 1_000_000
	for i := 0; i < 10; i++ {
		g.BuyServitor(0, csT(0))
	}
	priceBefore := g.servitorCost(0)
	if g.ServitorCounts[0] != 10 {
		t.Fatalf("ожидалось 10 купленных сервиторов тира 0, получено %d", g.ServitorCounts[0])
	}

	g.Corruption = corpseStarchCorruptionBreachThreshold - 1
	g.DipChainsword(csT(1), 0.95) // добивает Скверну до порога — варп-выброс, сервиторы уполовиниваются

	if g.ServitorCounts[0] != 5 {
		t.Fatalf("ожидалось уполовинивание до 5, получено %d", g.ServitorCounts[0])
	}
	priceAfter := g.servitorCost(0)
	if priceAfter != priceBefore {
		t.Errorf("цена следующей покупки не должна была измениться от потери инфраструктуры: было %v, стало %v", priceBefore, priceAfter)
	}
}

func TestBuyServitorBlockedDuringIncursion(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.Starch = 1_000_000
	g.ChaosIncursionUntil = csT(100)

	if g.BuyServitor(0, csT(0)) {
		t.Errorf("покупка сервитора не должна была пройти во время вторжения")
	}
	if g.ServitorCounts[0] != 0 {
		t.Errorf("счётчик не должен был увеличиться")
	}
}

func TestBuyServitorWorksAgainAfterIncursionEnds(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.Starch = 1_000_000
	g.ChaosIncursionUntil = csT(100)

	if !g.BuyServitor(0, csT(200)) { // время уже после конца вторжения
		t.Errorf("покупка должна была пройти после окончания вторжения")
	}
}

func TestWarpBreachHalvesServitorsWithFloorDivision(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.ServitorCounts[0] = 7 // нечётное число — проверяем округление вниз
	g.Corruption = corpseStarchCorruptionBreachThreshold - 1
	g.DipChainsword(csT(0), 0.95) // добивает Скверну до порога

	if g.ServitorCounts[0] != 3 {
		t.Errorf("7 сервиторов должны были уполовиниться с округлением вниз до 3, получено %d", g.ServitorCounts[0])
	}
}

func TestChaosIncursionReducesServitorYield(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.ServitorCounts[0] = 10 // 10 * 0.5/сек = 5/сек в обычном режиме
	// ChaosIncursionUntil остаётся нулевым — вторжения нет вообще
	g.Tick(csT(10))
	normalEarn := g.Starch

	g2 := NewCorpseStarchGame(csT(0))
	g2.ServitorCounts[0] = 10
	g2.ChaosIncursionUntil = csT(100) // вторжение идёт весь этот тик (10 < 100)
	g2.Tick(csT(10))

	if g2.Starch >= normalEarn {
		t.Errorf("доход во время вторжения должен быть меньше обычного: обычный %v, во время вторжения %v", normalEarn, g2.Starch)
	}
	if g2.Starch != normalEarn*corpseStarchIncursionYieldMultiplier {
		t.Errorf("ожидался ровно множитель %v от обычного дохода, получено %v (обычный: %v)",
			corpseStarchIncursionYieldMultiplier, g2.Starch, normalEarn)
	}
}

func TestDefendAgainstChaosOnlyWorksDuringIncursion(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	if g.DefendAgainstChaos(csT(0)) {
		t.Errorf("защита не должна была сработать без активного вторжения")
	}
	if g.ChaosDefendClicks != 0 {
		t.Errorf("счётчик защиты не должен был увеличиться вне вторжения")
	}

	g.ChaosIncursionUntil = csT(100)
	if !g.DefendAgainstChaos(csT(0)) {
		t.Errorf("защита должна была сработать во время вторжения")
	}
	if g.ChaosDefendClicks != 1 {
		t.Errorf("ожидался счётчик защиты 1, получено %d", g.ChaosDefendClicks)
	}
}

func TestIncursionResolvesAfterDurationWithoutBonus(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.ChaosIncursionUntil = csT(10)
	g.ChaosDefendClicks = corpseStarchDefendClicksForBonus - 1 // недостаточно для бонуса
	starchBefore := g.Starch

	g.Tick(csT(11)) // время вышло за отметку окончания вторжения

	if g.InChaosIncursion(csT(11)) {
		t.Errorf("вторжение должно было завершиться")
	}
	if !g.ChaosIncursionUntil.IsZero() {
		t.Errorf("ChaosIncursionUntil должен был сброситься в нулевое значение")
	}
	if g.ChaosDefendClicks != 0 {
		t.Errorf("счётчик защиты должен был сброситься после разрешения вторжения")
	}
	if g.Starch != starchBefore {
		t.Errorf("без достаточной защиты бонус не должен был начислиться, было %v, стало %v", starchBefore, g.Starch)
	}
}

func TestIncursionResolvesWithBonusWhenDefendedEnough(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.ChaosIncursionUntil = csT(10)
	g.ChaosDefendClicks = corpseStarchDefendClicksForBonus
	starchBefore := g.Starch

	g.Tick(csT(11))

	if g.Starch != starchBefore+corpseStarchDefendBonusReward {
		t.Errorf("ожидался бонус %v за успешную защиту, было %v, стало %v",
			corpseStarchDefendBonusReward, starchBefore, g.Starch)
	}
}

func TestWarpBreachIncrementsChaosMarks(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	for i := 0; i < 3; i++ {
		g.triggerWarpBreach(csT(i))
	}
	if g.ChaosMarks != 3 {
		t.Errorf("ожидалось 3 метки хаоса после трёх выбросов, получено %d", g.ChaosMarks)
	}
}

func TestChaosOfferPendingAtThreshold(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	for i := 0; i < corpseStarchChaosMarksForOffer-1; i++ {
		g.triggerWarpBreach(csT(i))
	}
	if g.ChaosOfferPending() {
		t.Fatalf("предложение не должно было появиться раньше порога")
	}
	g.triggerWarpBreach(csT(100))
	if !g.ChaosOfferPending() {
		t.Fatalf("предложение должно было появиться по достижении порога меток")
	}
}

func TestDeclineChaosOfferResetsMarksAndGivesReward(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.ChaosMarks = corpseStarchChaosMarksForOffer
	starchBefore := g.Starch

	g.DeclineChaosOffer()

	if g.ChaosMarks != 0 {
		t.Errorf("счётчик меток должен был сброситься в 0, получено %d", g.ChaosMarks)
	}
	if g.Starch != starchBefore+corpseStarchChaosDeclineReward {
		t.Errorf("ожидалась награда %v за отказ, было %v, стало %v", corpseStarchChaosDeclineReward, starchBefore, g.Starch)
	}
	if g.ChaosOfferPending() {
		t.Errorf("предложение не должно оставаться активным после отказа")
	}
}

func TestAcceptChaosOfferStartsRebirth(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.ChaosMarks = corpseStarchChaosMarksForOffer

	g.AcceptChaosOffer(csT(0))

	if !g.InRebirth(csT(0)) {
		t.Fatalf("перерождение должно было начаться сразу после принятия")
	}
	if g.ChaosMarks != 0 {
		t.Errorf("счётчик меток должен был сброситься при принятии, получено %d", g.ChaosMarks)
	}
	if g.Mode != CorpseStarchModeImperial {
		t.Errorf("режим не должен меняться сразу — только по завершении перерождения")
	}
	remaining := g.RebirthRemaining(csT(0))
	if remaining != corpseStarchRebirthDuration {
		t.Errorf("ожидалось %v до конца перерождения, получено %v", corpseStarchRebirthDuration, remaining)
	}
}

func TestRebirthBlocksIncomeEntirely(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.ServitorCounts[0] = 100 // солидный пассивный доход, если бы не блокировка
	g.AcceptChaosOffer(csT(0))
	starchBefore := g.Starch

	g.Tick(csT(3600)) // час прошёл — обычно немалый доход, но идёт перерождение

	if g.Starch != starchBefore {
		t.Errorf("во время перерождения доход не должен начисляться вообще, было %v, стало %v", starchBefore, g.Starch)
	}
	if g.Mode != CorpseStarchModeImperial {
		t.Errorf("перерождение длится сутки — часа недостаточно для завершения")
	}
}

func TestRebirthCompletesAndSwitchesToDemonicMode(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.AcceptChaosOffer(csT(0))

	g.Tick(csT(int(corpseStarchRebirthDuration.Seconds()) + 10)) // чуть больше суток

	if g.InRebirth(csT(int(corpseStarchRebirthDuration.Seconds()) + 10)) {
		t.Errorf("перерождение должно было завершиться")
	}
	if g.Mode != CorpseStarchModeDemonic {
		t.Errorf("режим должен был переключиться на демонический")
	}
	if !g.RebirthUntil.IsZero() {
		t.Errorf("RebirthUntil должен был сброситься в нулевое значение")
	}
}

func TestRebirthResumesIncomeAfterCompletion(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.ServitorCounts[0] = 10 // 5/сек
	g.AcceptChaosOffer(csT(0))

	rebirthEnd := int(corpseStarchRebirthDuration.Seconds())
	g.Tick(csT(rebirthEnd)) // перерождение как раз завершается этим тиком
	starchAfterRebirth := g.Starch

	g.Tick(csT(rebirthEnd + 10)) // ещё 10 секунд уже в демоническом режиме
	if g.Starch <= starchAfterRebirth {
		t.Errorf("доход должен был возобновиться после завершения перерождения")
	}
}

func TestServitorEffectiveNameKeySwitchesWithMode(t *testing.T) {
	def := corpseStarchServitorDefs[0]
	if def.effectiveNameKey(CorpseStarchModeImperial) != def.NameKey {
		t.Errorf("в обычном режиме ожидалось имперское имя")
	}
	if def.effectiveNameKey(CorpseStarchModeDemonic) != def.DemonicNameKey {
		t.Errorf("в демоническом режиме ожидалось демоническое имя")
	}
	if def.DemonicNameKey == "" {
		t.Errorf("демоническое имя не должно быть пустым")
	}
}

func TestMissionEffectiveNameKeySwitchesWithMode(t *testing.T) {
	def := corpseStarchMissionDefs[0]
	if def.effectiveNameKey(CorpseStarchModeImperial) != def.NameKey {
		t.Errorf("в обычном режиме ожидалось имперское имя")
	}
	if def.effectiveNameKey(CorpseStarchModeDemonic) != def.DemonicNameKey {
		t.Errorf("в демоническом режиме ожидалось демоническое имя")
	}
	if def.DemonicNameKey == "" {
		t.Errorf("демоническое имя не должно быть пустым")
	}
}

func TestChaosIncursionRemainingCountdown(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.ChaosIncursionUntil = csT(100)
	remaining := g.ChaosIncursionRemaining(csT(40))
	if remaining != 60*time.Second {
		t.Errorf("ожидалось 60 секунд до конца вторжения, получено %v", remaining)
	}
	if g.ChaosIncursionRemaining(csT(200)) != 0 {
		t.Errorf("после окончания вторжения обратный отсчёт должен быть 0")
	}
}

func TestStartMissionRequiresUnlockAndNoActiveMission(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	if g.StartMission(csT(0), 0) {
		t.Fatalf("миссия не должна была начаться без открытого порога")
	}
	g.TotalEarned = corpseStarchUnlockMissions
	if !g.StartMission(csT(0), 0) {
		t.Fatalf("миссия должна была начаться при открытом пороге")
	}
	if g.StartMission(csT(1), 1) {
		t.Errorf("нельзя начать вторую миссию, пока активна первая")
	}
}

func TestMissionRemainingAndCollect(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	g.TotalEarned = corpseStarchUnlockMissions
	g.StartMission(csT(0), 0) // "vermin", 20 секунд

	if g.MissionRemaining(csT(5)) <= 0 {
		t.Fatalf("через 5 секунд миссия ещё не должна была закончиться")
	}
	if _, ok := g.CollectMission(csT(5), 0.5); ok {
		t.Errorf("нельзя забрать награду до завершения миссии")
	}

	if g.MissionRemaining(csT(25)) != 0 {
		t.Errorf("через 25 секунд миссия (20 сек) должна была завершиться")
	}
	reward, ok := g.CollectMission(csT(25), 0.5)
	if !ok {
		t.Fatalf("награда должна была быть получена после завершения")
	}
	def := corpseStarchMissionDefs[0]
	wantReward := def.RewardMin + 0.5*(def.RewardMax-def.RewardMin)
	if reward != wantReward {
		t.Errorf("ожидалась награда %v, получено %v", wantReward, reward)
	}
	if g.ActiveMission != nil {
		t.Errorf("активная миссия должна была сброситься после получения награды")
	}
	if g.MissionsDone != 1 {
		t.Errorf("счётчик завершённых миссий должен был увеличиться")
	}
}

func TestLogTrimsToLimit(t *testing.T) {
	g := NewCorpseStarchGame(csT(0))
	for i := 0; i < corpseStarchLogLimit+5; i++ {
		g.addLog("test.key")
	}
	if len(g.Log) != corpseStarchLogLimit {
		t.Errorf("лог должен обрезаться до лимита %d, получено %d", corpseStarchLogLimit, len(g.Log))
	}
}
