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_add_test.go
package calls

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

	"skraak/datafile"
)

func TestCallsAddNewFile(t *testing.T) {
	tmpDir := t.TempDir()
	wavPath := filepath.Join(tmpDir, "test.wav")
	dataPath := wavPath + ".data"

	createTestWAV(t, wavPath, 16000, 10)

	result, err := CallsAdd(CallsAddInput{
		File:      dataPath,
		Segment:   "2-5",
		Species:   "Kiwi",
		Certainty: 100,
		Filter:    "Manual",
		Reviewer:  "David",
	})

	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !result.Created {
		t.Errorf("expected Created=true, got false")
	}
	if result.SegmentStart != 2 {
		t.Errorf("expected segment_start=2, got %f", result.SegmentStart)
	}
	if result.SegmentEnd != 5 {
		t.Errorf("expected segment_end=5, got %f", result.SegmentEnd)
	}
	if result.LowFreq != 0 {
		t.Errorf("expected low_freq=0, got %f", result.LowFreq)
	}
	if result.HighFreq != 16000 {
		t.Errorf("expected high_freq=16000, got %f", result.HighFreq)
	}
	if result.Species != "Kiwi" {
		t.Errorf("expected species=Kiwi, got %s", result.Species)
	}
	if result.Filter != "Manual" {
		t.Errorf("expected filter=Manual, got %s", result.Filter)
	}
}

