Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

test_helpers.go
package imp

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"testing"

	"skraak/datafile"
	"skraak/db"
	"skraak/utils"
)

// setupImportTestDB creates an in-memory DuckDB with the full schema and test data.
//
// Test data:
//   - Structured dataset:  dstest000001
//   - Unstructured dataset: dstest000002
//   - Location (active):    loctest00001 (in dstest000001)
//   - Location (inactive):  loctest00002 (in dstest000001)
//   - Cluster (active):     cltest000001 (in loctest00001)
//   - Cluster (inactive):   cltest000002 (in loctest00001)
//   - Species: Kiwi (sptest000001), Roroa (sptest000002)
//   - Calltypes: Kiwi/song (cttest000001), Kiwi/duet (cttest000002)
//   - Filters: kiwi.txt (fitest000001), test.txt (fitest000002)
func setupImportTestDB(t *testing.T) *sql.DB {
	t.Helper()
	database := db.SetupTestDB(t)

	// Datasets
	db.InsertTestDatasetWithType(t, database, "dstest000001", "Test Structured", "structured")
	db.InsertTestDatasetWithType(t, database, "dstest000002", "Test Unstructured", "unstructured")

	// Locations
	db.InsertTestLocation(t, database, "loctest00001", "dstest000001", "Test Location Active")
	db.InsertTestLocation(t, database, "loctest00002", "dstest000001", "Test Location Inactive")
	mustExec(t, database, "UPDATE location SET active = false WHERE id = 'loctest00002'")

	// Clusters
	db.InsertTestCluster(t, database, "cltest000001", "dstest000001", "loctest00001", "Test Cluster Active")
	db.InsertTestCluster(t, database, "cltest000002", "dstest000001", "loctest00001", "Test Cluster Inactive")
	mustExec(t, database, "UPDATE cluster SET active = false WHERE id = 'cltest000002'")

	// Species
	db.InsertTestSpecies(t, database, "sptest000001", "Kiwi")
	db.InsertTestSpecies(t, database, "sptest000002", "Roroa")

	// Calltypes
	db.InsertTestCallType(t, database, "cttest000001", "sptest000001", "song")
	db.InsertTestCallType(t, database, "cttest000002", "sptest000001", "duet")

	// Filters
	db.InsertTestFilter(t, database, "fitest000001", "kiwi.txt")
	db.InsertTestFilter(t, database, "fitest000002", "test.txt")

	return database
}

// setupFileBasedTestDB creates a file-based DuckDB for tests that need to
// open multiple connections to the same database (e.g., ImportAudioFiles).
// Returns the path to the database file. The database is closed after setup.
func setupFileBasedTestDB(t *testing.T) string {
	t.Helper()

	// Create temp file for database
	tmpDir := t.TempDir()
	dbPath := filepath.Join(tmpDir, "test.duckdb")

	// Open database
	database, err := sql.Open("duckdb", dbPath)
	if err != nil {
		t.Fatalf("failed to open database: %v", err)
	}

	// Apply schema
	schema, err := db.ReadSchemaSQL()
	if err != nil {
		database.Close()
		t.Fatalf("failed to read schema: %v", err)
	}
	if _, err = database.Exec(schema); err != nil {
		database.Close()
		t.Fatalf("failed to create schema: %v", err)
	}

	// Insert test data - same as setupImportTestDB
	db.InsertTestDatasetWithType(t, database, "dstest000001", "Test Structured", "structured")
	db.InsertTestDatasetWithType(t, database, "dstest000002", "Test Unstructured", "unstructured")

	db.InsertTestLocation(t, database, "loctest00001", "dstest000001", "Test Location Active")
	db.InsertTestLocation(t, database, "loctest00002", "dstest000001", "Test Location Inactive")
	mustExec(t, database, "UPDATE location SET active = false WHERE id = 'loctest00002'")

	db.InsertTestCluster(t, database, "cltest000001", "dstest000001", "loctest00001", "Test Cluster Active")
	db.InsertTestCluster(t, database, "cltest000002", "dstest000001", "loctest00001", "Test Cluster Inactive")
	mustExec(t, database, "UPDATE cluster SET active = false WHERE id = 'cltest000002'")

	db.InsertTestSpecies(t, database, "sptest000001", "Kiwi")
	db.InsertTestSpecies(t, database, "sptest000002", "Roroa")

	db.InsertTestCallType(t, database, "cttest000001", "sptest000001", "song")
	db.InsertTestCallType(t, database, "cttest000002", "sptest000001", "duet")

	db.InsertTestFilter(t, database, "fitest000001", "kiwi.txt")
	db.InsertTestFilter(t, database, "fitest000002", "test.txt")

	// Close the database so tests can open their own connections
	database.Close()

	return dbPath
}

// mustExec executes a SQL statement, failing the test on error.
func mustExec(t *testing.T, database *sql.DB, query string, args ...any) {
	t.Helper()
	if _, err := database.Exec(query, args...); err != nil {
		t.Fatalf("exec: %v", err)
	}
}

