package replay

import (
	"bytes"
	"context"
	"log/slog"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

// Отложенная доставка ДЕРЖИТ очередь: проход обязан прекратиться на ней, а не
// свернуть её преемниц раньше неё.
//
// Свойство это — единственное, на чём держится равенство «пересборка = приём»
// после смены тай-брейка: при равной полноте побеждает пришедшая доставка, то
// есть исход слияния стал функцией порядка свёртки. Перешагнув отложенную
// доставку, воркер оставил бы в витрине версию не той доставки, что стоит в
// журнале последней, — и расхождение было бы молчаливым.
//
// Свёртка подменяется полем, а не удерживается настоящая блокировка базы:
// `Deferred` наступает от занятости, а удержать её по-настоящему стоит
// двадцать пять секунд (`busy_timeout` 5 с × 5 повторов транзакции). Столько
// стоит `task verify:busy`, который в гейт намеренно не входит, — то есть
// проверка барьера жила бы только в прогоне, который никто не гоняет по
// расписанию.
func TestПроходПрекращаетсяНаОтложеннойДоставке(t *testing.T) {
	t.Parallel()

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

	ctx := context.Background()
	base := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC)
	ids := []string{"01AAAAAAAAAAAAAAAAAAAAAAAA", "01BBBBBBBBBBBBBBBBBBBBBBBB", "01CCCCCCCCCCCCCCCCCCCCCCCC"}
	for i, id := range ids {
		d := store.Delivery{
			ID:          id,
			ReceivedAt:  base.Add(time.Duration(i) * time.Second),
			Bytes:       1,
			SHA256:      "-",
			RawPath:     "x",
			ParseStatus: store.ParsePending,
		}
		if err := st.CreateDelivery(ctx, d); err != nil {
			t.Fatalf("учёт доставки: %v", err)
		}
	}

	w := &Worker{
		store: st,
		log:   slog.New(slog.DiscardHandler),
		wake:  make(chan struct{}, 1),
	}
	var seen []string
	w.fold = func(_ context.Context, id string) Outcome {
		seen = append(seen, id)
		if id == ids[1] {
			return Outcome{Deferred: 1}
		}
		return Outcome{Folded: 1}
	}

	out, err := w.Pass(ctx)
	if err != nil {
		t.Fatalf("проход: %v", err)
	}
	if len(seen) != 2 || seen[0] != ids[0] || seen[1] != ids[1] {
		t.Fatalf("проход тронул %v: он обязан остановиться на второй доставке", seen)
	}
	if out.Folded != 1 || out.Deferred != 1 {
		t.Errorf("исход прохода %+v: ожидались одна свёрнутая и одна отложенная", out)
	}

	// Следующий проход начинает с начала очереди и доходит до преемниц: барьер
	// откладывает работу, а не отменяет её. Порядок остаётся журнальным.
	//
	// Первая доставка появляется в списке снова потому, что подменённая свёртка
	// исхода разбора не пишет и очередь не покидает: проверяется здесь порядок,
	// а не учёт.
	seen = nil
	w.fold = func(_ context.Context, id string) Outcome {
		seen = append(seen, id)
		return Outcome{Folded: 1}
	}
	if _, err := w.Pass(ctx); err != nil {
		t.Fatalf("второй проход: %v", err)
	}
	if len(seen) != 3 || seen[0] != ids[0] || seen[1] != ids[1] || seen[2] != ids[2] {
		t.Fatalf("второй проход тронул %v, ожидался порядок журнала %v", seen, ids)
	}
}

// Метка отставания обязана срабатывать и на выходе по барьеру.
//
// Иначе барьер отменял бы её ровно в том состоянии, ради которого она заведена:
// занятая голова очереди не пропускает проход до пустой выборки НИКОГДА, и
// «очередь стоит два часа» стало бы неотличимо от «споткнулась один раз».
// Дельта capability приёма обещает это дословно: «Стоящая голова очереди
// молчать MUST NOT».
func TestМеткаОтставанияСрабатываетНаБарьере(t *testing.T) {
	t.Parallel()

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

	ctx := context.Background()
	// Доставка ждёт заведомо дольше порога задержки.
	at := store.Now().Add(-time.Hour)
	for i, id := range []string{"01AAAAAAAAAAAAAAAAAAAAAAAA", "01BBBBBBBBBBBBBBBBBBBBBBBB"} {
		d := store.Delivery{
			ID: id, ReceivedAt: at.Add(time.Duration(i) * time.Second),
			Bytes: 1, SHA256: "-", RawPath: "x", ParseStatus: store.ParsePending,
		}
		if err := st.CreateDelivery(ctx, d); err != nil {
			t.Fatalf("учёт доставки: %v", err)
		}
	}

	var buf bytes.Buffer
	w := &Worker{
		store: st,
		log:   slog.New(slog.NewJSONHandler(&buf, nil)),
		wake:  make(chan struct{}, 1),
	}
	// Первый проход уже был: метка задолженности старта подавляется намеренно.
	w.startupDone = true
	w.fold = func(_ context.Context, _ string) Outcome { return Outcome{Deferred: 1} }

	if _, err := w.Pass(ctx); err != nil {
		t.Fatalf("проход: %v", err)
	}
	if !strings.Contains(buf.String(), "deliveries waited for fold") {
		t.Errorf("метка отставания молчит на выходе по барьеру:\n%s", buf.String())
	}
}
