package main

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

func TestCorpseStarchModelRequisitionViaKey(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	next, _ := m.Update(key("1"))
	m = next.(CorpseStarchModel)
	if m.game.Starch != 1 {
		t.Fatalf("ожидался запас 1 после нажатия [1], получено %v", m.game.Starch)
	}
}

func TestCorpseStarchModelDipActionHiddenDuringIncursion(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.TotalEarned = corpseStarchUnlockSump // открываем отстойник

	m.game.ChaosIncursionUntil = m.game.LastTick.Add(time.Hour)
	during := m.buildActions(m.game.LastTick)

	for _, a := range during {
		if a.Label == T("corpsestarch.action.dip") {
			t.Errorf("окунание меча не должно предлагаться во время вторжения — иначе новая Скверна могла бы перезапустить уже идущее вторжение")
		}
	}
}

func TestCorpseStarchModelDipActionReappearsAfterIncursionEnds(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.TotalEarned = corpseStarchUnlockSump
	m.game.ChaosIncursionUntil = time.Time{} // вторжения нет

	actions := m.buildActions(m.game.LastTick)
	found := false
	for _, a := range actions {
		if a.Label == T("corpsestarch.action.dip") {
			found = true
		}
	}
	if !found {
		t.Errorf("окунание меча должно быть доступно вне вторжения")
	}
}

// TestWarpBreachDuringIncursionCannotBeTriggeredAgain — регрессия:
// раньше окунание меча оставалось доступным даже во время уже
// идущего вторжения, и если бросок снова достигал порога Скверны,
// triggerWarpBreach вызывался повторно поверх уже активного
// вторжения, сбрасывая его таймер (ChaosIncursionUntil) и прогресс
// защиты (ChaosDefendClicks) игрока. Теперь действие недоступно, пока
// идёт вторжение (см. buildActions), так что до самого движка такой
// повторный вызов дойти не может — но зафиксируем и в движке, что
// вызов triggerWarpBreach во время уже идущего вторжения продлевает
// его, а не более тонко сохраняет прогресс, чтобы явно видеть, какое
// именно поведение предотвращает интерфейс.
func TestWarpBreachDuringIncursionResetsProgressAtEngineLevel(t *testing.T) {
	g := NewCorpseStarchGame(time.Now())
	now := g.LastTick
	g.ChaosIncursionUntil = now.Add(time.Second) // вторжение вот-вот закончится
	g.ChaosDefendClicks = 5                      // накопленный прогресс защиты

	g.triggerWarpBreach(now)

	if g.ChaosDefendClicks != 0 {
		t.Errorf("на уровне движка triggerWarpBreach действительно обнуляет прогресс защиты (%d) — поэтому интерфейс и не должен допускать повторный вызов во время вторжения", g.ChaosDefendClicks)
	}
	if !g.ChaosIncursionUntil.After(now.Add(time.Second)) {
		t.Errorf("на уровне движка повторный вызов продлевает вторжение заново — поэтому интерфейс блокирует окунание меча, пока вторжение идёт")
	}
}

func TestCorpseStarchModelBuyServitorActionHiddenDuringIncursion(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.TotalEarned = corpseStarchUnlockServitors // открываем раздел сервиторов

	m.game.ChaosIncursionUntil = m.game.LastTick.Add(time.Hour)
	during := m.buildActions(m.game.LastTick)

	for _, a := range during {
		if strings.Contains(a.Label, T("corpsestarch.servitor.skull")) {
			t.Errorf("покупка сервитора не должна предлагаться во время вторжения")
		}
	}
}

func TestCorpseStarchModelDefendActionAppearsDuringIncursion(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	before := len(m.buildActions(m.game.LastTick))

	m.game.ChaosIncursionUntil = m.game.LastTick.Add(1)
	after := m.buildActions(m.game.LastTick)
	if len(after) != before+1 {
		t.Fatalf("ожидалось на одно действие больше во время вторжения, было %d, стало %d", before, len(after))
	}
	if after[len(after)-1].Label != Tf("corpsestarch.action.defend", 0) {
		t.Errorf("последнее действие должно быть 'отбиваться', получено: %q", after[len(after)-1].Label)
	}
}

func TestCorpseStarchModelDefendActionIncrementsCounterViaKey(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.ChaosIncursionUntil = time.Now().Add(time.Hour)
	actions := m.buildActions(time.Now())
	defendKey := fmt.Sprintf("%d", len(actions))

	next, _ := m.Update(key(defendKey))
	m = next.(CorpseStarchModel)
	if m.game.ChaosDefendClicks != 1 {
		t.Errorf("ожидался счётчик защиты 1 после нажатия, получено %d", m.game.ChaosDefendClicks)
	}
}

