package main

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

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

// Приём и свёртка разнесены, но связаны: обработчик отвечает `200`, ничего не
// сворачивая, а фоновый воркер доводит доставку до витрины. Проверяется целиком,
// потому что связь между ними — сигнал, и оборвать его можно, не сломав ни один
// модульный тест.
func TestServeПринимаетИСворачиваетФоном(t *testing.T) {
	dir := t.TempDir()
	cfg := serveConfig(dir)

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

	addrCh := make(chan string, 1)
	done := make(chan error, 1)
	go func() {
		done <- serve(ctx, cfg, slog.New(slog.DiscardHandler), func(addr string) { addrCh <- addr })
	}()

	var addr string
	select {
	case addr = <-addrCh:
	case err := <-done:
		t.Fatalf("сервис не поднялся: %v", err)
	}

	body := strings.NewReader(`{"data":{"metrics":[{"name":"heart_rate","units":"count/min","data":[` +
		`{"date":"2026-07-31 12:00:00 +0300","Min":60,"Avg":62,"Max":65}]}]}}`)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://"+addr+"/api/v1/ingest", body)
	if err != nil {
		t.Fatalf("запрос: %v", err)
	}
	req.Header.Set("automation-aggregation", "Minutes")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("приём: %v", err)
	}
	_ = res.Body.Close()
	if res.StatusCode != http.StatusOK {
		t.Fatalf("статус приёма %d, ожидался 200", res.StatusCode)
	}

	// Сигнал дошёл до воркера, и он довёл доставку до витрины. Опрос, а не сон:
	// снаружи процесса другого шва нет, а сон превратил бы проверку в лотерею.
	waitFolded(t, cfg.Storage.DBPath)

	// Остановка: приём прекращается раньше воркера, воркер выходит сам.
	cancel()
	select {
	case err := <-done:
		if err != nil {
			t.Fatalf("остановка вернула ошибку: %v", err)
		}
	case <-time.After(shutdownTimeout + 10*time.Second):
		t.Fatal("сервис не остановился в бюджет")
	}

	// Инвариант остановки: доставка либо свёрнута целиком, либо числится
	// `pending`; состояния «разобрана, а объектов половина» не существует.
	st, err := store.Open(cfg.Storage.DBPath)
	if err != nil {
		t.Fatalf("база: %v", err)
	}
	defer func() { _ = st.Close() }()

	d, err := st.LastDelivery(context.Background())
	if err != nil {
		t.Fatalf("LastDelivery: %v", err)
	}
	buckets, err := st.CountBuckets(context.Background())
	if err != nil {
		t.Fatalf("CountBuckets: %v", err)
	}
	switch d.ParseStatus {
	case store.ParseDone, store.ParsePartial:
		if buckets == 0 {
			t.Error("доставка числится разобранной, а объектов нет")
		}
	case store.ParsePending:
		if buckets != 0 {
			t.Error("доставка числится неразобранной, а объекты записаны")
		}
	default:
		t.Errorf("parse_status = %q", d.ParseStatus)
	}
}

// waitFolded ждёт, пока фоновый воркер разберёт принятую доставку.
func waitFolded(t *testing.T, dbPath string) {
	t.Helper()

	deadline := time.Now().Add(15 * time.Second)
	for time.Now().Before(deadline) {
		st, err := store.OpenForRead(dbPath)
		if err == nil {
			d, err := st.LastDelivery(context.Background())
			_ = st.Close()
			if err == nil && d.ParseStatus != store.ParsePending {
				if d.ParseStatus != store.ParseDone {
					t.Fatalf("parse_status = %q, ожидался %q", d.ParseStatus, store.ParseDone)
				}
				return
			}
		}
		time.Sleep(10 * time.Millisecond)
	}
	t.Fatal("воркер не свернул доставку: сигнал от приёма не дошёл")
}

func serveConfig(dir string) *config.Config {
	cfg := &config.Config{}
	cfg.Server.Addr = "127.0.0.1:0"
	cfg.Server.ReadTimeout = config.Duration(30 * time.Second)
	cfg.Server.WriteTimeout = config.Duration(30 * time.Second)
	cfg.Storage.DBPath = filepath.Join(dir, "healthlog.db")
	cfg.Storage.ArchiveDir = filepath.Join(dir, "raw")
	cfg.Ingest.MaxBodyMB = 1
	return cfg
}