func TestCallsAddNewFileMetadata(t *testing.T) {
	tmpDir := t.TempDir()
	wavPath := filepath.Join(tmpDir, "test.wav")
	dataPath := wavPath + ".data"

	createTestWAV(t, wavPath, 16000, 10)

	_, err := CallsAdd(CallsAddInput{
		File:      dataPath,
		Segment:   "2-5",
		Species:   "Kiwi",
		Certainty: 100,
		Filter:    "Manual",
		Reviewer:  "David",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if _, err := os.Stat(dataPath); os.IsNotExist(err) {
		t.Fatal("expected .data file to be created")
	}

	df, err := datafile.ParseDataFile(dataPath)
	if err != nil {
		t.Fatalf("failed to parse created file: %v", err)
	}
	if df.Meta.Operator != "Manual" {
		t.Errorf("expected Operator=Manual, got %s", df.Meta.Operator)
	}
	if df.Meta.Reviewer != "David" {
		t.Errorf("expected Reviewer=David, got %s", df.Meta.Reviewer)
	}
	if len(df.Segments) != 1 {
		t.Fatalf("expected 1 segment, got %d", len(df.Segments))
	}
	seg := df.Segments[0]
	if seg.StartTime != 2 || seg.EndTime != 5 {
		t.Errorf("expected segment 2-5, got %.1f-%.1f", seg.StartTime, seg.EndTime)
	}
	if len(seg.Labels) != 1 {
		t.Fatalf("expected 1 label, got %d", len(seg.Labels))
	}
	if seg.Labels[0].Species != "Kiwi" {
		t.Errorf("expected species=Kiwi, got %s", seg.Labels[0].Species)
	}
}

func TestCallsAddToExistingFile(t *testing.T) {
	tmpDir := t.TempDir()
	wavPath := filepath.Join(tmpDir, "test.wav")
	dataPath := wavPath + ".data"

	createTestWAV(t, wavPath, 16000, 10)

	df := &datafile.DataFile{
		Meta: &datafile.DataMeta{Operator: "BirdNET", Duration: 10, Reviewer: "AI"},
		Segments: []*datafile.Segment{
			{
				StartTime: 2,
				EndTime:   5,
				FreqLow:   0,
				FreqHigh:  16000,
				Labels: []*datafile.Label{
					{Species: "Tui", Certainty: 80, Filter: "BirdNET"},
				},
			},
		},
	}
	if err := df.Write(dataPath); err != nil {
		t.Fatalf("failed to write test file: %v", err)
	}

	result, err := CallsAdd(CallsAddInput{
		File:      dataPath,
		Segment:   "2-5",
		Species:   "Kiwi",
		Certainty: 100,
		Filter:    "Manual",
		Reviewer:  "David",
	})

	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result.Created {
		t.Errorf("expected Created=false (label added to existing segment), got true")
	}

	df2, err := datafile.ParseDataFile(dataPath)
	if err != nil {
		t.Fatalf("failed to parse file: %v", err)
	}
	if len(df2.Segments) != 1 {
		t.Fatalf("expected 1 segment, got %d", len(df2.Segments))
	}
	if len(df2.Segments[0].Labels) != 2 {
		t.Errorf("expected 2 labels, got %d", len(df2.Segments[0].Labels))
	}
	if df2.Meta.Reviewer != "David" {
		t.Errorf("expected Reviewer=David, got %s", df2.Meta.Reviewer)
	}
	if df2.Meta.Operator != "BirdNET" {
		t.Errorf("expected Operator=BirdNET (unchanged), got %s", df2.Meta.Operator)
	}
}

func TestCallsAddDuplicateFilter(t *testing.T) {
	tmpDir := t.TempDir()
	dataPath := filepath.Join(tmpDir, "test.data")

	df := &datafile.DataFile{
		Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
		Segments: []*datafile.Segment{
			{
				StartTime: 2,
				EndTime:   5,
				FreqLow:   0,
				FreqHigh:  16000,
				Labels: []*datafile.Label{
					{Species: "Kiwi", Certainty: 80, Filter: "Manual"},
				},
			},
		},
	}
	if err := df.Write(dataPath); err != nil {
		t.Fatalf("failed to write test file: %v", err)
	}

	result, err := CallsAdd(CallsAddInput{
		File:      dataPath,
		Segment:   "2-5",
		Species:   "Tui",
		Certainty: 100,
		Filter:    "Manual",
		Reviewer:  "David",
	})

	if err == nil {
		t.Error("expected error for duplicate filter, got nil")
	}
	if result.Error == "" {
		t.Error("expected error message in output")
	}
}

func TestCallsAddNewSegment(t *testing.T) {
	tmpDir := t.TempDir()
	dataPath := filepath.Join(tmpDir, "test.data")

	df := &datafile.DataFile{
		Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
		Segments: []*datafile.Segment{
			{
				StartTime: 2,
				EndTime:   5,
				FreqLow:   0,
				FreqHigh:  16000,
				Labels: []*datafile.Label{
					{Species: "Kiwi", Certainty: 80, Filter: "Manual"},
				},
			},
		},
	}
	if err := df.Write(dataPath); err != nil {
		t.Fatalf("failed to write test file: %v", err)
	}

	result, err := CallsAdd(CallsAddInput{
		File:      dataPath,
		Segment:   "10-15",
		Frequency: "200-4500",
		Species:   "Tui",
		Certainty: 90,
		Filter:    "Manual",
		Reviewer:  "David",
	})

	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !result.Created {
		t.Errorf("expected Created=true, got false")
	}
	if result.LowFreq != 200 {
		t.Errorf("expected low_freq=200, got %f", result.LowFreq)
	}
	if result.HighFreq != 4500 {
		t.Errorf("expected high_freq=4500, got %f", result.HighFreq)
	}

	df2, err := datafile.ParseDataFile(dataPath)
	if err != nil {
		t.Fatalf("failed to parse file: %v", err)
	}
	if len(df2.Segments) != 2 {
		t.Errorf("expected 2 segments, got %d", len(df2.Segments))
	}
}

func TestCallsAddWithCallType(t *testing.T) {
	tmpDir := t.TempDir()
	wavPath := filepath.Join(tmpDir, "test.wav")
	dataPath := wavPath + ".data"

	createTestWAV(t, wavPath, 16000, 10)

	result, err := CallsAdd(CallsAddInput{
		File:      dataPath,
		Segment:   "2-5",
		Species:   "Kiwi+Duet",
		Certainty: 100,
		Filter:    "Manual",
		Reviewer:  "David",
	})

	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result.Species != "Kiwi" {
		t.Errorf("expected species=Kiwi, got %s", result.Species)
	}
	if result.CallType != "Duet" {
		t.Errorf("expected calltype=Duet, got %s", result.CallType)
	}
}

func TestCallsAddMissingWAV(t *testing.T) {
	tmpDir := t.TempDir()
	dataPath := filepath.Join(tmpDir, "nonexistent.wav.data")

	result, err := CallsAdd(CallsAddInput{
		File:      dataPath,
		Segment:   "2-5",
		Species:   "Kiwi",
		Certainty: 100,
		Filter:    "Manual",
		Reviewer:  "David",
	})

	if err == nil {
		t.Error("expected error when WAV file missing, got nil")
	}
	if result.Error == "" {
		t.Error("expected error message in output")
	}
}

func TestCallsAddMissingSpecies(t *testing.T) {
	result, err := CallsAdd(CallsAddInput{
		File:     "test.data",
		Segment:  "2-5",
		Reviewer: "David",
	})

	if err == nil {
		t.Error("expected error for missing species, got nil")
	}
	if result.Error == "" {
		t.Error("expected error message in output")
	}
}

func TestCallsAddClampsToDuration(t *testing.T) {
	tmpDir := t.TempDir()
	wavPath := filepath.Join(tmpDir, "test.wav")
	dataPath := wavPath + ".data"

	createTestWAV(t, wavPath, 16000, 10)

	result, err := CallsAdd(CallsAddInput{
		File:      dataPath,
		Segment:   "8-15",
		Species:   "sample",
		Certainty: 100,
		Filter:    "Manual",
		Reviewer:  "David",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !result.Clamped {
		t.Error("expected Clamped=true when end exceeds duration")
	}
	if result.SegmentEnd != 10.0 {
		t.Errorf("expected segment_end clamped to 10.0, got %f", result.SegmentEnd)
	}
	if result.SegmentStart != 8.0 {
		t.Errorf("expected segment_start=8.0, got %f", result.SegmentStart)
	}

	df, err := datafile.ParseDataFile(dataPath)
	if err != nil {
		t.Fatalf("failed to parse file: %v", err)
	}
	if len(df.Segments) != 1 {
		t.Fatalf("expected 1 segment, got %d", len(df.Segments))
	}
	if df.Segments[0].EndTime != 10.0 {
		t.Errorf("expected persisted EndTime=10.0, got %f", df.Segments[0].EndTime)
	}
}

func TestCallsAddRejectsStartAtOrPastDuration(t *testing.T) {
	tmpDir := t.TempDir()
	wavPath := filepath.Join(tmpDir, "test.wav")
	dataPath := wavPath + ".data"

	createTestWAV(t, wavPath, 16000, 10)

	cases := []struct {
		name    string
		segment string
	}{
		{"start equals duration", "10-15"},
		{"start past duration", "12-15"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			result, err := CallsAdd(CallsAddInput{
				File:      dataPath,
				Segment:   tc.segment,
				Species:   "sample",
				Certainty: 100,
				Filter:    "Manual",
				Reviewer:  "David",
			})
			if err == nil {
				t.Errorf("expected error for segment %s, got nil", tc.segment)
			}
			if result.Error == "" {
				t.Error("expected error message in output")
			}
			if _, statErr := os.Stat(dataPath); !os.IsNotExist(statErr) {
				t.Error(".data file should not be created for impossible segment")
			}
		})
	}
}

func TestCallsAddExactDurationBoundary(t *testing.T) {
	tmpDir := t.TempDir()
	wavPath := filepath.Join(tmpDir, "test.wav")
	dataPath := wavPath + ".data"

	createTestWAV(t, wavPath, 16000, 10)

	result, err := CallsAdd(CallsAddInput{
		File:      dataPath,
		Segment:   "0-10",
		Species:   "sample",
		Certainty: 100,
		Filter:    "Manual",
		Reviewer:  "David",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result.Clamped {
		t.Error("expected Clamped=false when end == duration exactly")
	}
	if result.SegmentEnd != 10.0 {
		t.Errorf("expected segment_end=10.0, got %f", result.SegmentEnd)
	}
}

// createTestWAV creates a minimal valid WAV file for testing.
func createTestWAV(t *testing.T, path string, sampleRate int, durationSec int) {
	t.Helper()
	numSamples := sampleRate * durationSec
	dataSize := numSamples * 2 // 16-bit = 2 bytes per sample
	fileSize := 36 + dataSize

	f, err := os.Create(path)
	if err != nil {
		t.Fatalf("failed to create WAV file: %v", err)
	}
	defer f.Close()

	f.Write([]byte("RIFF"))
	writeUint32(f, uint32(fileSize))
	f.Write([]byte("WAVE"))
	f.Write([]byte("fmt "))
	writeUint32(f, 16)
	writeUint16(f, 1)
	writeUint16(f, 1)
	writeUint32(f, uint32(sampleRate))
	writeUint32(f, uint32(sampleRate*2))
	writeUint16(f, 2)
	writeUint16(f, 16)
	f.Write([]byte("data"))
	writeUint32(f, uint32(dataSize))
	silence := make([]byte, dataSize)
	f.Write(silence)
}

func writeUint32(f *os.File, v uint32) {
	buf := make([]byte, 4)
	buf[0] = byte(v)
	buf[1] = byte(v >> 8)
	buf[2] = byte(v >> 16)
	buf[3] = byte(v >> 24)
	f.Write(buf)
}

func writeUint16(f *os.File, v uint16) {
	buf := make([]byte, 2)
	buf[0] = byte(v)
	buf[1] = byte(v >> 8)
	f.Write(buf)
}