package replay_test

import (
	"context"
	"database/sql"
	"flag"
	"log/slog"
	"path/filepath"
	"testing"

	_ "modernc.org/sqlite" // тот же чистый Go-драйвер, что и у хранилища

	"git.vakhrushev.me/av/healthlog/internal/fold"
	"git.vakhrushev.me/av/healthlog/internal/replay"
)

// Прогон под удерживаемой блокировкой намеренно не входит в `task test` и
// `task gate`: `busy_timeout` — пять секунд, повторов транзакции пять, то есть
// один этот тест стоит около двадцати пяти секунд, а гейт гоняет тесты трижды.
// Запускается командой `task verify:busy`.
var runBusy = flag.Bool("healthlog.busy", false,
	"прогнать свёртку под удерживаемой блокировкой базы (около 25 секунд)")

// Композиция, ради которой заведён барьер: занятость базы откладывает доставку,
// проход прекращается на ней, и живая витрина всё равно совпадает с пересборкой.
//
// Порознь это уже проверено — барьер в `worker_internal_test.go` на подменённой
// свёртке, сходимость в `order_test.go` на журнале без отложенной доставки. Но
// свойство, ради которого изменение существует, — именно композиция: тай-брейк
// «побеждает пришедшая» сделал содержимое витрины функцией порядка свёртки, и
// занятость базы это ровно тот случай, который порядок разводит. Здесь она
// НАСТОЯЩАЯ, а не подменённая: второе соединение держит запись.
func TestBusyОтложеннаяДоставкаНеРазводитПриёмИПересборку(t *testing.T) {
	if !*runBusy {
		t.Skip("прогон под блокировкой выключен: задайте -healthlog.busy")
	}

	ctx := context.Background()
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "live.db")
	arch := openArchive(t, filepath.Join(dir, "raw"))
	src := openStore(t, dbPath)
	log := slog.New(slog.DiscardHandler)

	items := collidingJournal(t, 3)
	for i := range items {
		writeBody(t, arch, src, items[i], bumped(t, items[i].fixture, i))
	}

	w := replay.NewWorker(src, fold.New(arch, src, 0, log), log)

	// Второе соединение держит запись, как её держит свёртка широкой доставки:
	// измерено 11 секунд на 16 тысячах объектов, то есть окно реальное.
	holder, err := sql.Open("sqlite", "file:"+dbPath+"?_pragma=busy_timeout(100)&_txlock=immediate")
	if err != nil {
		t.Fatalf("второе соединение: %v", err)
	}
	defer func() { _ = holder.Close() }()

	tx, err := holder.BeginTx(ctx, nil)
	if err != nil {
		t.Fatalf("удержание записи: %v", err)
	}
	if _, err := tx.Exec(`UPDATE delivery SET points = points`); err != nil {
		t.Fatalf("удержание записи: %v", err)
	}

	out, err := w.Pass(ctx)
	if err != nil {
		t.Fatalf("проход под блокировкой: %v", err)
	}
	if out.Deferred != 1 {
		t.Fatalf("отложенных %d, ожидалась 1: занятость не дошла до классификации, %+v", out.Deferred, out)
	}
	if out.Folded != 0 {
		t.Fatalf("свёрнуто %d при отложенной ГОЛОВЕ очереди: барьер не сработал, %+v", out.Folded, out)
	}

	_ = tx.Rollback()

	// Очередь пошла: те же доставки, тот же порядок журнала.
	out, err = w.Pass(ctx)
	if err != nil {
		t.Fatalf("проход после разблокировки: %v", err)
	}
	if out.Folded != len(items) {
		t.Fatalf("свёрнуто %d из %d: %+v", out.Folded, len(items), out)
	}

	rep, _ := run(t, ctx, arch, src, filepath.Join(dir, "rebuild.db"))
	if got, want := rep.Fingerprint, fingerprint(t, src); got != want {
		t.Errorf("отложенная занятостью доставка развела приём и пересборку:\n  приём      %s\n  пересборка %s",
			want, got)
	}
}
