package store_test

import (
	"context"
	"errors"
	"fmt"
	"testing"
	"time"

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

// Очередь свёртки — сама таблица, и порядок её обхода это порядок журнала:
// `(received_at, id)`, тот же, в котором проигрывает пересборка.
func TestPendingDeliveriesИдётВПорядкеЖурнала(t *testing.T) {
	t.Parallel()

	st := open(t)
	ctx := context.Background()
	at := ts(t, "2026-08-01T12:00:00Z")

	// Учёт заполняется в порядке, обратном хронологии, и доставки одной секунды
	// различаются только идентификатором.
	seedPending(t, st, "d3", at.Add(time.Second))
	seedPending(t, st, "d2", at)
	seedPending(t, st, "d1", at)

	got, err := st.PendingDeliveries(ctx, store.PendingDelivery{}, 10)
	if err != nil {
		t.Fatalf("PendingDeliveries: %v", err)
	}
	want := []string{"d1", "d2", "d3"}
	if len(got) != len(want) {
		t.Fatalf("выбрано %d доставок, ожидалось %d", len(got), len(want))
	}
	for i, id := range want {
		if got[i].ID != id {
			t.Errorf("на месте %d доставка %q, ожидалась %q", i, got[i].ID, id)
		}
	}
}

// Курсор строго возрастает, и обход им конечен: без этого доставка, у которой
// не удалось записать даже исход разбора, выбиралась бы бесконечно.
func TestPendingDeliveriesКурсорСтрогоВозрастает(t *testing.T) {
	t.Parallel()

	st := open(t)
	ctx := context.Background()
	at := ts(t, "2026-08-01T12:00:00Z")

	seedPending(t, st, "d1", at)
	seedPending(t, st, "d2", at)

	first, err := st.PendingDeliveries(ctx, store.PendingDelivery{}, 1)
	if err != nil {
		t.Fatalf("PendingDeliveries: %v", err)
	}
	if len(first) != 1 || first[0].ID != "d1" {
		t.Fatalf("первая порция %+v", first)
	}

	second, err := st.PendingDeliveries(ctx, first[0], 1)
	if err != nil {
		t.Fatalf("PendingDeliveries: %v", err)
	}
	if len(second) != 1 || second[0].ID != "d2" {
		t.Fatalf("вторая порция %+v", second)
	}

	// Доставка, оставшаяся `pending`, за курсором больше не выбирается — именно
	// на этом стоит завершимость прохода воркера.
	third, err := st.PendingDeliveries(ctx, second[0], 1)
	if err != nil {
		t.Fatalf("PendingDeliveries: %v", err)
	}
	if len(third) != 0 {
		t.Errorf("за последней доставкой выбрано %d строк", len(third))
	}
}

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

	st := open(t)
	ctx := context.Background()
	at := ts(t, "2026-08-01T12:00:00Z")

	seedPending(t, st, "d1", at)
	seedPending(t, st, "d2", at.Add(time.Second))
	if err := st.FinishParse(ctx, "d1", store.ParseOutcome{Status: store.ParseDone}); err != nil {
		t.Fatalf("FinishParse: %v", err)
	}

	n, err := st.CountPendingDeliveries(ctx)
	if err != nil {
		t.Fatalf("CountPendingDeliveries: %v", err)
	}
	if n != 1 {
		t.Errorf("задолженность %d, ожидалась 1", n)
	}

	got, err := st.PendingDeliveries(ctx, store.PendingDelivery{}, 10)
	if err != nil {
		t.Fatalf("PendingDeliveries: %v", err)
	}
	if len(got) != 1 || got[0].ID != "d2" {
		t.Errorf("в очереди %+v, ожидалась только d2", got)
	}
}

// Правило «отказ обстоятельств, а не данных» живёт в одном месте: его читают и
// тот, кто пишет исход разбора, и тот, кто классифицирует этот исход.
func TestTransientРазличаетОбстоятельстваИДанные(t *testing.T) {
	t.Parallel()

	cases := map[string]struct {
		err  error
		want bool
	}{
		"работу прекратили снаружи": {context.Canceled, true},
		"база занята":               {store.ErrBusy, true},
		"база занята, обёрнута":     {fmt.Errorf("слияние: %w", store.ErrBusy), true},
		"не уложились в бюджет":     {context.DeadlineExceeded, false},
		"записи нет":                {store.ErrNotFound, false},
		"прочее":                    {errors.New("диск отвалился"), false},
		"ошибки нет":                {nil, false},
	}

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

			if got := store.Transient(c.err); got != c.want {
				t.Errorf("Transient(%v) = %v, ожидалось %v", c.err, got, c.want)
			}
		})
	}
}

func seedPending(t *testing.T, st *store.Store, id string, at time.Time) {
	t.Helper()

	err := st.CreateDelivery(context.Background(), store.Delivery{
		ID: id, ReceivedAt: at, RawPath: id + ".json.gz",
		SHA256: "-", ParseStatus: store.ParsePending,
	})
	if err != nil {
		t.Fatalf("запись доставки %q: %v", id, err)
	}
}

// Свёртка вне порядка журнала обнаруживается запросом «есть ли позже меня уже
// разобранная доставка». Это единственное наблюдение, по которому расхождение
// живой витрины с пересборкой видно до сверки отпечатков.
func TestParsedAfterВидитТолькоЗаписавшихВВитрину(t *testing.T) {
	t.Parallel()

	st := open(t)
	ctx := context.Background()
	at := ts(t, "2026-08-01T12:00:00Z")

	early := at
	late := at.Add(time.Second)
	seedPending(t, st, "d-early", early)
	seedPending(t, st, "d-late", late)

	// Пока преемница в очереди, расхождения нет.
	found, err := st.ParsedAfter(ctx, early, "d-early")
	if err != nil {
		t.Fatalf("ParsedAfter: %v", err)
	}
	if found {
		t.Error("неразобранная преемница засчитана расхождением")
	}

	// `failed` преемницей не считается: в витрину она ничего не записала, и
	// перестановка относительно неё содержимого не разводит. Исход этот
	// штатный — невыводимый слой, — и его учёт превратил бы WARN в шум.
	if err := st.FinishParse(ctx, "d-late", store.ParseOutcome{Status: store.ParseFailed}); err != nil {
		t.Fatalf("FinishParse: %v", err)
	}
	found, err = st.ParsedAfter(ctx, early, "d-early")
	if err != nil {
		t.Fatalf("ParsedAfter: %v", err)
	}
	if found {
		t.Error("отказавшая преемница засчитана расхождением")
	}

	// А разобранная — считается.
	if err := st.FinishParse(ctx, "d-late", store.ParseOutcome{Status: store.ParseDone}); err != nil {
		t.Fatalf("FinishParse: %v", err)
	}
	found, err = st.ParsedAfter(ctx, early, "d-early")
	if err != nil {
		t.Fatalf("ParsedAfter: %v", err)
	}
	if !found {
		t.Error("разобранная преемница не замечена: свёртка вне порядка журнала осталась бы молчащей")
	}

	// Сама преемница расхождением не является: предикат строго «позже».
	found, err = st.ParsedAfter(ctx, late, "d-late")
	if err != nil {
		t.Fatalf("ParsedAfter: %v", err)
	}
	if found {
		t.Error("доставка засчитала расхождением саму себя")
	}
}