func TestCorpseStarchModelWarpBreachVisibleInView(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.Starch = 1000
	m.game.ServitorCounts[0] = 10
	m.game.DipChainsword(m.game.LastTick, 0.95)
	m.game.DipChainsword(m.game.LastTick, 0.95)
	m.game.DipChainsword(m.game.LastTick, 0.95)

	view := m.View()
	normalized := strings.Join(strings.Fields(view), " ")
	wantLog := strings.Join(strings.Fields(T("corpsestarch.log.warp_breach")), " ")
	if !strings.Contains(normalized, wantLog) {
		t.Errorf("экран должен был показать сообщение о варп-выбросе в журнале")
	}
	if !strings.Contains(view, "ВТОРЖЕНИЕ ХАОСА") {
		t.Errorf("экран должен был показать баннер активного вторжения")
	}
	if m.game.ServitorCounts[0] != 5 {
		t.Errorf("сервиторы должны были уполовиниться, получено %d", m.game.ServitorCounts[0])
	}
}

func TestCorpseStarchModelActionsExpandAsUnlocked(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	if len(m.buildActions(time.Now())) != 1 {
		t.Fatalf("в начале должно быть только 1 действие (реквизиция)")
	}

	m.game.TotalEarned = corpseStarchUnlockServitors
	if len(m.buildActions(time.Now())) != 2 {
		t.Fatalf("после открытия сервиторов должно быть 2 действия")
	}

	m.game.TotalEarned = corpseStarchUnlockMissions
	actions := m.buildActions(time.Now())
	if len(actions) != 8 {
		t.Fatalf("ожидалось 8 действий при полностью открытых (кроме 3 тира) разделах, получено %d", len(actions))
	}
}

func TestCorpseStarchModelBuyServitorViaKey(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.TotalEarned = corpseStarchUnlockServitors
	m.game.Starch = 100

	next, _ := m.Update(key("2"))
	m = next.(CorpseStarchModel)
	if m.game.ServitorCounts[0] != 1 {
		t.Fatalf("ожидалась покупка сервочерпа, получено count=%d", m.game.ServitorCounts[0])
	}
}

func TestCorpseStarchModelTickAccruesIncome(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.ServitorCounts[0] = 10
	past := m.game.LastTick.Add(2 * time.Second)

	next, _ := m.Update(corpseStarchTickMsg{At: past})
	m = next.(CorpseStarchModel)
	if m.game.Starch != 10 {
		t.Errorf("ожидался запас 10 после тика на 2 секунды при 5/сек, получено %v", m.game.Starch)
	}
}

func TestCorpseStarchModelResetRequestShowsConfirmation(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	next, _ := m.Update(key("N"))
	m = next.(CorpseStarchModel)
	if !m.confirmingReset {
		t.Fatalf("ожидался запрос подтверждения после нажатия N")
	}
}

func TestCorpseStarchModelResetCancelKeepsProgress(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.Starch = 500
	m.game.TotalEarned = 500

	next, _ := m.Update(key("N"))
	m = next.(CorpseStarchModel)
	next, _ = m.Update(key("n"))
	m = next.(CorpseStarchModel)

	if m.confirmingReset {
		t.Errorf("запрос подтверждения должен был закрыться после отмены")
	}
	if m.game.Starch != 500 {
		t.Errorf("прогресс не должен был сброситься при отмене, получено Starch=%v", m.game.Starch)
	}
}

func TestCorpseStarchModelResetConfirmWipesProgress(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.Starch = 500
	m.game.TotalEarned = 500

	next, _ := m.Update(key("N"))
	m = next.(CorpseStarchModel)
	next, _ = m.Update(key("y"))
	m = next.(CorpseStarchModel)

	if m.confirmingReset {
		t.Errorf("запрос подтверждения должен был закрыться после подтверждения")
	}
	if m.game.Starch != 0 || m.game.TotalEarned != 0 {
		t.Errorf("прогресс должен был сброситься, получено Starch=%v TotalEarned=%v", m.game.Starch, m.game.TotalEarned)
	}
}

func TestCorpseStarchModelChaosOfferAcceptStartsRebirth(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.ChaosMarks = corpseStarchChaosMarksForOffer

	view := m.View()
	if !strings.Contains(view, T("corpsestarch.chaos.offer.title")) {
		t.Fatalf("экран должен был показать предложение Хаоса")
	}

	next, _ := m.Update(key("y"))
	m = next.(CorpseStarchModel)
	if !m.game.InRebirth(m.game.LastTick) && m.game.RebirthUntil.IsZero() {
		t.Errorf("принятие предложения должно было начать перерождение")
	}
}

