package daemon

import (
	"os"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestSchedulerStateManager_SaveTask(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()

	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	// Test saving a task
	task := NewScheduledTask(
		"test-task-1", "test-session", "test message", tmpDir,
		time.Now().Add(1*time.Hour), time.Now(), TaskStatusPending,
	)

	err := sm.SaveTask(task)
	if err != nil {
		t.Fatalf("SaveTask failed: %v", err)
	}

	// Flush to ensure disk write
	require.NoError(t, sm.Flush())

	// Verify the state file was created
	statePath := filepath.Join(tmpDir, SchedulerStateDir, SchedulerStateFile)
	if _, err := os.Stat(statePath); os.IsNotExist(err) {
		t.Error("State file should exist after save")
	}
}

func TestSchedulerStateManager_SaveTaskEmptyPath(t *testing.T) {
	t.Parallel()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	task := NewScheduledTask(
		"test-task", "", "test", "",
		time.Time{}, time.Time{}, TaskStatusPending,
	)

	err := sm.SaveTask(task)
	if err == nil {
		t.Error("Expected error for empty project path")
	}
}

func TestSchedulerStateManager_LoadTasks(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	// Load from non-existent path should return empty
	tasks, err := sm.LoadTasks(tmpDir)
	if err != nil {
		t.Fatalf("LoadTasks failed: %v", err)
	}
	if len(tasks) != 0 {
		t.Errorf("Expected 0 tasks, got %d", len(tasks))
	}

	// Save a task first
	task := NewScheduledTask(
		"load-test-1", "session-1", "test load", tmpDir,
		time.Now().Add(1*time.Hour), time.Now(), TaskStatusPending,
	)
	if err := sm.SaveTask(task); err != nil {
		t.Fatalf("SaveTask failed: %v", err)
	}

	// Now load should return the task (from in-memory cache)
	tasks, err = sm.LoadTasks(tmpDir)
	if err != nil {
		t.Fatalf("LoadTasks failed: %v", err)
	}
	if len(tasks) != 1 {
		t.Errorf("Expected 1 task, got %d", len(tasks))
	}
	if tasks[0].ID != "load-test-1" {
		t.Errorf("Expected task ID load-test-1, got %s", tasks[0].ID)
	}
}

func TestSchedulerStateManager_RemoveTask(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	// Save a task first
	task := NewScheduledTask(
		"remove-test-1", "session-1", "to be removed", tmpDir,
		time.Now().Add(1*time.Hour), time.Now(), TaskStatusPending,
	)
	if err := sm.SaveTask(task); err != nil {
		t.Fatalf("SaveTask failed: %v", err)
	}

	// Remove the task
	err := sm.RemoveTask("remove-test-1", tmpDir)
	if err != nil {
		t.Fatalf("RemoveTask failed: %v", err)
	}

	// Verify task was removed
	tasks, err := sm.LoadTasks(tmpDir)
	if err != nil {
		t.Fatalf("LoadTasks failed: %v", err)
	}
	if len(tasks) != 0 {
		t.Errorf("Expected 0 tasks after remove, got %d", len(tasks))
	}
}

func TestSchedulerStateManager_RemoveTaskEmptyPath(t *testing.T) {
	t.Parallel()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	err := sm.RemoveTask("task-id", "")
	if err == nil {
		t.Error("Expected error for empty project path")
	}
}

func TestSchedulerStateManager_RemoveTaskNonExistent(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	// Remove from non-existent state should succeed (no-op)
	err := sm.RemoveTask("nonexistent-task", tmpDir)
	if err != nil {
		t.Fatalf("RemoveTask from non-existent path should not error: %v", err)
	}
}

func TestSchedulerStateManager_ScanForProjects(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	// Create a state directory with a state file
	stateDir := filepath.Join(tmpDir, SchedulerStateDir)
	if err := os.MkdirAll(stateDir, 0755); err != nil {
		t.Fatalf("Failed to create state dir: %v", err)
	}

	// Create an empty state file
	statePath := filepath.Join(stateDir, SchedulerStateFile)
	if err := os.WriteFile(statePath, []byte(`{"version":1,"tasks":[]}`), 0644); err != nil {
		t.Fatalf("Failed to create state file: %v", err)
	}

	// Scan should discover this project
	sm.ScanForProjects([]string{tmpDir})

	// List projects should return the discovered project
	projects := sm.ListProjectsWithTasks()
	if len(projects) != 1 {
		t.Errorf("Expected 1 project, got %d", len(projects))
	}
	if len(projects) > 0 && projects[0] != tmpDir {
		t.Errorf("Expected project %s, got %s", tmpDir, projects[0])
	}
}

func TestSchedulerStateManager_RegisterProject(t *testing.T) {
	t.Parallel()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	// Register a project
	sm.RegisterProject("/test/project")

	// Should be in the list
	projects := sm.ListProjectsWithTasks()
	found := false
	for _, p := range projects {
		if p == "/test/project" {
			found = true
			break
		}
	}
	if !found {
		t.Error("Expected registered project to be in list")
	}
}

func TestSchedulerStateManager_ListProjectsWithTasks(t *testing.T) {
	t.Parallel()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	// Initially empty
	projects := sm.ListProjectsWithTasks()
	if len(projects) != 0 {
		t.Errorf("Expected 0 projects, got %d", len(projects))
	}

	// Register some projects
	sm.RegisterProject("/project1")
	sm.RegisterProject("/project2")

	projects = sm.ListProjectsWithTasks()
	if len(projects) != 2 {
		t.Errorf("Expected 2 projects, got %d", len(projects))
	}
}

func TestSchedulerStateManager_ClearProject(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	// Save a task first
	task := NewScheduledTask(
		"clear-test-1", "session-1", "to be cleared", tmpDir,
		time.Now().Add(1*time.Hour), time.Now(), TaskStatusPending,
	)
	if err := sm.SaveTask(task); err != nil {
		t.Fatalf("SaveTask failed: %v", err)
	}
	require.NoError(t, sm.Flush())

	// Clear the project
	err := sm.ClearProject(tmpDir)
	if err != nil {
		t.Fatalf("ClearProject failed: %v", err)
	}

	// State file should be gone
	statePath := filepath.Join(tmpDir, SchedulerStateDir, SchedulerStateFile)
	if _, err := os.Stat(statePath); !os.IsNotExist(err) {
		t.Error("State file should not exist after clear")
	}

	// Project should be removed from list
	projects := sm.ListProjectsWithTasks()
	for _, p := range projects {
		if p == tmpDir {
			t.Error("Project should not be in list after clear")
		}
	}
}

func TestSchedulerStateManager_LoadAllTasks(t *testing.T) {
	t.Parallel()
	tmpDir1 := t.TempDir()
	tmpDir2 := t.TempDir()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	// Save tasks in two projects
	task1 := NewScheduledTask(
		"all-test-1", "session-1", "task 1", tmpDir1,
		time.Now().Add(1*time.Hour), time.Now(), TaskStatusPending,
	)
	if err := sm.SaveTask(task1); err != nil {
		t.Fatalf("SaveTask 1 failed: %v", err)
	}

	task2 := NewScheduledTask(
		"all-test-2", "session-2", "task 2", tmpDir2,
		time.Now().Add(2*time.Hour), time.Now(), TaskStatusPending,
	)
	if err := sm.SaveTask(task2); err != nil {
		t.Fatalf("SaveTask 2 failed: %v", err)
	}

	// Load all tasks
	allTasks := sm.LoadAllTasks()
	if len(allTasks) != 2 {
		t.Errorf("Expected 2 tasks, got %d", len(allTasks))
	}
}

func TestSchedulerStateManager_UpdateExistingTask(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	// Save a task
	task := NewScheduledTask(
		"update-test-1", "session-1", "original message", tmpDir,
		time.Now().Add(1*time.Hour), time.Now(), TaskStatusPending,
	)
	if err := sm.SaveTask(task); err != nil {
		t.Fatalf("SaveTask failed: %v", err)
	}

	// Update the task
	task.Message = "updated message"
	task.SetStatus(TaskStatusDelivered)
	if err := sm.SaveTask(task); err != nil {
		t.Fatalf("SaveTask (update) failed: %v", err)
	}

	// Load and verify update (from in-memory cache)
	tasks, err := sm.LoadTasks(tmpDir)
	if err != nil {
		t.Fatalf("LoadTasks failed: %v", err)
	}
	if len(tasks) != 1 {
		t.Errorf("Expected 1 task (should update, not add), got %d", len(tasks))
	}
	if tasks[0].Message != "updated message" {
		t.Errorf("Expected updated message, got %s", tasks[0].Message)
	}
	if tasks[0].Status() != TaskStatusDelivered {
		t.Errorf("Expected status delivered, got %s", tasks[0].Status())
	}
}

func TestSchedulerStateManager_ClearNonExistentProject(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	// Clear a project that doesn't have a state file
	err := sm.ClearProject(tmpDir)
	if err != nil {
		t.Fatalf("ClearProject should succeed for non-existent state: %v", err)
	}
}

func TestSchedulerStateManager_RemoveLastTask(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	// Save one task
	task := NewScheduledTask(
		"last-task", "session-1", "only task", tmpDir,
		time.Now().Add(1*time.Hour), time.Now(), TaskStatusPending,
	)
	if err := sm.SaveTask(task); err != nil {
		t.Fatalf("SaveTask failed: %v", err)
	}

	// Flush to ensure file exists
	require.NoError(t, sm.Flush())

	// Verify state file exists
	statePath := filepath.Join(tmpDir, SchedulerStateDir, SchedulerStateFile)
	if _, err := os.Stat(statePath); os.IsNotExist(err) {
		t.Fatal("State file should exist after save")
	}

	// Remove the only task
	if err := sm.RemoveTask("last-task", tmpDir); err != nil {
		t.Fatalf("RemoveTask failed: %v", err)
	}

	// State file should be removed since no tasks remain
	if _, err := os.Stat(statePath); !os.IsNotExist(err) {
		t.Error("State file should be removed when last task is deleted")
	}
}

// New tests for write-behind channel behavior

func TestSchedulerState_ConcurrentSaveLoadNoDeadlock(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm.Close()

	var wg sync.WaitGroup
	const goroutines = 20
	const iterations = 50

	// Writers
	for i := 0; i < goroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < iterations; j++ {
				task := NewScheduledTask(
					"task-concurrent", "session", "msg", tmpDir,
					time.Now().Add(1*time.Hour), time.Now(), TaskStatusPending,
				)
				_ = sm.SaveTask(task)
			}
		}(i)
	}

	// Readers
	for i := 0; i < goroutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < iterations; j++ {
				_, _ = sm.LoadTasks(tmpDir)
			}
		}()
	}

	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-done:
		// No deadlock
	case <-time.After(10 * time.Second):
		t.Fatal("Deadlock detected: concurrent operations did not complete in 10s")
	}

	require.NoError(t, sm.Flush())
}