// createTestWAV creates a minimal valid WAV file at the given path.
// Returns the XXH64 hash of the file.
func createTestWAV(t *testing.T, path string) string {
	t.Helper()

	// Create a 1-second WAV file at 16kHz mono 16-bit
	// 44-byte header + 32000 bytes of data (16000 samples * 2 bytes)
	const sampleRate = 16000
	const numSamples = sampleRate   // 1 second
	const dataSize = numSamples * 2 // 2 bytes per sample
	const fileSize = 44 + dataSize - 8

	data := make([]byte, 44+dataSize)

	// RIFF header
	copy(data[0:4], "RIFF")
	data[4] = byte(fileSize & 0xFF)
	data[5] = byte((fileSize >> 8) & 0xFF)
	data[6] = byte((fileSize >> 16) & 0xFF)
	data[7] = byte((fileSize >> 24) & 0xFF)
	copy(data[8:12], "WAVE")

	// fmt chunk
	copy(data[12:16], "fmt ")
	data[16] = 16 // fmt chunk size
	data[20] = 1  // PCM format
	data[22] = 1  // mono
	data[24] = byte(sampleRate & 0xFF)
	data[25] = byte((sampleRate >> 8) & 0xFF)
	const byteRate = sampleRate * 2
	data[28] = byte(byteRate & 0xFF)
	data[29] = byte((byteRate >> 8) & 0xFF)
	data[32] = 2  // block align
	data[34] = 16 // bits per sample

	// data chunk
	copy(data[36:40], "data")
	data[40] = byte(dataSize & 0xFF)
	data[41] = byte((dataSize >> 8) & 0xFF)
	data[42] = byte((dataSize >> 16) & 0xFF)
	data[43] = byte((dataSize >> 24) & 0xFF)

	// Audio data is already zeros

	if err := os.WriteFile(path, data, 0644); err != nil {
		t.Fatalf("failed to create test WAV: %v", err)
	}

	hash, err := utils.ComputeXXH64(path)
	if err != nil {
		t.Fatalf("failed to compute hash: %v", err)
	}

	return hash
}

// createTestDataFile creates a minimal .data file with the given segments.
func createTestDataFile(t *testing.T, wavPath string, segments []*datafile.Segment) string {
	t.Helper()

	dataPath := wavPath + ".data"

	df := &datafile.DataFile{
		Meta: &datafile.DataMeta{
			Operator: "test",
			Duration: 0.0005,
		},
		Segments: segments,
	}

	if err := df.Write(dataPath); err != nil {
		t.Fatalf("failed to write test .data file: %v", err)
	}

	return dataPath
}

// createTestMappingFile creates a minimal mapping.json file.
func createTestMappingFile(t *testing.T, dir string) string {
	t.Helper()

	mapping := map[string]any{
		"Kiwi": map[string]any{
			"species": "Kiwi",
			"calltypes": map[string]string{
				"song": "song",
				"duet": "duet",
			},
		},
		"Roroa": map[string]any{
			"species": "Roroa",
		},
	}

	data, err := json.Marshal(mapping)
	if err != nil {
		t.Fatalf("failed to marshal mapping: %v", err)
	}

	path := filepath.Join(dir, "mapping.json")
	if err := os.WriteFile(path, data, 0644); err != nil {
		t.Fatalf("failed to write mapping file: %v", err)
	}

	return path
}

// createTestCSVFile creates a CSV file for bulk import testing.
// Columns: location_name, location_id, directory_path, date_range, sample_rate, file_count
func createTestCSVFile(t *testing.T, dir string, rows [][]string) string {
	t.Helper()

	path := filepath.Join(dir, "import.csv")
	file, err := os.Create(path)
	if err != nil {
		t.Fatalf("failed to create CSV: %v", err)
	}
	defer func() { _ = file.Close() }()

	// Write header
	if _, err := file.WriteString("location_name,location_id,directory_path,date_range,sample_rate,file_count\n"); err != nil {
		t.Fatalf("failed to write CSV header: %v", err)
	}

	// Write rows
	for _, row := range rows {
		line := fmt.Sprintf("%s,%s,%s,%s,%s,%s\n", row[0], row[1], row[2], row[3], row[4], row[5])
		if _, err := file.WriteString(line); err != nil {
			t.Fatalf("failed to write CSV row: %v", err)
		}
	}

	return path
}

// createTestLogFile creates a log file path for bulk import testing.
func createTestLogFile(t *testing.T, dir string) string {
	t.Helper()
	path := filepath.Join(dir, "import.log")
	// Create empty file
	if err := os.WriteFile(path, []byte{}, 0644); err != nil {
		t.Fatalf("failed to create log file: %v", err)
	}
	return path
}

// beginTestTx begins a logged transaction for testing.
func beginTestTx(t *testing.T, ctx context.Context, database *sql.DB) *db.LoggedTx {
	t.Helper()
	tx, err := db.BeginLoggedTx(ctx, database, "test")
	if err != nil {
		t.Fatalf("failed to begin transaction: %v", err)
	}
	return tx
}