ajhahn.de
← eeco
Go 310 lines
package queue

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

func TestAppend_CreatesFileWithHeader(t *testing.T) {
	dir := t.TempDir()
	item := Item{
		Kind:    "gc-review",
		Title:   "stale ref",
		Project: "demo",
		Detail:  "fact `foo` references missing path",
		Date:    time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC),
	}
	if err := Append(dir, item); err != nil {
		t.Fatal(err)
	}
	b, err := os.ReadFile(filepath.Join(dir, Filename))
	if err != nil {
		t.Fatal(err)
	}
	got := string(b)
	if !strings.HasPrefix(got, "# eeco queue\n") {
		t.Errorf("missing header:\n%s", got)
	}
	want := "- [ ] **gc-review** — stale ref _(demo, 2026-05-19)_\n      fact `foo` references missing path\n"
	if !strings.Contains(got, want) {
		t.Errorf("missing entry, got:\n%s\n\nwant contains:\n%s", got, want)
	}
}

func TestAppend_AppendsToExisting(t *testing.T) {
	dir := t.TempDir()
	first := Item{Kind: "k", Title: "first", Project: "p", Date: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC)}
	second := Item{Kind: "k", Title: "second", Project: "p", Date: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC)}
	if err := Append(dir, first); err != nil {
		t.Fatal(err)
	}
	if err := Append(dir, second); err != nil {
		t.Fatal(err)
	}
	b, _ := os.ReadFile(filepath.Join(dir, Filename))
	got := string(b)
	if !strings.Contains(got, "first") || !strings.Contains(got, "second") {
		t.Errorf("missing entries:\n%s", got)
	}
	if strings.Count(got, "# eeco queue") != 1 {
		t.Errorf("header duplicated:\n%s", got)
	}
}

func TestAppend_NoTrailingNewlineGetsFixed(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, Filename)
	if err := os.WriteFile(path, []byte("# eeco queue\n\n- [x] **k** — old _(p, 2026-05-19)_"), 0o644); err != nil {
		t.Fatal(err)
	}
	item := Item{Kind: "k", Title: "new", Project: "p", Date: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC)}
	if err := Append(dir, item); err != nil {
		t.Fatal(err)
	}
	b, _ := os.ReadFile(path)
	got := string(b)
	if !strings.Contains(got, "old _(p, 2026-05-19)_\n- [ ] **k** — new") {
		t.Errorf("appended entry stuck to previous line:\n%s", got)
	}
}

func TestAppend_RejectsMissingFields(t *testing.T) {
	dir := t.TempDir()
	cases := []Item{
		{Title: "no kind", Project: "p"},
		{Kind: "k", Project: "p"},
	}
	for _, item := range cases {
		if err := Append(dir, item); err == nil {
			t.Errorf("Append(%+v) succeeded; expected error", item)
		}
	}
}

func TestAppend_CreatesStateDir(t *testing.T) {
	dir := filepath.Join(t.TempDir(), "deeper", "state")
	item := Item{Kind: "k", Title: "t", Project: "p", Date: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC)}
	if err := Append(dir, item); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(filepath.Join(dir, Filename)); err != nil {
		t.Errorf("queue.md not created: %v", err)
	}
}

func TestAppendUnique_SkipsDuplicateOpenItem(t *testing.T) {
	dir := t.TempDir()
	item := Item{Kind: "memory-drift", Title: "fact x may be stale", Project: "p",
		Date: time.Date(2026, 5, 22, 0, 0, 0, 0, time.UTC)}

	added, err := AppendUnique(dir, item)
	if err != nil || !added {
		t.Fatalf("first AppendUnique: added=%v err=%v", added, err)
	}
	// Same finding on a later day: Project/Date differ but Kind+Title match.
	dup := item
	dup.Date = time.Date(2026, 5, 23, 0, 0, 0, 0, time.UTC)
	added, err = AppendUnique(dir, dup)
	if err != nil {
		t.Fatalf("second AppendUnique: %v", err)
	}
	if added {
		t.Error("duplicate open item was appended; want skip")
	}
	b, _ := os.ReadFile(filepath.Join(dir, Filename))
	if n := strings.Count(string(b), "fact x may be stale"); n != 1 {
		t.Errorf("item present %d times, want 1:\n%s", n, b)
	}
}

