package points_test

import (
	"context"
	"encoding/json"
	"log/slog"
	"os"
	"path/filepath"
	"runtime"
	"testing"
	"time"

	"git.vakhrushev.me/av/healthlog/internal/hae"
	"git.vakhrushev.me/av/healthlog/internal/points"
	"git.vakhrushev.me/av/healthlog/internal/store"
)

// ЦЕНА МАРШРУТА ТОЧЕК, ЗАМЕР.
//
// Предела размера ответа у маршрута нет намеренно — его вводит соседняя задача
// `read-api-response-limit`. Поэтому цена обязана быть НАЗВАНА ЧИСЛОМ, и число
// снимается на том режиме, ради которого предел заводится, а не на том, который
// оказался под рукой: прецедент 2026-08-04 (`docs/review.md`) — замер, снятый на
// корпусе, где измеряемого случая не бывает, стоил решения «индекс не нужен».
//
// Три режима, от лёгкого к худшему:
//
//	редкая метрика за год    — «вес за год», сценарий постановки;
//	плотная метрика за сутки — минутный слой, штатный запрос трекера;
//	плотная метрика за неделю в нижнем слое — то, что правило выбора слоя
//	                           отдаёт по умолчанию, когда охваты равны.
//
// Точки размножаются из РЕАЛЬНОГО пакета HAE (`internal/hae/testdata`), а не
// пишутся литералами: форма точки, длина строк и вид числового литерала входят
// в цену — они определяют и объём gzip, и работу разжатия.
//
// Прогон: go test ./internal/points -run XXX -bench . -benchtime 1x
func BenchmarkРядТочек(b *testing.B) {
	cases := []struct {
		name   string
		layer  hae.Layer
		hours  int
		perDay int
	}{
		// Год, взвешивание примерно раз в сутки.
		{"редкая метрика за год", hae.LayerHour, 365 * 24, 1},
		// Сутки минутного слоя: 60 точек в час.
		{"плотная метрика за сутки, minute", hae.LayerMinute, 24, 24 * 60},
		// Неделя нижнего слоя. Порядок взят из разведки: около 100 тысяч
		// координат в сутки на весь поток; на одну плотную метрику — 3600 в час.
		{"плотная метрика за неделю, raw", hae.LayerRaw, 7 * 24, 24 * 3600},
	}

	for _, c := range cases {
		b.Run(c.name, func(b *testing.B) {
			svc, from, to := benchStore(b, c.layer, c.hours, c.perDay)

			var before, after runtime.MemStats
			runtime.GC()
			runtime.ReadMemStats(&before)

			b.ResetTimer()
			var got points.Series
			for range b.N {
				var err error
				got, err = svc.Series(context.Background(), points.Request{
					Metric: "bench_metric", From: from, To: to, Layer: string(c.layer),
				})
				if err != nil {
					b.Fatalf("Series: %v", err)
				}
			}
			b.StopTimer()

			runtime.ReadMemStats(&after)
			b.ReportMetric(float64(len(got.Points)), "точек")
			b.ReportMetric(float64(after.TotalAlloc-before.TotalAlloc)/(1<<20)/float64(b.N), "МиБ_выделено")
		})
	}
}

// benchStore наполняет витрину точками, размноженными из реального пакета HAE.
func benchStore(b *testing.B, layer hae.Layer, hours, perDay int) (*points.Service, time.Time, time.Time) {
	b.Helper()

	raw := realPointBody(b)

	st, err := store.Open(filepath.Join(b.TempDir(), "healthlog.db"))
	if err != nil {
		b.Fatalf("store.Open: %v", err)
	}
	b.Cleanup(func() { _ = st.Close() })

	perHour := max(perDay/24, 1)
	step := time.Hour / time.Duration(perHour)
	start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)

	ctx := context.Background()
	// Пачками по часу: одна транзакция на весь год держала бы блокировку минуты.
	for h := range hours {
		hour := start.Add(time.Duration(h) * time.Hour)
		in := make([]store.IncomingPoint, 0, perHour)
		for i := range perHour {
			at := hour.Add(time.Duration(i) * step)
			in = append(in, store.IncomingPoint{
				Metric: "bench_metric", Layer: string(layer), Units: "count",
				Point: store.Point{Start: at, End: at, OffsetSeconds: 10800, Raw: raw},
			})
		}
		if _, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "bench"}); err != nil {
			b.Fatalf("слияние: %v", err)
		}
	}

	svc := points.New(st, slog.New(slog.DiscardHandler))
	return svc, start, start.Add(time.Duration(hours) * time.Hour)
}

// realPointBody достаёт содержимое настоящей точки из пакета HAE.
//
// Конвенция проекта: тесты формата держим на реальных пакетах. Здесь она нужна
// не ради разбора, а ради ЦЕНЫ — выдуманная точка `{"qty":1}` жмётся иначе и
// разжимается быстрее, чем то, что реально шлёт телефон.
func realPointBody(b *testing.B) json.RawMessage {
	b.Helper()

	body, err := os.ReadFile(filepath.Join("..", "hae", "testdata", "minute.json"))
	if err != nil {
		b.Fatalf("чтение пакета: %v", err)
	}
	var pkg struct {
		Data struct {
			Metrics []struct {
				Data []json.RawMessage `json:"data"`
			} `json:"metrics"`
		} `json:"data"`
	}
	if err := json.Unmarshal(body, &pkg); err != nil {
		b.Fatalf("разбор пакета: %v", err)
	}
	for _, m := range pkg.Data.Metrics {
		if len(m.Data) > 0 {
			return m.Data[0]
		}
	}
	b.Fatal("в пакете нет ни одной точки")
	return nil
}
