package beads
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
)
// milestoneFixture: six issues across two milestones. m1 carries the full
// hierarchy — a milestone-typed head, an epic with one (closed) subtask, and a
// loose bug; m2 has a single loose feature; one issue has no milestone label.
func milestoneFixture() *fakeSession {
issues := &browse.RowPage{
Columns: []string{"id", "title", "status", "priority", "issue_type", "assignee"},
Rows: [][]string{
{"i-m", "Mike", "open", "0", "milestone", ""},
{"i-e", "Echo", "open", "1", "epic", ""},
{"i-a", "Alpha", "closed", "1", "task", "alice"}, // subtask of i-e
{"i-b", "Bravo", "open", "0", "bug", "bob"},
{"i-c", "Charlie", "open", "2", "feature", ""},
{"i-d", "Delta", "open", "1", "task", ""}, // no milestone
},
Total: 6,
}
labels := &browse.RowPage{
Columns: []string{"issue_id", "label"},
Rows: [][]string{
{"i-m", "milestone:m1"},
{"i-e", "milestone:m1"},
{"i-a", "milestone:m1"},
{"i-b", "milestone:m1"},
{"i-c", "milestone:m2"},
{"i-b", "backend"}, // non-milestone label ignored by grouping
},
Total: 6,
}
deps := &browse.RowPage{
Columns: []string{"issue_id", "depends_on_issue_id", "type"},
Rows: [][]string{
{"i-a", "i-e", "parent-child"},
{"i-b", "i-a", "blocks"}, // non-hierarchy edge ignored by nesting
},
Total: 2,
}
statuses := &browse.RowPage{
Columns: []string{"name", "category"},
Rows: [][]string{{"open", "open"}, {"closed", "closed"}},
Total: 2,
}
return &fakeSession{
rowsByTable: map[string]*browse.RowPage{
"issues": issues,
"labels": labels,
"dependencies": deps,
"custom_statuses": statuses,
},
}
}
func TestMilestonesBuild(t *testing.T) {
d, err := BuildMilestones(context.Background(), milestoneFixture(), "main")
require.NoError(t, err)
require.Len(t, d.Milestones, 2, "milestones = %+v", d.Milestones)
assert.Equal(t, 1, d.Unlabeled)
assert.Equal(t, 6, d.Total)
m1 := d.Milestones[0]
assert.Equal(t, "m1", m1.Name)
assert.Equal(t, 4, m1.Total)
assert.Equal(t, 1, m1.Done)
assert.Equal(t, 3, m1.Open)
assert.Equal(t, 25, m1.Pct())
// Hierarchy: the milestone-typed issue heads the list, the epic nests its
// subtask, and the bug (whose only dep edge is "blocks") stays loose.
require.Len(t, m1.Heads, 1, "m1 heads = %+v", m1.Heads)
assert.Equal(t, "i-m", m1.Heads[0].ID)
require.Len(t, m1.Epics, 1, "m1 epics = %+v", m1.Epics)
epic := m1.Epics[0]
assert.Equal(t, "i-e", epic.Card.ID)
require.Len(t, epic.Children, 1, "epic = %+v", epic)
assert.Equal(t, "i-a", epic.Children[0].ID)
assert.Equal(t, 1, epic.Done)
assert.Equal(t, 1, epic.Total)
assert.Equal(t, "closed", epic.Children[0].Category, "i-a should be closed")
require.Len(t, m1.Loose, 1, "m1 loose = %+v", m1.Loose)
assert.Equal(t, "i-b", m1.Loose[0].ID)
m2 := d.Milestones[1]
assert.Equal(t, "m2", m2.Name)
assert.Equal(t, 1, m2.Total)
assert.Equal(t, 0, m2.Done)
assert.Empty(t, m2.Heads)
assert.Empty(t, m2.Epics)
require.Len(t, m2.Loose, 1, "m2 hierarchy = %+v", m2)
assert.Equal(t, "i-c", m2.Loose[0].ID)
}
// TestMilestonesEmpty: a beads DB with no milestone labels still produces a
// (empty) rollup rather than erroring — the tab renders an empty state.
func TestMilestonesEmpty(t *testing.T) {
d, err := BuildMilestones(context.Background(), beadsFixture(), "main")
require.NoError(t, err)
assert.Empty(t, d.Milestones, "expected no milestones")
assert.Equal(t, d.Total, d.Unlabeled,
"with no milestone labels all issues are unlabeled")
}