package sync

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

func setupTestDB(t *testing.T) *sql.DB {
	t.Helper()

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

	if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil {
		t.Fatalf("enable foreign keys: %v", err)
	}

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

	return db
}

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

	sourceDir := t.TempDir()
	storeDir := t.TempDir()

	db := setupTestDB(t)

	// Insert a test user to satisfy foreign key constraints
	if _, err := db.ExecContext(context.Background(), `
		INSERT INTO users (id, email, display_name, created_at)
		VALUES ('user:test', 'test@example.com', 'Test User', ?)
	`, time.Now().UTC().Format(time.RFC3339)); err != nil {
		t.Fatalf("insert test user: %v", err)
	}

	// Create some test markdown files
	if err := os.WriteFile(filepath.Join(sourceDir, "hello.md"), []byte("# Hello\n\nWorld"), 0o644); err != nil {
		t.Fatalf("create hello.md: %v", err)
	}
	if err := os.WriteFile(filepath.Join(sourceDir, "guide.md"), []byte("# Guide\n\nSteps"), 0o644); err != nil {
		t.Fatalf("create guide.md: %v", err)
	}

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

	renderer := markdown.NewRenderer()
	repo := docs.NewRepository(db)
	docService := docs.NewService(sourceDir, contentStore, renderer, repo, slog.Default())
	syncRepo := NewRepository(db)

	service := NewService(syncRepo, docService, contentStore, sourceDir, slog.Default())
	return service, sourceDir
}

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

	snap, err := service.InitSync(ctx, "device-1", "user:test")
	if err != nil {
		t.Fatalf("InitSync() error = %v", err)
	}

	if snap.ID == "" {
		t.Fatal("expected snapshot ID to be set")
	}
	if snap.DeviceID != "device-1" {
		t.Fatalf("expected device ID device-1, got %s", snap.DeviceID)
	}
	if len(snap.Files) != 2 {
		t.Fatalf("expected 2 files, got %d", len(snap.Files))
	}
}

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

	snap, err := service.InitSync(ctx, "device-1", "user:test")
	if err != nil {
		t.Fatalf("InitSync() error = %v", err)
	}

	// Modify a file on the server
	time.Sleep(10 * time.Millisecond)
	if err := os.WriteFile(filepath.Join(sourceDir, "hello.md"), []byte("# Hello\n\nUpdated"), 0o644); err != nil {
		t.Fatalf("update hello.md: %v", err)
	}

	// Client sends empty delta
	result, err := service.ApplyDelta(ctx, snap.ID, "user:test", Delta{Changes: nil})
	if err != nil {
		t.Fatalf("ApplyDelta() error = %v", err)
	}

	if len(result.ServerDelta) != 1 {
		t.Fatalf("expected 1 server delta, got %d", len(result.ServerDelta))
	}

	change := result.ServerDelta[0]
	if change.Type != ChangeUpdate {
		t.Fatalf("expected update change, got %s", change.Type)
	}
	if change.Path != "hello.md" {
		t.Fatalf("expected path hello.md, got %s", change.Path)
	}
	if len(result.Conflicts) != 0 {
		t.Fatalf("expected 0 conflicts, got %d", len(result.Conflicts))
	}
}

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

	snap, err := service.InitSync(ctx, "device-1", "user:test")
	if err != nil {
		t.Fatalf("InitSync() error = %v", err)
	}

	result, err := service.ApplyDelta(ctx, snap.ID, "user:test", Delta{})
	if err != nil {
		t.Fatalf("ApplyDelta() error = %v", err)
	}

	payload, err := json.Marshal(result)
	if err != nil {
		t.Fatalf("marshal delta result: %v", err)
	}
	for _, expected := range []string{`"serverDelta":[]`, `"conflicts":[]`} {
		if !strings.Contains(string(payload), expected) {
			t.Fatalf("delta JSON = %s, want %s", payload, expected)
		}
	}
}

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

	snap, err := service.InitSync(ctx, "device-1", "user:test")
	if err != nil {
		t.Fatalf("InitSync() error = %v", err)
	}

	// Both server and client change the same file
	time.Sleep(10 * time.Millisecond)
	if err := os.WriteFile(filepath.Join(sourceDir, "hello.md"), []byte("# Hello\n\nServer Update"), 0o644); err != nil {
		t.Fatalf("update hello.md: %v", err)
	}

	// Client also changes the file (different content = different hash)
	clientChange := Change{
		Type:     ChangeUpdate,
		Path:     "hello.md",
		Hash:     "clienthash123456789012345678901234567890123456789012345678901234",
		Size:     100,
		Modified: time.Now().UTC(),
	}

	result, err := service.ApplyDelta(ctx, snap.ID, "user:test", Delta{Changes: []Change{clientChange}})
	if err != nil {
		t.Fatalf("ApplyDelta() error = %v", err)
	}

	if len(result.Conflicts) != 1 {
		t.Fatalf("expected 1 conflict, got %d", len(result.Conflicts))
	}

	conflict := result.Conflicts[0]
	if conflict.Path != "hello.md" {
		t.Fatalf("expected conflict path hello.md, got %s", conflict.Path)
	}
	if conflict.Strategy != ResolutionLastWriteWins {
		t.Fatalf("expected strategy last-write-wins, got %s", conflict.Strategy)
	}
}

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

	snap, err := service.InitSync(ctx, "device-1", "user:test")
	if err != nil {
		t.Fatalf("InitSync() error = %v", err)
	}

	time.Sleep(10 * time.Millisecond)
	if err := os.WriteFile(filepath.Join(sourceDir, "guide.md"), []byte("# Guide\n\nServer-side guide update"), 0o644); err != nil {
		t.Fatalf("update guide.md: %v", err)
	}

	clientContent := "# Hello\n\nClient-side hello update"
	clientChange := Change{
		Type:     ChangeUpdate,
		Path:     "hello.md",
		Hash:     sha256Hex(clientContent),
		Content:  clientContent,
		Size:     int64(len(clientContent)),
		Modified: time.Now().UTC(),
	}

	result, err := service.ApplyDelta(ctx, snap.ID, "user:test", Delta{Changes: []Change{clientChange}})
	if err != nil {
		t.Fatalf("ApplyDelta() error = %v", err)
	}

	if len(result.Conflicts) != 0 {
		t.Fatalf("expected no conflicts for different-file edits, got %#v", result.Conflicts)
	}
	if len(result.ServerDelta) != 1 {
		t.Fatalf("expected one server delta for guide.md, got %d", len(result.ServerDelta))
	}
	if result.ServerDelta[0].Path != "guide.md" {
		t.Fatalf("server delta path = %q, want guide.md", result.ServerDelta[0].Path)
	}
}

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

	snap, err := service.InitSync(ctx, "device-1", "user:test")
	if err != nil {
		t.Fatalf("InitSync() error = %v", err)
	}

	content := "# Inbox\n\nCreated from macOS"
	result, err := service.ApplyDelta(ctx, snap.ID, "user:test", Delta{Changes: []Change{{
		Type:     ChangeCreate,
		Path:     "inbox/from-mac.md",
		Hash:     sha256Hex(content),
		Content:  content,
		Size:     int64(len(content)),
		Modified: time.Now().UTC(),
	}}})
	if err != nil {
		t.Fatalf("ApplyDelta() error = %v", err)
	}
	if len(result.Conflicts) != 0 {
		t.Fatalf("expected no conflicts, got %#v", result.Conflicts)
	}
	if result.NewSnapshotID == "" || result.NewSnapshotID == snap.ID {
		t.Fatalf("new snapshot id = %q, old = %q", result.NewSnapshotID, snap.ID)
	}

	written, err := os.ReadFile(filepath.Join(sourceDir, "inbox", "from-mac.md"))
	if err != nil {
		t.Fatalf("read synced file: %v", err)
	}
	if string(written) != content {
		t.Fatalf("synced content = %q, want %q", string(written), content)
	}

	stored, err := service.GetContent(sha256Hex(content))
	if err != nil {
		t.Fatalf("GetContent() error = %v", err)
	}
	if string(stored) != content {
		t.Fatalf("stored content = %q, want %q", string(stored), content)
	}
}

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

	snap, err := service.InitSync(ctx, "device-1", "user:test")
	if err != nil {
		t.Fatalf("InitSync() error = %v", err)
	}

	_, err = service.ApplyDelta(ctx, snap.ID, "user:test", Delta{Changes: []Change{{
		Type:    ChangeCreate,
		Path:    "bad.md",
		Hash:    "not-the-content-hash",
		Content: "# Bad\n",
	}}})
	if err == nil {
		t.Fatal("expected hash mismatch error")
	}
}

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

	snap, err := service.InitSync(ctx, "device-1", "user:test")
	if err != nil {
		t.Fatalf("InitSync() error = %v", err)
	}

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

	_, err = service.ResolveConflicts(ctx, snap.ID, "user:test", []Resolution{{
		Path:     "hello.md",
		Strategy: ResolutionServerWins,
	}})
	if err != nil {
		t.Fatalf("ResolveConflicts() error = %v", err)
	}

	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("resolved content = %q, want %q", string(content), serverContent)
	}
}

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

	snap, err := service.InitSync(ctx, "device-1", "user:test")
	if err != nil {
		t.Fatalf("InitSync() error = %v", err)
	}

	resolutions := []Resolution{
		{
			Path:     "hello.md",
			Strategy: ResolutionServerWins,
		},
	}

	newSnap, err := service.ResolveConflicts(ctx, snap.ID, "user:test", resolutions)
	if err != nil {
		t.Fatalf("ResolveConflicts() error = %v", err)
	}

	if newSnap.ID == "" {
		t.Fatal("expected new snapshot ID to be set")
	}
	if newSnap.ID == snap.ID {
		t.Fatal("expected new snapshot ID to be different from old")
	}
	if len(newSnap.Files) != 2 {
		t.Fatalf("expected 2 files in new snapshot, got %d", len(newSnap.Files))
	}
}

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

	snap, err := service.InitSync(ctx, "device-1", "user:test")
	if err != nil {
		t.Fatalf("InitSync() error = %v", err)
	}

	if len(snap.Files) == 0 {
		t.Fatal("expected at least one file")
	}

	hash := snap.Files[0].Hash
	content, err := service.GetContent(hash)
	if err != nil {
		t.Fatalf("GetContent() error = %v", err)
	}

	if len(content) == 0 {
		t.Fatal("expected non-empty content")
	}
}

