// Tests for the file-snapshot rewind journal: per-turn dedup of recorded paths,
// commit grouping (an untouched turn produces no restore point), and restore
// semantics — reverse replay across turns rolls a file to its oldest baseline,
// files that did not exist are deleted, and the journal is truncated to the tip.
package agenttool

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

func writeFileT(t *testing.T, path, content string) {
	t.Helper()
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatalf("write %s: %v", path, err)
	}
}

func readFileT(t *testing.T, path string) string {
	t.Helper()
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read %s: %v", path, err)
	}
	return string(data)
}

// A turn's first Record for a path is the baseline; later Records that turn are
// ignored, and Commit groups the turn's files into one point.
func TestRecorderRecordDedupAndCommit(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "a.txt")
	writeFileT(t, f, "v0")

	r := NewFileSnapshotRecorder()
	r.Record(f)            // baseline "v0"
	writeFileT(t, f, "v1") // model's first edit
	r.Record(f)            // second edit same turn — must be ignored
	writeFileT(t, f, "v2")

	if !r.Commit("leaf0", "edit a") {
		t.Fatal("Commit reported no restore point despite a recorded file")
	}
	// An untouched turn creates nothing.
	if r.Commit("leaf1", "no edits") {
		t.Fatal("Commit created a restore point for a turn with no records")
	}
	points := r.Points()
	if len(points) != 1 || len(points[0].Snapshots) != 1 {
		t.Fatalf("want 1 point with 1 snapshot, got %+v", points)
	}
	if got := string(points[0].Snapshots[0].Content); got != "v0" {
		t.Errorf("baseline content = %q, want v0", got)
	}
}

// Restoring rolls files back and deletes ones that did not exist before, and
// returns the pre-turn leaf id.
func TestRecorderRestore(t *testing.T) {
	dir := t.TempDir()
	existing := filepath.Join(dir, "keep.txt")
	created := filepath.Join(dir, "new.txt")
	writeFileT(t, existing, "orig")

	r := NewFileSnapshotRecorder()

	// Turn 1: edit an existing file.
	r.Record(existing)
	writeFileT(t, existing, "edited")
	r.Commit("leafA", "turn1")

	// Turn 2: create a brand-new file.
	r.Record(created)
	writeFileT(t, created, "brand new")
	r.Commit("leafB", "turn2")

	// Rewind to before turn 1 (index 0): both turns roll back.
	leaf, restored, warnings, err := r.Restore(0)
	if err != nil {
		t.Fatalf("Restore: %v", err)
	}
	if leaf != "leafA" {
		t.Errorf("target leaf = %q, want leafA", leaf)
	}
	if len(warnings) != 0 {
		t.Errorf("unexpected warnings: %v", warnings)
	}
	if len(restored) != 2 {
		t.Errorf("restored %d files, want 2", len(restored))
	}
	if got := readFileT(t, existing); got != "orig" {
		t.Errorf("existing file = %q, want orig", got)
	}
	if _, err := os.Stat(created); !os.IsNotExist(err) {
		t.Errorf("created file should have been deleted, stat err = %v", err)
	}
	if len(r.Points()) != 0 {
		t.Errorf("journal should be empty after restoring from index 0")
	}
}

// When a file is edited across several turns, restoring to before the earliest
// of them lands it at its oldest baseline (not an intermediate version).
func TestRecorderRestoreOldestBaselineWins(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "a.txt")
	writeFileT(t, f, "v0")

	r := NewFileSnapshotRecorder()
	r.Record(f) // baseline v0
	writeFileT(t, f, "v1")
	r.Commit("leaf0", "t1")

	r.Record(f) // baseline v1
	writeFileT(t, f, "v2")
	r.Commit("leaf1", "t2")

	// Rewind to before t2 only (index 1): file returns to v1.
	if _, _, _, err := r.Restore(1); err != nil {
		t.Fatalf("Restore(1): %v", err)
	}
	if got := readFileT(t, f); got != "v1" {
		t.Errorf("after rewind to before t2, file = %q, want v1", got)
	}
	// One point remains (t1); rewind it too → v0.
	if _, _, _, err := r.Restore(0); err != nil {
		t.Fatalf("Restore(0): %v", err)
	}
	if got := readFileT(t, f); got != "v0" {
		t.Errorf("after full rewind, file = %q, want v0", got)
	}
}

func TestRecorderRestoreOutOfRange(t *testing.T) {
	r := NewFileSnapshotRecorder()
	if _, _, _, err := r.Restore(0); err == nil {
		t.Error("Restore on empty journal should error")
	}
}

// A nil recorder is a safe no-op so tools can hold an optional handle.
func TestRecorderNilSafe(t *testing.T) {
	var r *FileSnapshotRecorder
	r.Record("/nonexistent")
	if r.Commit("x", "y") {
		t.Error("nil Commit should report no point")
	}
	if r.Points() != nil {
		t.Error("nil Points should be nil")
	}
}
