package fold

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

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

// Внутренний тест, а не через `Fold`, и это осознанно. Сломать сверку снаружи
// больше нечем: испорченную учётную запись запрос теперь пропускает, а
// единственная настоящая причина отказа — отмена контекста и занятость базы —
// в полном проходе свёртки утащила бы за собой и слияние, то есть проверялась
// бы уже другая ветвь. Заводить интерфейс хранилища ради мока дороже: он
// пережил бы тест и остался бы в коде навсегда.
func TestNoveltyОтменённыйКонтекстНеМолчит(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() })

	s := &Service{store: st, log: slog.New(slog.DiscardHandler)}

	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	got := s.novelty(ctx, []string{"ecg", "symptoms"}, store.DeliveryRef{
		ID:         "d1",
		ReceivedAt: time.Now().UTC(),
	})

	if !got.unknown {
		t.Fatal("сверка на отменённом контексте объявила себя состоявшейся")
	}
	if len(got.fresh) != 2 {
		t.Errorf("новыми объявлены %v, ожидались все имена доставки", got.fresh)
	}
	if got.cause == nil {
		t.Error("причина отказа потеряна: занятость базы и вечную порчу колонки по признаку не различить")
	}
}

// Пустой список сверку не берёт вовсе: на живом потоке все три приезжающие
// секции покрыты, и запрос по журналу в штатном режиме не стоит ничего.
func TestNoveltyПустойСписокЗапросаНеБерёт(t *testing.T) {
	t.Parallel()

	s := &Service{store: nil, log: slog.New(slog.DiscardHandler)}

	// Хранилище nil: возьмись запрос — тест упал бы паникой.
	got := s.novelty(context.Background(), nil, store.DeliveryRef{ID: "d1"})
	if got.unknown || len(got.fresh) != 0 {
		t.Errorf("на пустом списке сверка что-то решила: %+v", got)
	}
}

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

	var buf bytes.Buffer
	s := &Service{log: slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))}

	s.logResult(context.Background(), "d1", Stats{
		Uncovered:            []string{"ecg"},
		UncoveredNew:         []string{"ecg"},
		UncoveredSeenUnknown: true,
		UncoveredSeenError:   errors.New("журнал недоступен"),
	})

	var rec map[string]any
	line := strings.TrimSpace(buf.String())
	if err := json.Unmarshal([]byte(line), &rec); err != nil {
		t.Fatalf("запись не разбирается: %v", err)
	}
	if rec["uncovered_seen_unknown"] != true {
		t.Error("признак несостоявшейся сверки не выставлен")
	}
	if rec["uncovered_seen_error"] != "журнал недоступен" {
		t.Errorf("причина в записи: %v", rec["uncovered_seen_error"])
	}
	if rec["level"] != "WARN" {
		t.Errorf("уровень %v, ожидался WARN: имена объявлены новыми", rec["level"])
	}
}

// Запись об отказе несёт и признак несостоявшейся сверки, и причину: у отказа
// разбора та же цена молчания, что у успешного пути — имя уже в учёте.
func TestFailПечатаетНесостоявшуюсяСверку(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()
	err = st.CreateDelivery(ctx, store.Delivery{
		ID: "d1", ReceivedAt: time.Now().UTC(), RawPath: "d1.json.gz",
		SHA256: "-", ParseStatus: store.ParsePending,
	})
	if err != nil {
		t.Fatalf("учёт доставки: %v", err)
	}

	var buf bytes.Buffer
	s := &Service{
		store: st,
		log:   slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})),
	}

	s.fail(ctx, "d1", errors.New("непонятое содержимое"), parseResidue{
		uncovered: []string{"ecg"},
		novelty:   sectionNovelty{fresh: []string{"ecg"}, unknown: true, cause: errors.New("журнал недоступен")},
	})

	out := buf.String()
	for _, want := range []string{"uncovered_new", "ecg", "uncovered_seen_unknown", "журнал недоступен"} {
		if !strings.Contains(out, want) {
			t.Errorf("в записи об отказе нет %q:\n%s", want, out)
		}
	}
}