func TestAppendUnique_AppendsWhenTitleDiffers(t *testing.T) {
	dir := t.TempDir()
	base := Item{Kind: "memory-drift", Project: "p", Date: time.Date(2026, 5, 22, 0, 0, 0, 0, time.UTC)}
	a := base
	a.Title = "fact a may be stale"
	b := base
	b.Title = "fact b may be stale"
	if added, err := AppendUnique(dir, a); err != nil || !added {
		t.Fatalf("append a: added=%v err=%v", added, err)
	}
	if added, err := AppendUnique(dir, b); err != nil || !added {
		t.Fatalf("append b: added=%v err=%v", added, err)
	}
	content, _ := os.ReadFile(filepath.Join(dir, Filename))
	if !strings.Contains(string(content), "fact a") || !strings.Contains(string(content), "fact b") {
		t.Errorf("both distinct items should be present:\n%s", content)
	}
}

func TestAppendUnique_ResolvedItemDoesNotBlockRefile(t *testing.T) {
	dir := t.TempDir()
	item := Item{Kind: "doc-drift", Title: "tag v1.2.3 not documented", Project: "p",
		Date: time.Date(2026, 5, 22, 0, 0, 0, 0, time.UTC)}
	if added, err := AppendUnique(dir, item); err != nil || !added {
		t.Fatalf("first append: added=%v err=%v", added, err)
	}
	// Operator resolves it (checks the box) but the drift persists.
	path := filepath.Join(dir, Filename)
	b, _ := os.ReadFile(path)
	resolved := strings.Replace(string(b), "- [ ] **doc-drift**", "- [x] **doc-drift**", 1)
	if err := os.WriteFile(path, []byte(resolved), 0o644); err != nil {
		t.Fatal(err)
	}
	added, err := AppendUnique(dir, item)
	if err != nil {
		t.Fatal(err)
	}
	if !added {
		t.Error("re-file after resolve was skipped; a resolved item must not block a re-file")
	}
}

func TestParseOpenRow(t *testing.T) {
	cases := []struct {
		line             string
		wantKind, wantTitle string
		wantOK           bool
	}{
		{"- [ ] **memory-drift** — fact x may be stale _(p, 2026-05-22)_", "memory-drift", "fact x may be stale", true},
		{"  - [ ] **k** — t _(proj, 2026-05-22)_", "k", "t", true},
		{"- [x] **k** — resolved _(p, 2026-05-22)_", "", "", false},
		{"      indented detail line", "", "", false},
		{"# eeco queue", "", "", false},
		{"- [ ] no bold kind _(p, 2026-05-22)_", "", "", false},
		// A title that itself contains "_(" trims at the last suffix marker.
		{"- [ ] **k** — weird _(x)_ title _(p, 2026-05-22)_", "k", "weird _(x)_ title", true},
		// No closing "**" after the kind.
		{"- [ ] **k — t _(p, 2026-05-22)_", "", "", false},
		// No " — " separator between kind and title.
		{"- [ ] **k** t _(p, 2026-05-22)_", "", "", false},
		// Empty kind (matched "****" gives an empty kind → rejected).
		{"- [ ] **** — t _(p, 2026-05-22)_", "", "", false},
		// Empty title (nothing between the separator and the suffix).
		{"- [ ] **k** —  _(p, 2026-05-22)_", "", "", false},
	}
	for _, c := range cases {
		k, ti, ok := parseOpenRow(c.line)
		if ok != c.wantOK || k != c.wantKind || ti != c.wantTitle {
			t.Errorf("parseOpenRow(%q) = (%q, %q, %v), want (%q, %q, %v)",
				c.line, k, ti, ok, c.wantKind, c.wantTitle, c.wantOK)
		}
	}
}

func TestAppendUnique_RejectsMissingFields(t *testing.T) {
	dir := t.TempDir()
	if _, err := AppendUnique(dir, Item{Title: "no kind", Project: "p"}); err == nil {
		t.Error("AppendUnique with no kind succeeded; expected error")
	}
}

