Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

calls_summarise_test.go
package calls

import (
	"reflect"
	"testing"

	"skraak/datafile"
)

func TestFilterLabels(t *testing.T) {
	a := &datafile.Label{Filter: "kiwi.txt", Species: "Kiwi"}
	b := &datafile.Label{Filter: "tomtit.txt", Species: "Tomtit"}
	c := &datafile.Label{Filter: "kiwi.txt", Species: "Kiwi2"}
	labels := []*datafile.Label{a, b, c}

	tests := []struct {
		name   string
		filter string
		want   []*datafile.Label
	}{
		{"no filter returns all", "", labels},
		{"matching filter returns subset", "kiwi.txt", []*datafile.Label{a, c}},
		{"non-matching filter returns nil", "missing.txt", nil},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := filterLabels(labels, tt.filter)
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("got %v want %v", got, tt.want)
			}
		})
	}
}

func TestBuildLabelSummaries_OmitsEmptyFields(t *testing.T) {
	labels := []*datafile.Label{
		{Filter: "f", Certainty: 80, Species: "Sp"},
		{Filter: "f", Certainty: 100, Species: "Sp", CallType: "song", Comment: "hi", Bookmark: true},
	}
	got := buildLabelSummaries(labels)
	if len(got) != 2 {
		t.Fatalf("len=%d want 2", len(got))
	}
	if got[0].CallType != "" || got[0].Comment != "" || got[0].Bookmark {
		t.Errorf("empty fields leaked: %+v", got[0])
	}
	if got[1].CallType != "song" || got[1].Comment != "hi" || !got[1].Bookmark {
		t.Errorf("populated fields wrong: %+v", got[1])
	}
}

func TestUpdateReviewStatus(t *testing.T) {
	out := &CallsSummariseOutput{}
	labels := []*datafile.Label{
		{Certainty: 100},
		{Certainty: 100, CallType: "song"},
		{Certainty: 0},
		{Certainty: 50, Comment: "hmm", Bookmark: true},
	}
	for _, l := range labels {
		updateReviewStatus(l, out)
	}
	want := ReviewStatus{
		Confirmed:    2,
		DontKnow:     1,
		Unreviewed:   1,
		WithCallType: 1,
		WithComments: 1,
		Bookmarked:   1,
	}
	if out.ReviewStatus != want {
		t.Errorf("got %+v want %+v", out.ReviewStatus, want)
	}
}

func TestUpdateFilterStats_AccumulatesAcrossLabels(t *testing.T) {
	out := &CallsSummariseOutput{Filters: map[string]FilterStats{}}
	labels := []*datafile.Label{
		{Filter: "kiwi.txt", Species: "Kiwi"},
		{Filter: "kiwi.txt", Species: "Kiwi", CallType: "brrr"},
		{Filter: "kiwi.txt", Species: "Kiwi", CallType: "brrr"},
		{Filter: "kiwi.txt", Species: "Kiwi2"},
		{Filter: "tomtit.txt", Species: "Tomtit", CallType: "song"},
	}
	for _, l := range labels {
		updateFilterStats(l, out)
	}

	kiwi := out.Filters["kiwi.txt"]
	if kiwi.Segments != 4 {
		t.Errorf("kiwi.Segments=%d want 4", kiwi.Segments)
	}
	if kiwi.Species["Kiwi"] != 3 || kiwi.Species["Kiwi2"] != 1 {
		t.Errorf("kiwi.Species=%v", kiwi.Species)
	}
	if kiwi.Calltypes["Kiwi"]["brrr"] != 2 {
		t.Errorf("kiwi calltype brrr=%d want 2", kiwi.Calltypes["Kiwi"]["brrr"])
	}

	tomtit := out.Filters["tomtit.txt"]
	if tomtit.Segments != 1 || tomtit.Calltypes["Tomtit"]["song"] != 1 {
		t.Errorf("tomtit=%+v", tomtit)
	}
}

