package library

import (
	"bytes"
	"context"
	"encoding/binary"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"testing"

	"github.com/jackc/pgx/v5/pgxpool"

	"git.fabledsword.com/bvandeusen/minstrel/internal/db"
	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)

func TestReleaseDateFromYear(t *testing.T) {
	cases := []struct {
		in    int
		ok    bool
		label string
	}{
		{0, false, "missing tag"},
		{-1, false, "negative"},
		{1, true, "lower bound"},
		{1999, true, "ordinary"},
		{9999, true, "upper bound"},
		{10000, false, "five digits"},
		{99999999, false, "tag corruption"},
	}
	for _, c := range cases {
		got, ok := releaseDateFromYear(c.in)
		if ok != c.ok {
			t.Errorf("%s: ok = %v, want %v", c.label, ok, c.ok)
		}
		if ok && (!got.Valid || got.Time.Year() != c.in) {
			t.Errorf("%s: date = %+v, want year=%d", c.label, got, c.in)
		}
	}
}

// TestScanner_Integration exercises walk → tag-parse → upsert → incremental
// skip against a real Postgres. Gated on MINSTREL_TEST_DATABASE_URL.
func TestScanner_Integration(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping scanner integration in -short mode")
	}
	dsn := os.Getenv("MINSTREL_TEST_DATABASE_URL")
	if dsn == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	ctx := context.Background()
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))

	if err := db.Migrate(dsn, logger); err != nil {
		t.Fatalf("migrate: %v", err)
	}
	pool, err := pgxpool.New(ctx, dsn)
	if err != nil {
		t.Fatalf("pool: %v", err)
	}
	t.Cleanup(pool.Close)

	if _, err := pool.Exec(ctx, "TRUNCATE tracks, albums, artists RESTART IDENTITY CASCADE"); err != nil {
		t.Fatalf("truncate: %v", err)
	}

	root := t.TempDir()
	writeTestMP3(t, filepath.Join(root, "artistX/albumA/01.mp3"), map[string]string{
		"TIT2": "Track One", "TPE1": "Artist X", "TALB": "Album A", "TRCK": "1", "TYER": "2020",
	})
	writeTestMP3(t, filepath.Join(root, "artistX/albumA/02.mp3"), map[string]string{
		"TIT2": "Track Two", "TPE1": "Artist X", "TALB": "Album A", "TRCK": "2", "TYER": "2020",
	})
	writeTestMP3(t, filepath.Join(root, "artistX/albumB/01.mp3"), map[string]string{
		"TIT2": "B Song", "TPE1": "Artist X", "TALB": "Album B", "TRCK": "1",
	})
	writeTestMP3(t, filepath.Join(root, "artistY/01.mp3"), map[string]string{
		"TIT2": "Solo", "TPE1": "The Artist Y", "TALB": "Y Album", "TRCK": "1",
	})

	scanner := New(pool, logger, []string{root})
	stats, err := scanner.Scan(ctx)
	if err != nil {
		t.Fatalf("first scan: %v", err)
	}
	if stats.Scanned != 4 || stats.Added != 4 {
		t.Errorf("first scan stats = %+v, want Scanned=4 Added=4", stats)
	}

	q := dbq.New(pool)
	artists, err := q.ListArtists(ctx)
	if err != nil {
		t.Fatalf("ListArtists: %v", err)
	}
	if len(artists) != 2 {
		t.Fatalf("artist rows = %d (%+v), want 2", len(artists), artists)
	}
	// "The Artist Y" should sort under "Artist Y" (article stripped).
	if artists[0].SortName != "Artist X" || artists[1].SortName != "Artist Y" {
		t.Errorf("artist sort = [%q, %q], want [Artist X, Artist Y]", artists[0].SortName, artists[1].SortName)
	}

	stats2, err := scanner.Scan(ctx)
	if err != nil {
		t.Fatalf("second scan: %v", err)
	}
	if stats2.Added != 0 || stats2.Updated != 0 || stats2.Skipped != 4 {
		t.Errorf("incremental scan stats = %+v, want Skipped=4", stats2)
	}
}

// writeTestMP3 emits a file with only an ID3v2.3 tag payload plus a few
// bytes of MPEG-sync trailer. dhowden/tag reads tags without decoding audio,
// so this is enough for scanner round-trip tests without shipping binaries.
func writeTestMP3(t *testing.T, path string, frames map[string]string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatal(err)
	}
	var framesBuf bytes.Buffer
	for id, value := range frames {
		if len(id) != 4 {
			t.Fatalf("frame id %q not 4 bytes", id)
		}
		payload := append([]byte{0x03}, []byte(value)...)
		framesBuf.WriteString(id)
		_ = binary.Write(&framesBuf, binary.BigEndian, uint32(len(payload)))
		framesBuf.WriteByte(0x00)
		framesBuf.WriteByte(0x00)
		framesBuf.Write(payload)
	}
	total := framesBuf.Len()
	header := []byte{
		'I', 'D', '3', 0x03, 0x00, 0x00,
		byte((total >> 21) & 0x7F),
		byte((total >> 14) & 0x7F),
		byte((total >> 7) & 0x7F),
		byte(total & 0x7F),
	}
	f, err := os.Create(path)
	if err != nil {
		t.Fatal(err)
	}
	defer func() { _ = f.Close() }()
	if _, err := f.Write(header); err != nil {
		t.Fatal(err)
	}
	if _, err := f.Write(framesBuf.Bytes()); err != nil {
		t.Fatal(err)
	}
	if _, err := f.Write([]byte{0xFF, 0xFB, 0x90, 0x00}); err != nil {
		t.Fatal(err)
	}
}
