package store

import (
	"context"
	"io/fs"
	"path/filepath"
	"testing"

	"github.com/jmoiron/sqlx"
	"github.com/pressly/goose/v3"
)

// Миграция 00005 переводит существующие `parsed` в `pending`. Статус, который
// поставил код, не различавший частичного разбора, ничего не доказывает: под
// ним лежат и полностью разобранные доставки, и доставки из одних тренировок с
// нулём точек (48 из 99 на живом архиве). Ретеншен, ради которого признак и
// заводится, поверил бы им и срезал тела — а для stateOfMind это необратимо.
func TestMigrationПрежниеParsedСтановятсяPending(t *testing.T) {
	db, err := sqlx.Connect("sqlite", dsn(filepath.Join(t.TempDir(), "healthlog.db")))
	if err != nil {
		t.Fatalf("открытие базы: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })

	sub, err := fs.Sub(migrationsFS, "migrations")
	if err != nil {
		t.Fatalf("миграции: %v", err)
	}
	p, err := goose.NewProvider(goose.DialectSQLite3, db.DB, sub)
	if err != nil {
		t.Fatalf("провайдер: %v", err)
	}

	ctx := context.Background()

	// Состояние ДО этой миграции: схема четвёртой версии.
	if _, err := p.UpTo(ctx, 4); err != nil {
		t.Fatalf("миграция до 4: %v", err)
	}

	const insert = `
		INSERT INTO delivery (id, received_at, automation_name, automation_id,
		    aggregation, period, session_id, bytes, sha256, raw_path,
		    parse_status, points)
		VALUES (?, '2025-06-05T10:00:00Z', '', '', '', '', '', 0, '-', '-', ?, ?)`
	for _, c := range []struct {
		id     string
		status string
		points int
	}{
		{"d-parsed-points", "parsed", 12},
		{"d-parsed-empty", "parsed", 0},
		{"d-failed", "failed", 0},
		{"d-pending", "pending", 0},
	} {
		if _, err := db.ExecContext(ctx, insert, c.id, c.status, c.points); err != nil {
			t.Fatalf("вставка %s: %v", c.id, err)
		}
	}

	if _, err := p.Up(ctx); err != nil {
		t.Fatalf("миграция до последней: %v", err)
	}

	want := map[string]string{
		// Переводятся ОБА варианта parsed, а не только пустой: наблюдение
		// «секции не смешиваются» собрано за двое суток потока, и ставить на
		// него необратимое удаление тел значит повторять ошибку, ради которой
		// задача и заведена.
		"d-parsed-points": "pending",
		"d-parsed-empty":  "pending",
		"d-failed":        "failed",
		"d-pending":       "pending",
	}
	for id, wantStatus := range want {
		var status, sections string
		err := db.QueryRowContext(ctx,
			`SELECT parse_status, uncovered_sections FROM delivery WHERE id = ?`, id).
			Scan(&status, &sections)
		if err != nil {
			t.Fatalf("чтение %s: %v", id, err)
		}
		if status != wantStatus {
			t.Errorf("%s: статус %q, ожидался %q", id, status, wantStatus)
		}
		if sections != "[]" {
			t.Errorf("%s: список %q, ожидался `[]`", id, sections)
		}
	}
}

// Миграция 00007 исполняет правило «покрыли секцию — пересверните»: список
// непокрытых ключей это снимок покрытия на момент свёртки, и доставки,
// свёрнутые до того, как workouts и stateOfMind стали покрытыми, остались бы
// `partial` со старым списком навсегда — а ретеншен вечно щадил бы их тела.
//
// Перевод ТОЧЕЧНЫЙ: доставка, у которой непокрыта только `ecg`, пересворачивать
// нечего, и трогать её значило бы гонять весь архив на каждую новую секцию.
func TestMigrationПокрытыеСекцииВозвращаютсяВОчередь(t *testing.T) {
	db, err := sqlx.Connect("sqlite", dsn(filepath.Join(t.TempDir(), "healthlog.db")))
	if err != nil {
		t.Fatalf("открытие базы: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })

	sub, err := fs.Sub(migrationsFS, "migrations")
	if err != nil {
		t.Fatalf("миграции: %v", err)
	}
	p, err := goose.NewProvider(goose.DialectSQLite3, db.DB, sub)
	if err != nil {
		t.Fatalf("провайдер: %v", err)
	}

	ctx := context.Background()
	if _, err := p.UpTo(ctx, 6); err != nil {
		t.Fatalf("миграция до 6: %v", err)
	}

	const insert = `
		INSERT INTO delivery (id, received_at, automation_name, automation_id,
		    aggregation, period, session_id, bytes, sha256, raw_path,
		    parse_status, points, uncovered_sections)
		VALUES (?, '2025-06-05T10:00:00Z', '', '', '', '', '', 0, '-', '-', ?, 0, ?)`
	cases := []struct {
		id        string
		status    string
		uncovered string
		want      string
	}{
		{"d-workouts", ParsePartial, `["workouts"]`, ParsePending},
		{"d-mind", ParsePartial, `["stateOfMind"]`, ParsePending},
		{"d-both", ParsePartial, `["stateOfMind","workouts"]`, ParsePending},
		{"d-mixed", ParsePartial, `["ecg","workouts"]`, ParsePending},
		// Ничего из ставшего покрытым: трогать нечего.
		{"d-ecg", ParsePartial, `["ecg"]`, ParsePartial},
		// Подстрока имени секции — не имя секции: отбор идёт по элементу
		// массива, иначе чужое тело управляло бы тем, что мы пересворачиваем.
		{"d-lookalike", ParsePartial, `["myworkoutsx"]`, ParsePartial},
		// Статус `failed` возвращает только пересборка, а `parsed` этой
		// миграцией не трогается: у него пустой список непокрытых.
		{"d-failed", ParseFailed, `["workouts"]`, ParseFailed},
		{"d-parsed", ParseDone, `[]`, ParseDone},
	}
	for _, c := range cases {
		if _, err := db.ExecContext(ctx, insert, c.id, c.status, c.uncovered); err != nil {
			t.Fatalf("вставка %s: %v", c.id, err)
		}
	}

	if _, err := p.UpTo(ctx, 7); err != nil {
		t.Fatalf("миграция до 7: %v", err)
	}

	for _, c := range cases {
		var got string
		if err := db.GetContext(ctx, &got, `SELECT parse_status FROM delivery WHERE id = ?`, c.id); err != nil {
			t.Fatalf("чтение %s: %v", c.id, err)
		}
		if got != c.want {
			t.Errorf("%s: статус %q, ожидался %q (непокрытые %s)", c.id, got, c.want, c.uncovered)
		}
	}
}
