package web
import (
"net/http/httptest"
"path/filepath"
"regexp"
"strings"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ops"
)
// statServer is a timeline of three check-ins: a counted change with
// both sides, an addition-only one, and one whose lines are unknown.
func statServer(t *testing.T) *httptest.Server {
t.Helper()
ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
if err := ix.FoldCheckins([]index.CheckinRecord{
{Hash: "a1" + strings.Repeat("0", 38), Date: "2026-10-01T10:00:00", User: "u", Comment: "both",
Changes: []string{"src/a.go", "src/b.go"},
Lines: map[string]index.LineCount{"src/a.go": {Added: 160, Removed: 2}, "src/b.go": {Added: 9}}},
{Hash: "b2" + strings.Repeat("0", 38), Date: "2026-10-02T10:00:00", User: "u", Comment: "adds",
Parents: []string{"a1" + strings.Repeat("0", 38)},
Changes: []string{"src/a.go"}, Lines: map[string]index.LineCount{"src/a.go": {Added: 4}}},
{Hash: "c3" + strings.Repeat("0", 38), Date: "2026-10-03T10:00:00", User: "u", Comment: "unknown",
Parents: []string{"b2" + strings.Repeat("0", 38)}, Changes: []string{"docs/x.md"}},
}); err != nil {
t.Fatal(err)
}
r := ops.NewRegistry()
if err := api.RegisterAll(r, api.Deps{Version: "v",
Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}})}); err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(HandlerWith(r, nil, nil, nil))
t.Cleanup(srv.Close)
return srv
}
var statRe = regexp.MustCompile(`<span class="tl-stat" data-hook="checkin-stat" data-files="(\d+)"(?: data-added="(\d+)" data-removed="(\d+)")?[^>]*>(.*?)</span></span>`)
// Every check-in card sizes its change: files always, lines when they
// were counted, and a side that is zero is not printed.
func TestTimelineCardsSizeTheirChange(t *testing.T) {
srv := statServer(t)
_, body := getPage(t, srv, "/r/alpha/timeline")
got := map[string][]string{}
for _, m := range statRe.FindAllStringSubmatch(body, -1) {
got[m[1]+"/"+m[2]+"/"+m[3]] = m
}
both, ok := got["2/169/2"]
if !ok || !strings.Contains(both[4], `class="ci-add"`) || !strings.Contains(both[4], `class="ci-del"`) {
t.Errorf("FAIL: the counted change: %v (all: %v)", both, keys(got))
}
if adds, ok := got["1/4/0"]; !ok || strings.Contains(adds[4], `class="ci-del"`) {
t.Errorf("FAIL: an addition-only change prints a removed side, or none: %v", adds)
}
if unk, ok := got["1//"]; !ok || strings.Contains(unk[4], "ci-stat") {
t.Errorf("FAIL: an uncounted change should show its files alone: %v", unk)
}
// A file's history sizes that file's change alone.
_, body = getPage(t, srv, "/r/alpha/timeline?p=src/b.go")
if m := statRe.FindAllStringSubmatch(body, -1); len(m) != 1 || m[0][1] != "1" || m[0][2] != "9" {
t.Errorf("FAIL: src/b.go's history: %v", m)
}
}
func keys(m map[string][]string) []string {
var out []string
for k := range m {
out = append(out, k)
}
return out
}