package docs

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"reflect"
	"testing"
	"time"

	"github.com/tim/cairnquire/apps/server/internal/database"
	"github.com/tim/cairnquire/apps/server/internal/markdown"
	"github.com/tim/cairnquire/apps/server/internal/store"
)

func TestNormalizeRequestPathCandidatesSupportsIndexFiles(t *testing.T) {
	tests := []struct {
		name string
		path string
		want []string
	}{
		{
			name: "root",
			path: "",
			want: []string{"index.md", "getting-started.md"},
		},
		{
			name: "clean folder or page path",
			path: "guide",
			want: []string{"guide.md", "guide/index.md"},
		},
		{
			name: "explicit markdown path",
			path: "guide/index.md",
			want: []string{"guide/index.md"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := normalizeRequestPathCandidates(tt.path)
			if !reflect.DeepEqual(got, tt.want) {
				t.Fatalf("normalizeRequestPathCandidates(%q) = %#v, want %#v", tt.path, got, tt.want)
			}
		})
	}
}

func TestSaveSourcePageWithBaseHashRejectsStaleEditor(t *testing.T) {
	service, sourceDir := setupDocsTestService(t)
	ctx := context.Background()

	page, err := service.LoadSourcePage(ctx, "hello")
	if err != nil {
		t.Fatalf("LoadSourcePage() error = %v", err)
	}

	serverContent := "# Hello\n\nServer edit"
	if err := os.WriteFile(filepath.Join(sourceDir, "hello.md"), []byte(serverContent), 0o644); err != nil {
		t.Fatalf("write server edit: %v", err)
	}

	_, err = service.SaveSourcePageWithBaseHash(ctx, "hello", "# Hello\n\nClient edit", page.Hash)
	var conflict *DocumentConflictError
	if !errors.As(err, &conflict) {
		t.Fatalf("SaveSourcePageWithBaseHash() error = %v, want DocumentConflictError", err)
	}
	if conflict.BaseHash != page.Hash {
		t.Fatalf("conflict base hash = %q, want %q", conflict.BaseHash, page.Hash)
	}
	if conflict.CurrentContent != serverContent {
		t.Fatalf("conflict current content = %q, want %q", conflict.CurrentContent, serverContent)
	}

	content, err := os.ReadFile(filepath.Join(sourceDir, "hello.md"))
	if err != nil {
		t.Fatalf("read hello.md: %v", err)
	}
	if string(content) != serverContent {
		t.Fatalf("stale save overwrote content: %q", string(content))
	}
}

func TestSaveSourcePageWithBaseHashAcceptsMatchingHash(t *testing.T) {
	service, sourceDir := setupDocsTestService(t)
	ctx := context.Background()

	page, err := service.LoadSourcePage(ctx, "hello")
	if err != nil {
		t.Fatalf("LoadSourcePage() error = %v", err)
	}

	nextContent := "# Hello\n\nClient edit"
	updated, err := service.SaveSourcePageWithBaseHash(ctx, "hello", nextContent, page.Hash)
	if err != nil {
		t.Fatalf("SaveSourcePageWithBaseHash() error = %v", err)
	}
	if updated.Hash == page.Hash {
		t.Fatal("expected hash to change after save")
	}

	content, err := os.ReadFile(filepath.Join(sourceDir, "hello.md"))
	if err != nil {
		t.Fatalf("read hello.md: %v", err)
	}
	if string(content) != nextContent {
		t.Fatalf("saved content = %q, want %q", string(content), nextContent)
	}
}

func TestSaveSourcePageWithBaseHashCanResolveWithLatestHash(t *testing.T) {
	service, sourceDir := setupDocsTestService(t)
	ctx := context.Background()

	page, err := service.LoadSourcePage(ctx, "hello")
	if err != nil {
		t.Fatalf("LoadSourcePage() error = %v", err)
	}

	serverContent := "# Hello\n\nServer edit"
	if err := os.WriteFile(filepath.Join(sourceDir, "hello.md"), []byte(serverContent), 0o644); err != nil {
		t.Fatalf("write server edit: %v", err)
	}

	_, err = service.SaveSourcePageWithBaseHash(ctx, "hello", "# Hello\n\nQueued offline edit", page.Hash)
	var conflict *DocumentConflictError
	if !errors.As(err, &conflict) {
		t.Fatalf("SaveSourcePageWithBaseHash() error = %v, want DocumentConflictError", err)
	}

	resolvedContent := conflict.CurrentContent + "\n\nResolved local addition"
	updated, err := service.SaveSourcePageWithBaseHash(ctx, "hello", resolvedContent, conflict.CurrentHash)
	if err != nil {
		t.Fatalf("SaveSourcePageWithBaseHash() latest hash error = %v", err)
	}
	if updated.Hash == conflict.CurrentHash {
		t.Fatal("expected resolved save to produce a new hash")
	}

	content, err := os.ReadFile(filepath.Join(sourceDir, "hello.md"))
	if err != nil {
		t.Fatalf("read hello.md: %v", err)
	}
	if string(content) != resolvedContent {
		t.Fatalf("resolved content = %q, want %q", string(content), resolvedContent)
	}
}

