package archive_test

import (
	"io"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"
	"time"

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

func TestWriteAndOpenRoundTrip(t *testing.T) {
	a := newArchive(t)
	body := []byte(`{"data":{"metrics":[]}}`)
	at := time.Date(2026, 7, 31, 14, 5, 0, 0, time.UTC)

	rel, err := a.Write("01k1abc", at, body)
	if err != nil {
		t.Fatalf("Write: %v", err)
	}

	if want := filepath.Join("2026", "07", "31", "01k1abc.json.gz"); rel != want {
		t.Errorf("путь = %q, ожидался %q", rel, want)
	}

	r, err := a.Open(rel)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer func() { _ = r.Close() }()

	got, err := io.ReadAll(r)
	if err != nil {
		t.Fatalf("чтение: %v", err)
	}
	if string(got) != string(body) {
		t.Errorf("прочитано %q, записано %q", got, body)
	}
}

// Раскладка по дате приёма — в UTC: иначе один и тот же момент попадал бы в
// разные каталоги в зависимости от зоны машины.
func TestWriteUsesUTCDate(t *testing.T) {
	a := newArchive(t)
	// 01:30 по московскому времени — это ещё предыдущие сутки по UTC.
	msk := time.FixedZone("MSK", 3*60*60)
	at := time.Date(2026, 8, 1, 1, 30, 0, 0, msk)

	rel, err := a.Write("01k1def", at, []byte(`{"data":{}}`))
	if err != nil {
		t.Fatalf("Write: %v", err)
	}
	if want := filepath.Join("2026", "07", "31", "01k1def.json.gz"); rel != want {
		t.Errorf("путь = %q, ожидался %q", rel, want)
	}
}

// Временных файлов после успешной записи остаться не должно.
func TestWriteLeavesNoTempFiles(t *testing.T) {
	root := t.TempDir()
	a, err := archive.New(root)
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	if _, err := a.Write("01k1ghi", time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), []byte(`{"data":{}}`)); err != nil {
		t.Fatalf("Write: %v", err)
	}

	err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if strings.HasSuffix(path, ".tmp") {
			t.Errorf("остался временный файл: %s", path)
		}
		return nil
	})
	if err != nil {
		t.Fatalf("обход архива: %v", err)
	}
}

func newArchive(t *testing.T) *archive.Archive {
	t.Helper()
	a, err := archive.New(filepath.Join(t.TempDir(), "raw"))
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	return a
}

// Перечисление тел — вход пересборки. Всё, что телом не является, обязано быть
// посчитано, а не выброшено молча: тело есть, а в отчёте его нет.
func TestListРазводитТелаИПрочиеФайлы(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	a, err := archive.New(root)
	if err != nil {
		t.Fatalf("New: %v", err)
	}

	at := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC)
	for _, id := range []string{"b", "a"} {
		if _, err := a.Write(id, at, []byte(`{"data":{}}`)); err != nil {
			t.Fatalf("Write: %v", err)
		}
	}
	day := filepath.Join(root, "2026", "08", "01")
	for _, name := range []string{"a.json.gz.tmp", "readme.txt"} {
		if err := os.WriteFile(filepath.Join(day, name), []byte("x"), 0o600); err != nil {
			t.Fatalf("подготовка файла: %v", err)
		}
	}

	got, err := a.List()
	if err != nil {
		t.Fatalf("List: %v", err)
	}
	want := []string{
		filepath.Join("2026", "08", "01", "a.json.gz"),
		filepath.Join("2026", "08", "01", "b.json.gz"),
	}
	if !reflect.DeepEqual(got.Bodies, want) {
		t.Errorf("тела %v, ожидались %v", got.Bodies, want)
	}
	if len(got.Skipped) != 2 {
		t.Errorf("пропущено %v, ожидалось два файла", got.Skipped)
	}
	if id := archive.BodyID(got.Bodies[0]); id != "a" {
		t.Errorf("BodyID = %q, ожидался %q", id, "a")
	}
}

// Нечитаемый каталог означает «неизвестно, есть ли тела», а не «тел нет»:
// пустой журнал даёт пустую витрину, которая по отпечатку совпадает с любой
// другой пустой витриной и выглядит идеальной сходимостью.
func TestОтсутствующийКаталогНеЯвляетсяПустымАрхивом(t *testing.T) {
	t.Parallel()

	missing := filepath.Join(t.TempDir(), "нет-такого")
	if _, err := archive.Existing(missing); err == nil {
		t.Error("Existing создал или принял отсутствующий каталог")
	}
	if _, err := os.Stat(missing); err == nil {
		t.Error("Existing создал каталог — пересборке этого делать нельзя")
	}

	// А приёму каталог создать надо: это и есть разница между конструкторами.
	if _, err := archive.New(missing); err != nil {
		t.Errorf("New: %v", err)
	}
	a, err := archive.Existing(missing)
	if err != nil {
		t.Fatalf("Existing после New: %v", err)
	}
	list, err := a.List()
	if err != nil {
		t.Fatalf("List: %v", err)
	}
	if len(list.Bodies) != 0 {
		t.Errorf("в пустом архиве нашлись тела: %v", list.Bodies)
	}
}