func TestCorpseStarchModelChaosOfferDeclineResetsMarks(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.ChaosMarks = corpseStarchChaosMarksForOffer
	starchBefore := m.game.Starch

	next, _ := m.Update(key("n"))
	m = next.(CorpseStarchModel)

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

func TestCorpseStarchModelActionsIgnoredDuringChaosOffer(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.ChaosMarks = corpseStarchChaosMarksForOffer
	starchBefore := m.game.Starch

	next, _ := m.Update(key("1")) // обычная реквизиция не должна сработать
	m = next.(CorpseStarchModel)
	if m.game.Starch != starchBefore {
		t.Errorf("обычные действия не должны срабатывать, пока предложение Хаоса не разрешено")
	}
}

func TestCorpseStarchModelRebirthShowsHintForQuitAndNewGame(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.RebirthUntil = m.game.LastTick.Add(time.Hour)

	view := m.View()
	if !strings.Contains(view, "[q]") || !strings.Contains(view, "[N]") {
		t.Errorf("экран перерождения должен был показывать подсказки [q] и [N], получено:\n%s", view)
	}
}

func TestCorpseStarchModelNKeyDuringRebirthOpensConfirmation(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.RebirthUntil = m.game.LastTick.Add(time.Hour)

	next, _ := m.Update(key("N"))
	m = next.(CorpseStarchModel)
	if !m.confirmingReset {
		t.Fatalf("клавиша N во время перерождения должна была открыть подтверждение сброса")
	}

	view := m.View()
	if !strings.Contains(view, T("corpsestarch.confirm_reset")) {
		t.Errorf("экран подтверждения должен был показаться поверх экрана перерождения")
	}
}

func TestCorpseStarchModelConfirmNewGameDuringRebirthActuallyResets(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.RebirthUntil = m.game.LastTick.Add(time.Hour)
	m.game.Starch = 999

	next, _ := m.Update(key("N"))
	m = next.(CorpseStarchModel)
	next, _ = m.Update(key("y"))
	m = next.(CorpseStarchModel)

	if m.confirmingReset {
		t.Errorf("подтверждение должно было закрыться после ответа")
	}
	if m.game.InRebirth(time.Now()) {
		t.Errorf("новая партия не должна была унаследовать перерождение старой")
	}
	if m.game.Starch != 0 {
		t.Errorf("новая партия должна была начаться с чистого счёта, получено %v", m.game.Starch)
	}
}

func TestCorpseStarchModelRebirthBlocksAllActionsExceptQuit(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.RebirthUntil = m.game.LastTick.Add(time.Hour)
	starchBefore := m.game.Starch

	next, _ := m.Update(key("1"))
	m = next.(CorpseStarchModel)
	if m.game.Starch != starchBefore {
		t.Errorf("во время перерождения обычные действия не должны срабатывать")
	}

	view := m.View()
	if !strings.Contains(view, "ПЕРЕРОЖДЕНИЕ") {
		t.Errorf("экран должен был показать статус перерождения")
	}

	next, _ = m.Update(key("q"))
	m = next.(CorpseStarchModel)
	if !m.backToMenu {
		t.Errorf("клавиша q должна была сработать даже во время перерождения (сохранение и выход)")
	}
}

func TestCorpseStarchModelResetActionsIgnoredWhileConfirming(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	starchBefore := m.game.Starch

	next, _ := m.Update(key("N"))
	m = next.(CorpseStarchModel)
	// нажатие "1" (реквизиция) не должно срабатывать, пока ждём подтверждения
	next, _ = m.Update(key("1"))
	m = next.(CorpseStarchModel)
	if m.game.Starch != starchBefore {
		t.Errorf("обычные действия не должны выполняться во время запроса подтверждения")
	}
}

func TestCorpseStarchModelQReturnsToMenu(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	next, _ := m.Update(key("q"))
	m = next.(CorpseStarchModel)
	if !m.backToMenu {
		t.Fatalf("ожидался флаг backToMenu после 'q'")
	}
	if m.quitting {
		t.Fatalf("'q' не должен закрывать всё приложение")
	}
}

func TestCorpseStarchModelMissionFlowViaKeys(t *testing.T) {
	withTempConfigDir(t)
	m := NewCorpseStarchModel()
	m.game.TotalEarned = corpseStarchUnlockMissions
	m.game.Starch = 0

	actions := m.buildActions(time.Now())
	missionActionIdx := -1
	for i, a := range actions {
		if a.Label == Tf("corpsestarch.action.start_mission", T("corpsestarch.mission.vermin"), formatDuration(corpseStarchMissionDefs[0].Duration)) {
			missionActionIdx = i
			break
		}
	}
	if missionActionIdx == -1 {
		t.Fatalf("не нашли действие запуска первой миссии среди %d действий", len(actions))
	}

	next, _ := m.Update(key(string(rune('1' + missionActionIdx))))
	m = next.(CorpseStarchModel)
	if m.game.ActiveMission == nil {
		t.Fatalf("миссия должна была начаться")
	}

	finished := m.game.ActiveMission.StartedAt.Add(corpseStarchMissionDefs[0].Duration + time.Second)
	if m.game.MissionRemaining(finished) != 0 {
		t.Fatalf("миссия должна была завершиться к этому моменту")
	}

	collectActions := m.buildActions(finished)
	collectIdx := -1
	for i, a := range collectActions {
		if a.Label == T("corpsestarch.action.collect_mission") {
			collectIdx = i
			break
		}
	}
	if collectIdx == -1 {
		t.Fatalf("не нашли действие сбора награды")
	}
	collectActions[collectIdx].Handle(&m, finished)
	if m.game.ActiveMission != nil {
		t.Errorf("миссия должна была завершиться после сбора награды")
	}
	if m.game.MissionsDone != 1 {
		t.Errorf("счётчик завершённых миссий должен был увеличиться")
	}
}