func TestSaveSourcePageCreatesNewDocument(t *testing.T) {
	service, sourceDir := setupDocsTestService(t)
	ctx := context.Background()

	content := "# Inbox Note\n\nCreated from clipboard"
	page, err := service.SaveSourcePageWithBaseHash(ctx, "inbox/note", content, "")
	if err != nil {
		t.Fatalf("SaveSourcePageWithBaseHash() error = %v", err)
	}
	if page.Path != "inbox/note.md" {
		t.Fatalf("page path = %q, want inbox/note.md", page.Path)
	}

	written, err := os.ReadFile(filepath.Join(sourceDir, "inbox", "note.md"))
	if err != nil {
		t.Fatalf("read new document: %v", err)
	}
	if string(written) != content {
		t.Fatalf("new document content = %q, want %q", string(written), content)
	}
}

func TestSaveSourcePageRejectsTraversalForNewDocument(t *testing.T) {
	service, _ := setupDocsTestService(t)
	ctx := context.Background()

	_, err := service.SaveSourcePageWithBaseHash(ctx, "../outside", "# Outside\n", "")
	if err == nil {
		t.Fatal("expected traversal path to be rejected")
	}
}

func TestSyncSourceDirDoesNotRebroadcastArchivedDocuments(t *testing.T) {
	service, _ := setupDocsTestService(t)
	ctx := context.Background()

	if _, err := service.SyncSourceDir(ctx); err != nil {
		t.Fatalf("initial SyncSourceDir() error = %v", err)
	}

	var changed []DocumentChange
	service.OnChange(func(change DocumentChange) {
		changed = append(changed, change)
	})

	if err := service.ArchiveDocument(ctx, "hello.md"); err != nil {
		t.Fatalf("ArchiveDocument() error = %v", err)
	}
	if len(changed) != 1 {
		t.Fatalf("changes after archive = %d, want 1", len(changed))
	}

	changed = nil
	changes, err := service.SyncSourceDir(ctx)
	if err != nil {
		t.Fatalf("SyncSourceDir() error = %v", err)
	}
	if len(changes) != 0 {
		t.Fatalf("sync changes = %#v, want none for archived document", changes)
	}
	if len(changed) != 0 {
		t.Fatalf("broadcast changes = %#v, want none for archived document", changed)
	}
}

func TestSyncSourceDirHandles100FilesUnderTarget(t *testing.T) {
	service, sourceDir := setupDocsTestService(t)
	ctx := context.Background()

	for i := 0; i < 100; i++ {
		name := filepath.Join(sourceDir, fmt.Sprintf("note-%03d.md", i))
		content := fmt.Sprintf("# Note %03d\n\nDeterministic body %03d\n", i, i)
		if err := os.WriteFile(name, []byte(content), 0o644); err != nil {
			t.Fatalf("create %s: %v", name, err)
		}
	}

	start := time.Now()
	changes, err := service.SyncSourceDir(ctx)
	if err != nil {
		t.Fatalf("SyncSourceDir() error = %v", err)
	}
	if elapsed := time.Since(start); elapsed > 10*time.Second {
		t.Fatalf("SyncSourceDir() took %s, want <= 10s", elapsed)
	}
	if len(changes) != 101 {
		t.Fatalf("changes = %d, want 101", len(changes))
	}
}

func setupDocsTestService(t *testing.T) (*Service, string) {
	t.Helper()

	sourceDir := t.TempDir()
	storeDir := t.TempDir()
	if err := os.WriteFile(filepath.Join(sourceDir, "hello.md"), []byte("# Hello\n\nOriginal"), 0o644); err != nil {
		t.Fatalf("create hello.md: %v", err)
	}

	dbPath := filepath.Join(t.TempDir(), "test.db")
	db, err := sql.Open("libsql", "file:"+dbPath)
	if err != nil {
		t.Fatalf("open database: %v", err)
	}
	t.Cleanup(func() { db.Close() })

	ctx := context.Background()
	if err := database.ApplyMigrations(ctx, db); err != nil {
		t.Fatalf("apply migrations: %v", err)
	}

	contentStore, err := store.New(storeDir)
	if err != nil {
		t.Fatalf("create content store: %v", err)
	}

	repo := NewRepository(db)
	return NewService(sourceDir, contentStore, markdown.NewRenderer(), repo, slog.Default()), sourceDir
}