func TestGetContentRejectsInvalidHash(t *testing.T) {
	service, _ := setupTestService(t)

	if _, err := service.GetContent("short"); err == nil {
		t.Fatal("expected invalid hash error")
	}
}

func TestSnapshotDeltaHandles100FilesUnderTarget(t *testing.T) {
	service, sourceDir := setupTestService(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()
	snap, err := service.InitSync(ctx, "device-100", "user:test")
	if err != nil {
		t.Fatalf("InitSync() error = %v", err)
	}
	if elapsed := time.Since(start); elapsed > 10*time.Second {
		t.Fatalf("InitSync() took %s, want <= 10s", elapsed)
	}
	if len(snap.Files) != 102 {
		t.Fatalf("snapshot files = %d, want 102", len(snap.Files))
	}

	if err := os.WriteFile(filepath.Join(sourceDir, "note-050.md"), []byte("# Note 050\n\nUpdated body\n"), 0o644); err != nil {
		t.Fatalf("update note-050.md: %v", err)
	}

	start = time.Now()
	result, err := service.ApplyDelta(ctx, snap.ID, "user:test", Delta{})
	if err != nil {
		t.Fatalf("ApplyDelta() error = %v", err)
	}
	if elapsed := time.Since(start); elapsed > 10*time.Second {
		t.Fatalf("ApplyDelta() took %s, want <= 10s", elapsed)
	}
	if len(result.ServerDelta) != 1 {
		t.Fatalf("server delta count = %d, want 1", len(result.ServerDelta))
	}
	if result.ServerDelta[0].Path != "note-050.md" {
		t.Fatalf("server delta path = %q, want note-050.md", result.ServerDelta[0].Path)
	}
}

func sha256Hex(content string) string {
	sum := sha256.Sum256([]byte(content))
	return hex.EncodeToString(sum[:])
}