func TestSchedulerState_FlushOnClose(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Second) // Very long debounce

	task := NewScheduledTask(
		"close-flush-task", "session", "msg", tmpDir,
		time.Now().Add(1*time.Hour), time.Now(), TaskStatusPending,
	)
	require.NoError(t, sm.SaveTask(task))

	// Close should flush pending writes
	require.NoError(t, sm.Close())

	// Verify file was written
	statePath := filepath.Join(tmpDir, SchedulerStateDir, SchedulerStateFile)
	data, err := os.ReadFile(statePath)
	require.NoError(t, err)
	assert.Contains(t, string(data), "close-flush-task")
}

func TestSchedulerState_DebouncedSavesCoalesce(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sm := NewSchedulerStateManagerWithInterval(100 * time.Millisecond)
	defer sm.Close()

	// Rapid mutations — should coalesce
	for i := 0; i < 100; i++ {
		task := NewScheduledTask(
			"coalesce-task", "session", "msg", tmpDir,
			time.Now().Add(1*time.Hour), time.Now(), TaskStatusPending,
		)
		require.NoError(t, sm.SaveTask(task))
	}

	// Wait for debounce
	statePath := filepath.Join(tmpDir, SchedulerStateDir, SchedulerStateFile)
	require.Eventually(t, func() bool {
		data, err := os.ReadFile(statePath)
		return err == nil && len(data) > 0
	}, 2*time.Second, 10*time.Millisecond, "state file should be written after debounce")

	// Verify file exists with task
	data, err := os.ReadFile(statePath)
	require.NoError(t, err)
	assert.Contains(t, string(data), "coalesce-task")
}

func TestSchedulerState_PersistAcrossRestart(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()

	// First manager: save task and close
	sm1 := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	task := NewScheduledTask(
		"persist-task", "session", "persist msg", tmpDir,
		time.Now().Add(1*time.Hour), time.Now(), TaskStatusPending,
	)
	require.NoError(t, sm1.SaveTask(task))
	require.NoError(t, sm1.Close())

	// Second manager: should load from disk
	sm2 := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	defer sm2.Close()

	tasks, err := sm2.LoadTasks(tmpDir)
	require.NoError(t, err)
	require.Len(t, tasks, 1)
	assert.Equal(t, "persist-task", tasks[0].ID)
	assert.Equal(t, "persist msg", tasks[0].Message)
}

func TestSchedulerState_CloseIdempotent(t *testing.T) {
	t.Parallel()
	sm := NewSchedulerStateManagerWithInterval(10 * time.Millisecond)
	require.NoError(t, sm.Close())
	require.NoError(t, sm.Close())
}
