package main

import (
	"os"
	"testing"
)

// withTempConfigDir подменяет XDG_CONFIG_HOME на временную — так
// os.UserConfigDir() на Linux/macOS будет смотреть в неё, и тесты
// не трогают реальный конфиг пользователя.
func withTempConfigDir(t *testing.T) {
	t.Helper()
	dir := t.TempDir()
	oldXDG, hadXDG := os.LookupEnv("XDG_CONFIG_HOME")
	os.Setenv("XDG_CONFIG_HOME", dir)
	t.Cleanup(func() {
		if hadXDG {
			os.Setenv("XDG_CONFIG_HOME", oldXDG)
		} else {
			os.Unsetenv("XDG_CONFIG_HOME")
		}
	})
}

func TestLoadCorpseStarchGameWithNoSaveFile(t *testing.T) {
	withTempConfigDir(t)
	_, err := LoadCorpseStarchGame()
	if err != ErrCorpseStarchNoSave {
		t.Fatalf("ожидалась ErrCorpseStarchNoSave при отсутствии файла, получено: %v", err)
	}
}

func TestSaveAndLoadRoundTrip(t *testing.T) {
	withTempConfigDir(t)
	g := NewCorpseStarchGame(csT(0))
	g.Starch = 123.5
	g.TotalEarned = 999
	g.ClickYield = 4
	g.ClickUpgrades = 3
	g.ServitorCounts[0] = 5
	g.ServitorCounts[1] = 2
	g.Corruption = 2
	g.MissionsDone = 1
	g.Log = []string{"a", "b", "c"}

	if err := SaveCorpseStarchGame(g); err != nil {
		t.Fatalf("неожиданная ошибка сохранения: %v", err)
	}

	loaded, err := LoadCorpseStarchGame()
	if err != nil {
		t.Fatalf("неожиданная ошибка загрузки: %v", err)
	}
	if loaded.Starch != g.Starch || loaded.TotalEarned != g.TotalEarned {
		t.Errorf("запас/накопленное не совпадают после загрузки: %+v vs %+v", loaded, g)
	}
	if loaded.ClickYield != g.ClickYield || loaded.ClickUpgrades != g.ClickUpgrades {
		t.Errorf("параметры клика не совпадают после загрузки")
	}
	if loaded.ServitorCounts[0] != 5 || loaded.ServitorCounts[1] != 2 {
		t.Errorf("счётчики сервиторов не совпадают после загрузки: %v", loaded.ServitorCounts)
	}
	if loaded.Corruption != 2 || loaded.MissionsDone != 1 {
		t.Errorf("скверна/завершённые миссии не совпадают после загрузки")
	}
	if len(loaded.Log) != 3 {
		t.Errorf("журнал должен был сохраниться полностью, получено %v", loaded.Log)
	}
}

func TestSaveAndLoadPreservesActiveMission(t *testing.T) {
	withTempConfigDir(t)
	g := NewCorpseStarchGame(csT(0))
	g.TotalEarned = corpseStarchUnlockMissions
	g.StartMission(csT(100), 1)

	if err := SaveCorpseStarchGame(g); err != nil {
		t.Fatalf("неожиданная ошибка сохранения: %v", err)
	}
	loaded, err := LoadCorpseStarchGame()
	if err != nil {
		t.Fatalf("неожиданная ошибка загрузки: %v", err)
	}
	if loaded.ActiveMission == nil {
		t.Fatalf("активная миссия должна была сохраниться")
	}
	if loaded.ActiveMission.DefIdx != 1 {
		t.Errorf("ожидался индекс миссии 1, получено %d", loaded.ActiveMission.DefIdx)
	}
	if !loaded.ActiveMission.StartedAt.Equal(csT(100)) {
		t.Errorf("время начала миссии должно было сохраниться точно, получено %v", loaded.ActiveMission.StartedAt)
	}
}

// TestOfflineProgressAccruesOnLoad проверяет ключевое ожидаемое
// поведение: если между сохранением и загрузкой прошло реальное
// время, пассивный доход от сервиторов должен по-честному
// начислиться при первом Tick после загрузки — ровно так же, как
// если бы игрок всё это время не отходил от экрана.
func TestOfflineProgressAccruesOnLoad(t *testing.T) {
	withTempConfigDir(t)
	g := NewCorpseStarchGame(csT(0))
	g.ServitorCounts[0] = 10 // 10 * 0.5/сек = 5/сек
	if err := SaveCorpseStarchGame(g); err != nil {
		t.Fatalf("неожиданная ошибка сохранения: %v", err)
	}

	loaded, err := LoadCorpseStarchGame()
	if err != nil {
		t.Fatalf("неожиданная ошибка загрузки: %v", err)
	}
	loaded.Tick(csT(100))
	if loaded.Starch != 500 {
		t.Errorf("ожидался начисленный доход за 100 секунд отсутствия (500), получено %v", loaded.Starch)
	}
}

func TestDeleteCorpseStarchSave(t *testing.T) {
	withTempConfigDir(t)
	g := NewCorpseStarchGame(csT(0))
	if err := SaveCorpseStarchGame(g); err != nil {
		t.Fatalf("неожиданная ошибка сохранения: %v", err)
	}
	if err := DeleteCorpseStarchSave(); err != nil {
		t.Fatalf("неожиданная ошибка удаления: %v", err)
	}
	if _, err := LoadCorpseStarchGame(); err != ErrCorpseStarchNoSave {
		t.Errorf("после удаления файла загрузка должна вернуть ErrCorpseStarchNoSave, получено: %v", err)
	}

	if err := DeleteCorpseStarchSave(); err != nil {
		t.Errorf("повторное удаление отсутствующего файла не должно быть ошибкой, получено: %v", err)
	}
}

func TestLoadCorruptedSaveFileReturnsError(t *testing.T) {
	withTempConfigDir(t)
	path, err := corpseStarchSavePath()
	if err != nil {
		t.Fatalf("неожиданная ошибка получения пути: %v", err)
	}
	dir := path[:len(path)-len("/corpsestarch_save.json")]
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatalf("неожиданная ошибка создания директории: %v", err)
	}
	if err := os.WriteFile(path, []byte("это не json"), 0o644); err != nil {
		t.Fatalf("неожиданная ошибка записи повреждённого файла: %v", err)
	}

	if _, err := LoadCorpseStarchGame(); err == nil {
		t.Fatalf("ожидалась ошибка при загрузке повреждённого файла")
	}
}

func TestSaveOverwritesPreviousSave(t *testing.T) {
	withTempConfigDir(t)
	g1 := NewCorpseStarchGame(csT(0))
	g1.Starch = 10
	SaveCorpseStarchGame(g1)

	g2 := NewCorpseStarchGame(csT(0))
	g2.Starch = 999
	SaveCorpseStarchGame(g2)

	loaded, err := LoadCorpseStarchGame()
	if err != nil {
		t.Fatalf("неожиданная ошибка загрузки: %v", err)
	}
	if loaded.Starch != 999 {
		t.Errorf("повторное сохранение должно было перезаписать файл, получено %v", loaded.Starch)
	}
}
