package store

import (
	"path/filepath"
	"sync"
	"testing"
)

func openTemp(t *testing.T) (*Store, string) {
	t.Helper()
	path := filepath.Join(t.TempDir(), "test.db")
	s, err := Open(path)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { s.Close() })
	return s, path
}

func mustSnapshotAndJob(t *testing.T, s *Store) *Job {
	t.Helper()
	if err := s.UpsertSnapshot("snap1", "brief1", `{}`); err != nil {
		t.Fatal(err)
	}
	job, err := s.EnsureJob("book", 1, "draft", "snap1")
	if err != nil {
		t.Fatal(err)
	}
	return job
}

func TestReserveSettleLifecycle(t *testing.T) {
	s, _ := openTemp(t)
	job := mustSnapshotAndJob(t, s)
	caps := Ceilings{BookUSD: 1.0, DayUSD: 2.0}

	res, verdict, err := s.Reserve("book", 0.10, caps)
	if err != nil || verdict != ReserveOK {
		t.Fatalf("reserve: %v %v", verdict, err)
	}
	committed, reserved, _ := s.SpentUSD("book")
	if committed != 0 || reserved != 0.10 {
		t.Fatalf("after reserve: committed=%v reserved=%v", committed, reserved)
	}

	cp := Checkpoint{RequestHash: "h1", JobID: job.ID, Stage: "draft", Role: "translator",
		ModelRequested: "m", ModelActual: "m", ResponseText: "текст", UsageJSON: "{}", CostUSD: 0.04}
	if err := s.SettleWithCheckpoint(res, 0.04, cp); err != nil {
		t.Fatal(err)
	}
	committed, reserved, _ = s.SpentUSD("book")
	if committed != 0.04 || reserved != 0 {
		t.Fatalf("after settle: committed=%v reserved=%v", committed, reserved)
	}

	got, err := s.GetCheckpoint("h1")
	if err != nil || got == nil || got.ResponseText != "текст" {
		t.Fatalf("checkpoint: %+v err=%v", got, err)
	}

	// Повторный settle того же hash (ретрай вызывающего) идемпотентен:
	// резерв снимается, деньги второй раз не книжатся.
	res2, verdict, err := s.Reserve("book", 0.10, caps)
	if err != nil || verdict != ReserveOK {
		t.Fatalf("reserve2: %v %v", verdict, err)
	}
	if err := s.SettleWithCheckpoint(res2, 0.04, cp); err != nil {
		t.Fatal(err)
	}
	committed, reserved, _ = s.SpentUSD("book")
	if committed != 0.04 || reserved != 0 {
		t.Fatalf("duplicate settle must book nothing: committed=%v reserved=%v", committed, reserved)
	}
}

func TestCeilingsDeny(t *testing.T) {
	s, _ := openTemp(t)
	mustSnapshotAndJob(t, s)

	// Книжный потолок: committed+reserved+estimate > cap ⇒ отказ.
	res, verdict, err := s.Reserve("book", 0.9, Ceilings{BookUSD: 1.0})
	if err != nil || verdict != ReserveOK {
		t.Fatalf("first reserve: %v %v", verdict, err)
	}
	_, verdict, err = s.Reserve("book", 0.2, Ceilings{BookUSD: 1.0})
	if err != nil || verdict != ReserveDeniedBook {
		t.Fatalf("second reserve must hit the book ceiling, got %v %v", verdict, err)
	}
	// Release возвращает headroom.
	if err := s.Release(res); err != nil {
		t.Fatal(err)
	}
	_, verdict, err = s.Reserve("book", 0.2, Ceilings{BookUSD: 1.0})
	if err != nil || verdict != ReserveOK {
		t.Fatalf("after release: %v %v", verdict, err)
	}

	// Дневной потолок считается по всем книгам за день.
	_, verdict, err = s.Reserve("other-book", 0.1, Ceilings{DayUSD: 0.25})
	if err != nil || verdict != ReserveDeniedDay {
		t.Fatalf("day ceiling must deny across books, got %v %v", verdict, err)
	}
}

