package gobackend

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestLibraryScanFullIncrementalAndMetadataFallbacks(t *testing.T) {
	dir := t.TempDir()
	albumDir := filepath.Join(dir, "Album")
	if err := os.MkdirAll(albumDir, 0755); err != nil {
		t.Fatal(err)
	}
	mp3Path := filepath.Join(albumDir, "Artist - Song.mp3")
	if err := os.WriteFile(mp3Path, []byte("not really mp3"), 0600); err != nil {
		t.Fatal(err)
	}
	numberedPath := filepath.Join(albumDir, "01 - Intro.ogg")
	if err := os.WriteFile(numberedPath, []byte("not really ogg"), 0600); err != nil {
		t.Fatal(err)
	}
	apePath := filepath.Join(albumDir, "tagged.ape")
	if err := os.WriteFile(apePath, []byte("audio"), 0600); err != nil {
		t.Fatal(err)
	}
	if err := WriteAPETags(apePath, &APETag{Items: AudioMetadataToAPEItems(&AudioMetadata{
		Title:       "Tagged",
		Artist:      "APE Artist",
		Album:       "APE Album",
		TrackNumber: 2,
		TotalTracks: 3,
		Date:        "2026",
		Genre:       "Pop",
		Composer:    "Composer",
	})}); err != nil {
		t.Fatalf("write ape tags: %v", err)
	}
	cuePath, _ := writeExportCueFixture(t, albumDir)
	if err := os.WriteFile(filepath.Join(albumDir, "ignored.txt"), []byte("ignore"), 0600); err != nil {
		t.Fatal(err)
	}
	legacyPartialPath := filepath.Join(albumDir, "Artist - Song.partial.flac")
	if err := os.WriteFile(legacyPartialPath, []byte("partial flac"), 0600); err != nil {
		t.Fatal(err)
	}
	newPartialPath := filepath.Join(albumDir, "Artist - Song.flac.partial")
	if err := os.WriteFile(newPartialPath, []byte("partial flac"), 0600); err != nil {
		t.Fatal(err)
	}

	files, err := collectLibraryAudioFiles(dir, make(chan struct{}))
	if err != nil {
		t.Fatalf("collectLibraryAudioFiles: %v", err)
	}
	if len(files) < 4 {
		t.Fatalf("files = %#v", files)
	}
	for _, file := range files {
		if file.path == legacyPartialPath || file.path == newPartialPath {
			t.Fatalf("staging file should be ignored: %#v", files)
		}
	}
	cancelCh := make(chan struct{})
	close(cancelCh)
	if _, err := collectLibraryAudioFiles(dir, cancelCh); err == nil {
		t.Fatal("expected cancelled collect")
	}

	jsonText, err := ScanLibraryFolder(dir)
	if err != nil {
		t.Fatalf("ScanLibraryFolder: %v", err)
	}
	var results []LibraryScanResult
	if err := json.Unmarshal([]byte(jsonText), &results); err != nil {
		t.Fatalf("decode scan results: %v", err)
	}
	if len(results) < 4 {
		t.Fatalf("scan results = %#v", results)
	}
	foundTagged := false
	for _, result := range results {
		if result.FilePath == apePath {
			foundTagged = result.TrackName == "Tagged" && result.ArtistName == "APE Artist"
		}
	}
	if !foundTagged {
		t.Fatalf("tagged APE not found in %#v", results)
	}

	ndjsonPath := filepath.Join(t.TempDir(), "library.ndjson")
	streamedCount, err := ScanLibraryFolderToNDJSONFile(dir, ndjsonPath)
	if err != nil {
		t.Fatalf("ScanLibraryFolderToNDJSONFile: %v", err)
	}
	ndjsonFile, err := os.Open(ndjsonPath)
	if err != nil {
		t.Fatal(err)
	}
	defer ndjsonFile.Close()
	decodedCount := 0
	scanner := bufio.NewScanner(ndjsonFile)
	for scanner.Scan() {
		var result LibraryScanResult
		if err := json.Unmarshal(scanner.Bytes(), &result); err != nil {
			t.Fatalf("decode NDJSON row: %v", err)
		}
		if result.FilePath == "" {
			t.Fatal("NDJSON row has no file path")
		}
		decodedCount++
	}
	if err := scanner.Err(); err != nil {
		t.Fatal(err)
	}
	if decodedCount != streamedCount || decodedCount != len(results) {
		t.Fatalf("NDJSON counts = decoded:%d streamed:%d array:%d", decodedCount, streamedCount, len(results))
	}
	if progress := GetLibraryScanProgress(); !strings.Contains(progress, `"IsComplete":true`) && !strings.Contains(progress, `"is_complete":true`) {
		t.Fatalf("progress = %s", progress)
	}

	metaJSON, err := ReadAudioMetadataWithDisplayName(mp3Path, "Display Artist - Display Song.mp3")
	if err != nil {
		t.Fatalf("ReadAudioMetadataWithDisplayName: %v", err)
	}
	if !strings.Contains(metaJSON, "Display Song") {
		t.Fatalf("metadata json = %s", metaJSON)
	}
	noExtPath := filepath.Join(albumDir, "noext")
	if err := os.WriteFile(noExtPath, []byte("audio"), 0600); err != nil {
		t.Fatal(err)
	}
	noExtJSON, err := ReadAudioMetadataWithDisplayNameAndCoverCacheKey(noExtPath, "Artist - No Ext.mp3", "cache-key")
	if err != nil {
		t.Fatalf("ReadAudioMetadataWithDisplayNameAndCoverCacheKey: %v", err)
	}
	if !strings.Contains(noExtJSON, "No Ext") {
		t.Fatalf("no ext metadata = %s", noExtJSON)
	}

	existing := map[string]int64{}
	for _, file := range files {
		existing[file.path] = file.modTime
	}
	if info, err := os.Stat(cuePath); err == nil {
		existing[cuePath+"#track01"] = info.ModTime().UnixMilli()
	}
	incJSON, err := scanLibraryFolderIncrementalWithExistingFiles(dir, existing)
	if err != nil {
		t.Fatalf("incremental existing: %v", err)
	}
	var inc IncrementalScanResult
	if err := json.Unmarshal([]byte(incJSON), &inc); err != nil {
		t.Fatalf("decode incremental: %v", err)
	}
	if inc.SkippedCount == 0 {
		t.Fatalf("incremental = %#v", inc)
	}
	if _, err := ScanLibraryFolderIncremental("", "{}"); err == nil {
		t.Fatal("expected empty incremental folder error")
	}
	if incJSON, err := ScanLibraryFolderIncremental(dir, `not-json`); err != nil || incJSON == "" {
		t.Fatalf("incremental invalid existing JSON = %q/%v", incJSON, err)
	}

	snapshot := filepath.Join(dir, "snapshot.txt")
	if err := os.WriteFile(snapshot, []byte("bad\n123\t"+mp3Path+"\nnotint\tpath\n999\t"+filepath.Join(dir, "deleted.mp3")+"\n"), 0600); err != nil {
		t.Fatal(err)
	}
	fromSnapshot, err := ScanLibraryFolderIncrementalFromSnapshot(dir, snapshot)
	if err != nil {
		t.Fatalf("snapshot incremental: %v", err)
	}
	if !strings.Contains(fromSnapshot, "deleted.mp3") {
		t.Fatalf("snapshot result = %s", fromSnapshot)
	}
	if _, err := ScanLibraryFolder(""); err == nil {
		t.Fatal("expected empty folder scan error")
	}
	fileInsteadOfFolder := filepath.Join(dir, "file.flac")
	if err := os.WriteFile(fileInsteadOfFolder, []byte("audio"), 0600); err != nil {
		t.Fatal(err)
	}
	if _, err := ScanLibraryFolder(fileInsteadOfFolder); err == nil {
		t.Fatal("expected not folder error")
	}
	CancelLibraryScan()
	SetLibraryCoverCacheDir("")
}

func TestScanLibraryFolderPreservesFileOrderWithParallelWorkers(t *testing.T) {
	dir := t.TempDir()
	for i := 0; i < 20; i++ {
		name := fmt.Sprintf("%02d - Track.mp3", i)
		if err := os.WriteFile(filepath.Join(dir, name), []byte("not really mp3"), 0600); err != nil {
			t.Fatal(err)
		}
	}

	jsonText, err := ScanLibraryFolder(dir)
	if err != nil {
		t.Fatal(err)
	}
	var results []LibraryScanResult
	if err := json.Unmarshal([]byte(jsonText), &results); err != nil {
		t.Fatal(err)
	}
	if len(results) != 20 {
		t.Fatalf("results = %d", len(results))
	}
	for i, result := range results {
		expected := filepath.Join(dir, fmt.Sprintf("%02d - Track.mp3", i))
		if result.FilePath != expected {
			t.Fatalf("result %d path = %q, want %q", i, result.FilePath, expected)
		}
	}
}
