package store_test

import (
	"context"
	"fmt"
	"sync"
	"testing"
	"time"

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

func category(metric, field, value, code string) store.CategoryValue {
	return store.CategoryValue{Metric: metric, Field: field, Value: value, Code: code}
}

func mergeCategories(t *testing.T, st *store.Store, d store.DeliveryRef, vs ...store.CategoryValue) {
	t.Helper()

	if _, err := st.Merge(context.Background(), store.Incoming{Categories: vs}, d); err != nil {
		t.Fatalf("слияние наблюдений: %v", err)
	}
}

func categories(t *testing.T, st *store.Store) []store.CategoryValue {
	t.Helper()

	got, err := st.CategoryValues(context.Background())
	if err != nil {
		t.Fatalf("чтение реестра: %v", err)
	}
	return got
}

func TestMergeCategoriesКладётНаблюдение(t *testing.T) {
	t.Parallel()

	st := open(t)
	d := from(t, "01AAA", "2026-08-03T10:00:00Z")
	mergeCategories(t, st, d,
		category("sleep_analysis", "value", "Во сне", "HKCategoryValueSleepAnalysisAsleepUnspecified"),
		category("heart_rate", "context", "Не задано", ""))

	got := categories(t, st)
	if len(got) != 2 {
		t.Fatalf("строк реестра %d, ожидалось 2: %+v", len(got), got)
	}
	// Порядок детерминирован ключом.
	if got[0].Metric != "heart_rate" || got[1].Metric != "sleep_analysis" {
		t.Errorf("порядок реестра не по ключу: %+v", got)
	}
	// Пустой код — законное состояние: словарь этой строки не знает.
	if got[0].Code != "" {
		t.Errorf("контекст пульса получил код %q", got[0].Code)
	}
	if got[1].Code != "HKCategoryValueSleepAnalysisAsleepUnspecified" {
		t.Errorf("фаза сна получила код %q", got[1].Code)
	}
	if got[1].FirstDeliveryID != d.ID || !got[1].FirstSeen.Equal(d.ReceivedAt) {
		t.Errorf("провенанс %s/%s, ожидался %s/%s",
			got[1].FirstDeliveryID, got[1].FirstSeen, d.ID, d.ReceivedAt)
	}
}

// Повторная свёртка той же доставки не меняет ни одной колонки: иначе состояние
// зависело бы от числа прогонов, а пересборка перестала бы быть no-op.
func TestMergeCategoriesПовторНичегоНеМеняет(t *testing.T) {
	t.Parallel()

	st := open(t)
	d := from(t, "01AAA", "2026-08-03T10:00:00Z")
	v := category("sleep_analysis", "value", "Во сне", "HKCategoryValueSleepAnalysisAsleepUnspecified")

	mergeCategories(t, st, d, v)
	first := fingerprint(t, st)
	before := categories(t, st)

	mergeCategories(t, st, d, v)
	if got := fingerprint(t, st); got != first {
		t.Error("повторная свёртка сдвинула отпечаток")
	}
	after := categories(t, st)
	if len(after) != len(before) || after[0] != before[0] {
		t.Errorf("повтор изменил реестр: было %+v, стало %+v", before, after)
	}
}

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

	deliveries := []store.DeliveryRef{
		from(t, "01AAA", "2026-08-01T10:00:00Z"),
		from(t, "01BBB", "2026-08-02T10:00:00Z"),
		// Та же секунда, что у предыдущей: порядок задаёт идентификатор, и
		// сравнение по одной метке сделало бы исход зависящим от того, какая
		// свернулась раньше.
		from(t, "01AAB", "2026-08-02T10:00:00Z"),
	}
	v := category("sleep_analysis", "value", "Во сне", "HKCategoryValueSleepAnalysisAsleepUnspecified")

	orders := [][]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}}
	var want string
	for _, order := range orders {
		t.Run(fmt.Sprint(order), func(t *testing.T) {
			st := open(t)
			for _, i := range order {
				mergeCategories(t, st, deliveries[i], v)
			}
			got := categories(t, st)
			if len(got) != 1 {
				t.Fatalf("строк реестра %d, ожидалась одна", len(got))
			}
			if got[0].FirstDeliveryID != "01AAA" {
				t.Errorf("провенанс %s, ожидалась самая ранняя доставка 01AAA", got[0].FirstDeliveryID)
			}
			// Отпечаток обязан совпасть у всех шести порядков.
			fp := fingerprint(t, st)
			if want == "" {
				want = fp
			} else if fp != want {
				t.Errorf("порядок %v дал другой отпечаток", order)
			}
		})
	}
}

// Наблюдение в отпечаток входит, выведенный код — нет. Код производен от
// словаря в бинаре, а не от журнала: включённый в отпечаток, он заставил бы
// всякое пополнение словаря давать расхождение при совпавшем журнале.
func TestFingerprintРеестрБезКода(t *testing.T) {
	t.Parallel()

	d := from(t, "01AAA", "2026-08-03T10:00:00Z")

	withCode := open(t)
	mergeCategories(t, withCode, d, category("sleep_analysis", "value", "Во сне", "HKCategoryValueSleepAnalysisAsleepUnspecified"))

	otherCode := open(t)
	mergeCategories(t, otherCode, d, category("sleep_analysis", "value", "Во сне", ""))

	if fingerprint(t, withCode) != fingerprint(t, otherCode) {
		t.Error("расхождение только по коду сдвинуло отпечаток — оракул сходимости стал функцией версии словаря")
	}

	otherValue := open(t)
	mergeCategories(t, otherValue, d, category("sleep_analysis", "value", "Бодрствование", "HKCategoryValueSleepAnalysisAwake"))
	if fingerprint(t, withCode) == fingerprint(t, otherValue) {
		t.Error("разошедшееся наблюдение отпечаток не сдвинуло")
	}
}