func TestConcurrentReservesRespectCeiling(t *testing.T) {
	s, _ := openTemp(t)
	mustSnapshotAndJob(t, s)
	caps := Ceilings{BookUSD: 0.5}

	// 20 конкурентных резерваций по 0.1 при потолке 0.5: пройти могут максимум 5.
	var wg sync.WaitGroup
	var mu sync.Mutex
	admitted := 0
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			_, verdict, err := s.Reserve("book", 0.1, caps)
			if err != nil {
				t.Error(err)
				return
			}
			if verdict == ReserveOK {
				mu.Lock()
				admitted++
				mu.Unlock()
			}
		}()
	}
	wg.Wait()
	if admitted != 5 {
		t.Fatalf("ceiling 0.5 with 0.1 reserves must admit exactly 5, admitted %d", admitted)
	}
}

func TestRecoveryClearsStaleReservations(t *testing.T) {
	path := filepath.Join(t.TempDir(), "test.db")
	s, err := Open(path)
	if err != nil {
		t.Fatal(err)
	}
	if _, verdict, err := s.Reserve("book", 0.3, Ceilings{BookUSD: 1}); err != nil || verdict != ReserveOK {
		t.Fatalf("%v %v", verdict, err)
	}
	s.Close() // «упали», не сняв резерв

	s2, err := Open(path)
	if err != nil {
		t.Fatal(err)
	}
	defer s2.Close()
	committed, reserved, _ := s2.SpentUSD("book")
	if committed != 0 || reserved != 0 {
		t.Fatalf("reopen must clear stale reservations: committed=%v reserved=%v", committed, reserved)
	}
}

func TestJobKeepsOriginalSnapshot(t *testing.T) {
	s, _ := openTemp(t)
	if err := s.UpsertSnapshot("snapA", "brief", `{}`); err != nil {
		t.Fatal(err)
	}
	if err := s.UpsertSnapshot("snapB", "brief", `{"v":2}`); err != nil {
		t.Fatal(err)
	}
	j1, err := s.EnsureJob("book", 1, "draft", "snapA")
	if err != nil {
		t.Fatal(err)
	}
	// Повторный EnsureJob с другим snapshot НЕ перепривязывает джобу (Р6:
	// snapshot фиксируется на джобу до её завершения).
	j2, err := s.EnsureJob("book", 1, "draft", "snapB")
	if err != nil {
		t.Fatal(err)
	}
	if j1.ID != j2.ID || j2.SnapshotID != "snapA" {
		t.Fatalf("job must keep its original snapshot: %+v", j2)
	}
}

// RequestLogRows обязан вернуть ВСЕ строки, читаемые вызывающим ПОСЛЕ возврата
// метода: раньше он отдавал *sql.Rows с `defer cancel()`, и контекст отменялся
// посреди ленивой итерации в tmctl report («context canceled», обрыв таблицы —
// находка реальной приёмки Фазы 0).
func TestRequestLogRowsMaterializes(t *testing.T) {
	s, _ := openTemp(t)
	for i := 1; i <= 3; i++ {
		if err := s.InsertRequestLog(RequestLog{BookID: "book", Stage: "draft", PromptTokens: 10 * i, OK: true}); err != nil {
			t.Fatal(err)
		}
	}
	// Строка другой книги не должна попасть в выборку.
	if err := s.InsertRequestLog(RequestLog{BookID: "other", Stage: "draft", OK: true}); err != nil {
		t.Fatal(err)
	}
	rows, err := s.RequestLogRows("book")
	if err != nil {
		t.Fatal(err)
	}
	if len(rows) != 3 {
		t.Fatalf("want 3 materialized rows, got %d (context canceled mid-iteration?)", len(rows))
	}
	if rows[0].PromptTokens != 10 || rows[2].PromptTokens != 30 {
		t.Fatalf("rows not in id order or wrong data: %+v", rows)
	}
}

func TestMigrateIsIdempotent(t *testing.T) {
	path := filepath.Join(t.TempDir(), "test.db")
	for i := 0; i < 3; i++ {
		s, err := Open(path)
		if err != nil {
			t.Fatalf("open #%d: %v", i, err)
		}
		s.Close()
	}
}

func TestSingleProcessOwnership(t *testing.T) {
	// Второй Open того же проекта обязан отказать: параллельный процесс
	// (report во время translate) recovery-проходом обнулил бы живые резервы.
	path := filepath.Join(t.TempDir(), "test.db")
	s1, err := Open(path)
	if err != nil {
		t.Fatal(err)
	}
	defer s1.Close()
	if _, err := Open(path); err == nil {
		t.Fatal("second Open must be denied while the project is locked")
	}
	s1.Close()
	s2, err := Open(path)
	if err != nil {
		t.Fatalf("after Close the lock must be free: %v", err)
	}
	s2.Close()
}