func TestCount(t *testing.T) {
	dir := t.TempDir()
	if n, err := Count(dir); err != nil || n != 0 {
		t.Errorf("missing queue: n=%d err=%v", n, err)
	}
	for i, kind := range []string{"a", "b", "c"} {
		_ = i
		if err := Append(dir, Item{Kind: kind, Title: kind, Project: "p", Date: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC)}); err != nil {
			t.Fatal(err)
		}
	}
	// Resolve one item manually.
	path := filepath.Join(dir, Filename)
	b, _ := os.ReadFile(path)
	s := strings.Replace(string(b), "- [ ] **a**", "- [x] **a**", 1)
	if err := os.WriteFile(path, []byte(s), 0o644); err != nil {
		t.Fatal(err)
	}
	n, err := Count(dir)
	if err != nil {
		t.Fatal(err)
	}
	if n != 2 {
		t.Errorf("Count = %d, want 2", n)
	}
}

func TestResolved(t *testing.T) {
	t.Run("missing file", func(t *testing.T) {
		got, err := Resolved(t.TempDir(), "evolve", "done")
		if err != nil || got {
			t.Fatalf("missing file → (%v, %v), want (false, nil)", got, err)
		}
	})

	dir := t.TempDir()
	content := "# eeco queue\n\n" +
		"- [x] **evolve** — done _(p, 2026-05-22)_\n" +
		"- [ ] **gc** — open one _(p, 2026-05-22)_\n"
	if err := os.WriteFile(filepath.Join(dir, Filename), []byte(content), 0o644); err != nil {
		t.Fatal(err)
	}
	cases := []struct {
		kind, title string
		want        bool
	}{
		{"evolve", "done", true},
		{"evolve", "wrong-title", false},
		{"wrong-kind", "done", false},
		{"gc", "open one", false}, // an open row is not resolved
	}
	for _, c := range cases {
		got, err := Resolved(dir, c.kind, c.title)
		if err != nil {
			t.Errorf("Resolved(%q,%q) err = %v", c.kind, c.title, err)
		}
		if got != c.want {
			t.Errorf("Resolved(%q,%q) = %v, want %v", c.kind, c.title, got, c.want)
		}
	}
}

func TestResolved_ReadErrWrapped(t *testing.T) {
	dir := t.TempDir()
	// queue.md as a directory → ReadFile returns a non-NotExist error.
	if err := os.MkdirAll(filepath.Join(dir, Filename), 0o755); err != nil {
		t.Fatal(err)
	}
	if _, err := Resolved(dir, "k", "t"); err == nil || !strings.Contains(err.Error(), "queue.Resolved:") {
		t.Fatalf("err = %v, want 'queue.Resolved:'", err)
	}
}

func TestValidateItem_Defaults(t *testing.T) {
	if err := validateItem("", &Item{Kind: "k", Title: "t"}); err == nil ||
		!strings.Contains(err.Error(), "queue: stateDir is empty") {
		t.Fatalf("empty stateDir err = %v, want 'queue: stateDir is empty'", err)
	}
	item := &Item{Kind: "k", Title: "t"} // zero Date
	if err := validateItem("somedir", item); err != nil {
		t.Fatalf("validateItem: %v", err)
	}
	if item.Date.IsZero() {
		t.Error("zero Date was not defaulted to now")
	}
}

func TestReadQueue_NonNotExistErrWrapped(t *testing.T) {
	dir := t.TempDir()
	if err := os.MkdirAll(filepath.Join(dir, Filename), 0o755); err != nil {
		t.Fatal(err)
	}
	if _, err := readQueue(dir); err == nil || !strings.Contains(err.Error(), "queue: read:") {
		t.Fatalf("err = %v, want 'queue: read:'", err)
	}
}

func TestCount_ReadErrWrapped(t *testing.T) {
	dir := t.TempDir()
	if err := os.MkdirAll(filepath.Join(dir, Filename), 0o755); err != nil {
		t.Fatal(err)
	}
	if _, err := Count(dir); err == nil || !strings.Contains(err.Error(), "queue.Count:") {
		t.Fatalf("err = %v, want 'queue.Count:'", err)
	}
}