package store

import (
	"context"
	"errors"
	"path/filepath"
	"strings"
	"testing"
)

// Смерть щупа — не гипотеза: `sql.Conn`, закрытый кем угодно, отвечает
// `ErrConnDone` навсегда. Без пересоздания читающий контур остался бы без
// условного запроса до конца жизни процесса; с пересозданием обязано смениться
// и поколение — счётчик у нового соединения начинается заново, и старая метка
// совпала бы с новой на другом состоянии витрины.
func TestЩупПересоздаётсяСНовымПоколением(t *testing.T) {
	t.Parallel()

	st, err := Open(filepath.Join(t.TempDir(), "healthlog.db"))
	if err != nil {
		t.Fatalf("открытие базы: %v", err)
	}
	t.Cleanup(func() { _ = st.Close() })

	before, err := st.StateVersion(t.Context())
	if err != nil {
		t.Fatalf("версия витрины: %v", err)
	}

	// Ровно то, что делает непригодным настоящее соединение: явное закрытие.
	st.probe.mu.Lock()
	_ = st.probe.conn.Close()
	st.probe.mu.Unlock()

	after, err := st.StateVersion(t.Context())
	if err != nil {
		t.Fatalf("версия после смерти щупа: %v", err)
	}
	if generationOf(before) == generationOf(after) {
		t.Errorf("поколение переиспользовано: %q → %q", before, after)
	}
}

func generationOf(version string) string {
	return strings.SplitN(version, "-", 2)[0]
}

// Отказ пробы — не отказ чтения: маршрут обязан деградировать до полного
// ответа, а не до `500`. Ветка исполняется только когда проба не удалась, а
// чтение прошло, и другого способа туда попасть нет.
func TestVersionedReadПриОтказеПробыОтдаётЧтениеБезВерсии(t *testing.T) {
	t.Parallel()

	st, err := Open(filepath.Join(t.TempDir(), "healthlog.db"))
	if err != nil {
		t.Fatalf("открытие базы: %v", err)
	}
	t.Cleanup(func() { _ = st.Close() })

	// Щуп закрыт, а база — нет: проба отказывает, чтение проходит.
	st.closeProbe()

	read := false
	version, err := st.VersionedRead(t.Context(), func(context.Context) error {
		read = true
		return nil
	})
	if err != nil {
		t.Fatalf("чтение подменено отказом пробы: %v", err)
	}
	if !read {
		t.Error("чтение не выполнено")
	}
	if version != "" {
		t.Errorf("ответ подписан версией %q, хотя проба отказала", version)
	}
}

// Пул закрыт, а флаг «закрыто» не взведён: щупа нет и завести его нечем.
// Отдельная ветка от ErrClosed, и она обязана быть отказом, а не пустой
// версией — иначе отказ базы выглядел бы как «версии нет».
func TestВерсияОтказываетКогдаПулЗакрыт(t *testing.T) {
	t.Parallel()

	st, err := Open(filepath.Join(t.TempDir(), "healthlog.db"))
	if err != nil {
		t.Fatalf("открытие базы: %v", err)
	}
	if err := st.db.Close(); err != nil {
		t.Fatalf("закрытие пула: %v", err)
	}

	if _, err := st.StateVersion(t.Context()); err == nil {
		t.Error("версия снята с закрытого пула")
	}
}

// Запрос версии против закрытия хранилища: ровно та гонка, ради которой заведён
// флаг «закрыто». Проверяется под `-race`; исход законен любой, кроме
// воскресшего соединения — его ловит требование «после Close версия отказывает».
func TestВерсияПротивЗакрытия(t *testing.T) {
	t.Parallel()

	st, err := Open(filepath.Join(t.TempDir(), "healthlog.db"))
	if err != nil {
		t.Fatalf("открытие базы: %v", err)
	}

	const n = 8
	done := make(chan struct{}, n)
	for range n {
		go func() {
			_, _ = st.StateVersion(context.Background())
			done <- struct{}{}
		}()
	}
	_ = st.Close()
	for range n {
		<-done
	}

	if _, err := st.StateVersion(context.Background()); !errors.Is(err, ErrClosed) {
		t.Errorf("после закрытия версия отвечает %v, ждали %v", err, ErrClosed)
	}
}

// Оборванный клиентом запрос — обычное событие, и смертью щупа он быть не
// имеет права: иначе каждый такой обрыв менял бы поколение и обнулял метки всех
// потребителей, то есть механизм схлопывался бы под нагрузкой.
func TestОборванныйЗапросНеМеняетПоколение(t *testing.T) {
	t.Parallel()

	st, err := Open(filepath.Join(t.TempDir(), "healthlog.db"))
	if err != nil {
		t.Fatalf("открытие базы: %v", err)
	}
	t.Cleanup(func() { _ = st.Close() })

	before, err := st.StateVersion(t.Context())
	if err != nil {
		t.Fatalf("версия витрины: %v", err)
	}

	dead, cancel := context.WithCancel(context.Background())
	cancel()
	if _, err := st.StateVersion(dead); err == nil {
		t.Fatal("запрос на отменённом контексте прошёл — тест проверяет не то")
	}

	after, err := st.StateVersion(t.Context())
	if err != nil {
		t.Fatalf("версия после обрыва: %v", err)
	}
	if generationOf(before) != generationOf(after) {
		t.Errorf("обрыв запроса сменил поколение: %q → %q", before, after)
	}
}

// Предел файла журнала — строка в DSN, и её пропажу не заметит ни один тест
// поведения: журнал просто останется на пике навсегда.
func TestПределЖурналаЗадан(t *testing.T) {
	t.Parallel()

	st, err := Open(filepath.Join(t.TempDir(), "healthlog.db"))
	if err != nil {
		t.Fatalf("открытие базы: %v", err)
	}
	t.Cleanup(func() { _ = st.Close() })

	var limit int64
	if err := st.db.QueryRowContext(t.Context(), "PRAGMA journal_size_limit").Scan(&limit); err != nil {
		t.Fatalf("предел журнала: %v", err)
	}
	if limit != journalSizeLimit {
		t.Errorf("предел журнала %d, ждали %d", limit, journalSizeLimit)
	}
}