func TestTrackMeta(t *testing.T) {
	ops, revs := map[string]bool{}, map[string]bool{}
	trackMeta(nil, ops, revs)
	if len(ops) != 0 || len(revs) != 0 {
		t.Errorf("nil meta should be no-op")
	}
	trackMeta(&datafile.DataMeta{Operator: "alice", Reviewer: ""}, ops, revs)
	trackMeta(&datafile.DataMeta{Operator: "", Reviewer: "bob"}, ops, revs)
	trackMeta(&datafile.DataMeta{Operator: "alice", Reviewer: "bob"}, ops, revs)
	if !ops["alice"] || len(ops) != 1 {
		t.Errorf("operators=%v want only alice", ops)
	}
	if !revs["bob"] || len(revs) != 1 {
		t.Errorf("reviewers=%v want only bob", revs)
	}
}

func TestExtractRelativePath(t *testing.T) {
	tests := []struct {
		dataPath string
		want     string
	}{
		{"/folder/tx51_LISTENING_20260221_203004.WAV.data", "tx51_LISTENING_20260221_203004.WAV"},
		{"foo.wav.data", "foo.wav"},
		{"/a/b/c/file.WAV.data", "file.WAV"},
		{"noslash.data", "noslash"},
	}
	for _, tt := range tests {
		t.Run(tt.dataPath, func(t *testing.T) {
			got := extractRelativePath("", tt.dataPath)
			if got != tt.want {
				t.Errorf("got %q want %q", got, tt.want)
			}
		})
	}
}

func TestUpdateStatsFromLabels_DelegatesCorrectly(t *testing.T) {
	out := &CallsSummariseOutput{Filters: map[string]FilterStats{}}
	labels := []*datafile.Label{
		{Filter: "f1", Species: "Kiwi", Certainty: 100, CallType: "song"},
		{Filter: "f1", Species: "Kiwi", Certainty: 0},
	}
	updateStatsFromLabels(labels, out)

	// Should have delegated to both updateFilterStats and updateReviewStatus
	if out.Filters["f1"].Segments != 2 {
		t.Errorf("Segments=%d want 2", out.Filters["f1"].Segments)
	}
	if out.ReviewStatus.Confirmed != 1 {
		t.Errorf("Confirmed=%d want 1", out.ReviewStatus.Confirmed)
	}
	if out.ReviewStatus.DontKnow != 1 {
		t.Errorf("DontKnow=%d want 1", out.ReviewStatus.DontKnow)
	}
	if out.ReviewStatus.WithCallType != 1 {
		t.Errorf("WithCallType=%d want 1", out.ReviewStatus.WithCallType)
	}
}

func TestFinaliseSummary_SortsAndClearsEmptyCalltypes(t *testing.T) {
	out := &CallsSummariseOutput{
		Filters: map[string]FilterStats{
			"f1": {Segments: 1, Species: map[string]int{"S": 1}, Calltypes: map[string]map[string]int{}},
			"f2": {Segments: 1, Species: map[string]int{"S": 1}, Calltypes: map[string]map[string]int{"S": {"c": 1}}},
		},
		Segments: []SegmentSummary{
			{File: "b.wav", StartTime: 1},
			{File: "a.wav", StartTime: 5},
			{File: "a.wav", StartTime: 2},
		},
	}
	finaliseSummary(out, map[string]bool{"zoe": true, "alice": true}, map[string]bool{"bob": true}, false)

	if out.Filters["f1"].Calltypes != nil {
		t.Errorf("empty calltypes not cleared: %v", out.Filters["f1"].Calltypes)
	}
	if out.Filters["f2"].Calltypes == nil {
		t.Errorf("populated calltypes cleared in error")
	}
	wantOps := []string{"alice", "zoe"}
	if !reflect.DeepEqual(out.Operators, wantOps) {
		t.Errorf("operators=%v want %v", out.Operators, wantOps)
	}
	wantOrder := []struct {
		file  string
		start float64
	}{{"a.wav", 2}, {"a.wav", 5}, {"b.wav", 1}}
	for i, w := range wantOrder {
		if out.Segments[i].File != w.file || out.Segments[i].StartTime != w.start {
			t.Errorf("segment[%d]=%+v want file=%s start=%v", i, out.Segments[i], w.file, w.start)
		}
	}
}