// Значение приходит из тела дословно и может содержать что угодно, включая
// признак раздела и разделители полей. Длина впереди каждого поля — то, что не
// даёт двум разным состояниям витрины сойтись отпечатком.
func TestFingerprintРазделительВЗначенииГраницуНеПодделывает(t *testing.T) {
	t.Parallel()

	d := from(t, "01AAA", "2026-08-03T10:00:00Z")

	a := open(t)
	mergeCategories(t, a, d, category("sleep", "value|c|9", "x", ""))

	b := open(t)
	mergeCategories(t, b, d, category("sleep", "value", "|c|9|x", ""))

	if fingerprint(t, a) == fingerprint(t, b) {
		t.Error("разное разбиение тех же байтов по полям дало один отпечаток")
	}
}

// Пустой реестр отпечаток не ломает: раздел, у которого нет строк, не пишет в
// хеш ничего.
func TestFingerprintПустойРеестр(t *testing.T) {
	t.Parallel()

	empty := open(t)
	ctx := context.Background()

	fp, err := empty.Fingerprint(ctx)
	if err != nil {
		t.Fatalf("отпечаток пустой витрины: %v", err)
	}
	if fp == "" {
		t.Error("отпечаток пустой витрины пуст")
	}
	n, err := empty.CountCategoryValues(ctx)
	if err != nil {
		t.Fatalf("счёт реестра: %v", err)
	}
	if n != 0 {
		t.Errorf("строк реестра %d, ожидался ноль", n)
	}
}

// Отказ слияния не оставляет строк реестра: доставка — единица свёртки, и
// частичное состояние повторная свёртка не чинит.
func TestMergeCategoriesОтказНеОставляетСтрок(t *testing.T) {
	t.Parallel()

	st := open(t)
	ctx := context.Background()

	// Точка, содержимое которой не канонизируется, роняет всю транзакцию.
	in := store.Incoming{
		Points:     []store.IncomingPoint{point(t, "m", "hour", "2026-08-03T10:00:00Z", "2026-08-03T10:00:00Z", `{{{`)},
		Categories: []store.CategoryValue{category("sleep_analysis", "value", "Во сне", "код")},
	}
	if _, err := st.Merge(ctx, in, from(t, "01AAA", "2026-08-03T10:00:00Z")); err == nil {
		t.Skip("слияние не отказало — проверять нечего")
	}
	if got := categories(t, st); len(got) != 0 {
		t.Errorf("после отказа в реестре %d строк: %+v", len(got), got)
	}
}

// Доставки приходят внахлёст, а провенанс реестра выбирается правилом
// «минимум по журналу» прямо в SQL. Правило, ни разу не исполненное
// конкурентно, проверено ровно наполовину: у точек такой прогон есть с самого
// начала (TestMergeКонкурентноеСлияниеНеТеряетТочки), у реестра его не было.
//
// Порядок горутин недетерминирован намеренно — в этом весь смысл: исход обязан
// быть функцией МНОЖЕСТВА доставок, а не того, кто первым добрался до
// транзакции.
func TestMergeCategoriesКонкурентноНеПортитПровенанс(t *testing.T) {
	t.Parallel()

	st := open(t)
	ctx := context.Background()

	const writers = 8
	const perWriter = 10

	shared := category("sleep_analysis", "value", "Во сне", "HKCategoryValueSleepAnalysisAsleepUnspecified")
	var wg sync.WaitGroup
	errs := make(chan error, writers)

	for w := range writers {
		wg.Go(func() {
			for i := range perWriter {
				// Метка приёма растёт вместе с номером доставки: самая ранняя —
				// у первой итерации первого писателя, и она обязана победить
				// независимо от того, кто дошёл до базы раньше.
				n := w*perWriter + i
				d := store.DeliveryRef{
					ID:         fmt.Sprintf("d%03d", n),
					ReceivedAt: ts(t, "2026-08-03T10:00:00Z").Add(time.Duration(n) * time.Second),
				}
				// Общий ключ у всех писателей плюс свой собственный: проверяется
				// и спор за одну строку, и параллельная вставка разных.
				own := category("heart_rate", "context", fmt.Sprintf("контекст-%03d", n), "")
				if _, err := st.Merge(ctx, store.Incoming{Categories: []store.CategoryValue{shared, own}}, d); err != nil {
					errs <- err
					return
				}
			}
		})
	}

	wg.Wait()
	close(errs)
	for err := range errs {
		t.Fatalf("конкурентное слияние реестра: %v", err)
	}

	got := categories(t, st)
	if len(got) != writers*perWriter+1 {
		t.Fatalf("строк реестра %d, ожидалось %d", len(got), writers*perWriter+1)
	}
	for _, v := range got {
		if v.Metric != "sleep_analysis" {
			continue
		}
		if v.FirstDeliveryID != "d000" {
			t.Errorf("провенанс общей строки %q, ожидалась самая ранняя доставка d000", v.FirstDeliveryID)
		}
		if !v.FirstSeen.Equal(ts(t, "2026-08-03T10:00:00Z")) {
			t.Errorf("метка первой встречи %s, ожидалась 2026-08-03T10:00:00Z", v.FirstSeen)
		}
	}
}
