package store

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

// План выполнения — единственный оракул требования «каталог не читает
// содержимого объектов». `bucket` объявлена WITHOUT ROWID, то есть строка
// целиком, вместе со сжатым payload, живёт в дереве первичного ключа: обход по
// нему тащил бы страницы содержимого. Проверяется, что обе выборки идут по
// ПОКРЫВАЮЩЕМУ индексу — по нему таблица не открывается вовсе.
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() })

	cases := []struct {
		name string
		sql  string
		args []any
	}{
		{"разрезы метрик", layerRangesQuery, nil},
		{"общие часы двух слоёв", commonHoursQuery,
			[]any{"step_count", "hour", "step_count", "minute", "2026-08-02T00:00:00Z", 48}},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			t.Parallel()

			plan := explain(t, st, c.sql, c.args...)
			t.Logf("план: %s", plan)
			if !strings.Contains(plan, "COVERING INDEX bucket_catalog") {
				t.Errorf("выборка не идёт по покрывающему индексу:\n%s", plan)
			}
			// Обращение к самой таблице выглядит в плане как SCAN/SEARCH bucket
			// без слова INDEX — именно оно и тащило бы страницы содержимого.
			for _, line := range strings.Split(plan, "\n") {
				if strings.Contains(line, "bucket") && !strings.Contains(line, "INDEX") {
					t.Errorf("строка плана читает таблицу, а не индекс: %q", line)
				}
			}
		})
	}
}

func explain(t *testing.T, st *Store, query string, args ...any) string {
	t.Helper()

	rows, err := st.db.QueryContext(context.Background(), "EXPLAIN QUERY PLAN "+query, args...)
	if err != nil {
		t.Fatalf("EXPLAIN QUERY PLAN: %v", err)
	}
	defer func() { _ = rows.Close() }()

	var out []string
	for rows.Next() {
		var id, parent, notused int
		var detail string
		if err := rows.Scan(&id, &parent, &notused, &detail); err != nil {
			t.Fatalf("разбор плана: %v", err)
		}
		out = append(out, detail)
	}
	if err := rows.Err(); err != nil {
		t.Fatalf("EXPLAIN QUERY PLAN: %v", err)
	}
	if len(out) == 0 {
		t.Fatal("план пуст — проверять нечего")
	}
	return strings.Join(out, "\n")
}

// Свойство, ради которого выборка объектов окна вынесена отдельной функцией:
// сколько бы часов ни было в окне, запрос ОДИН. Чтение по объекту за раз давало
// бы под сотню обращений на метрику и столько же снимков витрины.
func TestВыборкаОкнаОстаётсяОднимЗапросом(t *testing.T) {
	t.Parallel()

	for _, hours := range []int{1, 2, 48, 200} {
		q := hourPairsQuery(hours)
		if n := strings.Count(q, ";"); n != 0 {
			t.Errorf("часов %d: в запросе %d разделителей — это уже не один запрос", hours, n)
		}
		// Три подстановки на метрику и слои плюс по одной на час.
		if got, want := strings.Count(q, "?"), hours+3; got != want {
			t.Errorf("часов %d: подстановок %d, ждали %d", hours, got, want)
		}
	}
}
