package postgres

import (
	"context"
	"database/sql"
	"testing"
	"time"

	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/testutil"
)

func cleanupTestQueue(t *testing.T, db *sql.DB) {
	t.Helper()
	_, err := db.Exec("DELETE FROM command_queue WHERE project_id LIKE 'test-%'")
	if err != nil {
		t.Logf("cleanup test queue: %v", err)
	}
}

func TestCommandQueueRepository_Enqueue(t *testing.T) {
	db := testutil.TestDB(t)
	t.Cleanup(func() { cleanupTestQueue(t, db) })

	repo := NewCommandQueueRepository(db)
	ctx := context.Background()

	t.Run("enqueues command successfully", func(t *testing.T) {
		cmd := &domain.QueuedCommand{
			ProjectID:   "test-proj-enqueue-1",
			Command:     "explain this code",
			CommandType: domain.CommandTypeClaude,
			Status:      domain.QueueStatusPending,
			Priority:    0,
			APIKeyID:    "key-1",
		}

		err := repo.Enqueue(ctx, cmd)
		if err != nil {
			t.Fatalf("Enqueue() error = %v", err)
		}

		if cmd.ID == "" {
			t.Error("ID should be set after enqueue")
		}
		if cmd.CreatedAt.IsZero() {
			t.Error("CreatedAt should be set after enqueue")
		}
	})

	t.Run("enqueues with working directory", func(t *testing.T) {
		cmd := &domain.QueuedCommand{
			ProjectID:   "test-proj-enqueue-2",
			Command:     "ls -la",
			CommandType: domain.CommandTypeShell,
			WorkingDir:  "/tmp",
			Status:      domain.QueueStatusPending,
			Priority:    1,
		}

		err := repo.Enqueue(ctx, cmd)
		if err != nil {
			t.Fatalf("Enqueue() error = %v", err)
		}

		// Retrieve and verify
		retrieved, err := repo.GetByID(ctx, cmd.ID)
		if err != nil {
			t.Fatalf("GetByID() error = %v", err)
		}

		if retrieved.WorkingDir != "/tmp" {
			t.Errorf("WorkingDir = %q, want %q", retrieved.WorkingDir, "/tmp")
		}
	})
}

func TestCommandQueueRepository_Dequeue(t *testing.T) {
	db := testutil.TestDB(t)
	t.Cleanup(func() { cleanupTestQueue(t, db) })

	repo := NewCommandQueueRepository(db)
	ctx := context.Background()

	t.Run("dequeues pending command", func(t *testing.T) {
		// Create a pending command
		cmd := &domain.QueuedCommand{
			ProjectID:   "test-proj-dequeue-1",
			Command:     "test command",
			CommandType: domain.CommandTypeClaude,
			Status:      domain.QueueStatusPending,
			Priority:    0,
		}
		_ = repo.Enqueue(ctx, cmd)

		// Dequeue it
		dequeued, err := repo.Dequeue(ctx, "test-proj-dequeue-1")
		if err != nil {
			t.Fatalf("Dequeue() error = %v", err)
		}

		if dequeued == nil {
			t.Fatal("Dequeue() returned nil")
		}

		if dequeued.Status != domain.QueueStatusRunning {
			t.Errorf("Status = %q, want %q", dequeued.Status, domain.QueueStatusRunning)
		}
		if dequeued.StartedAt == nil {
			t.Error("StartedAt should be set after dequeue")
		}
	})

	t.Run("returns nil when no pending commands", func(t *testing.T) {
		dequeued, err := repo.Dequeue(ctx, "test-proj-dequeue-empty")
		if err != nil {
			t.Fatalf("Dequeue() error = %v", err)
		}

		if dequeued != nil {
			t.Error("Dequeue() should return nil when no pending commands")
		}
	})

	t.Run("dequeues highest priority first", func(t *testing.T) {
		projectID := "test-proj-dequeue-priority"

		// Create commands with different priorities
		low := &domain.QueuedCommand{
			ProjectID:   projectID,
			Command:     "low priority",
			CommandType: domain.CommandTypeClaude,
			Status:      domain.QueueStatusPending,
			Priority:    0,
		}
		_ = repo.Enqueue(ctx, low)

		// Small delay to ensure different timestamps
		time.Sleep(10 * time.Millisecond)

		high := &domain.QueuedCommand{
			ProjectID:   projectID,
			Command:     "high priority",
			CommandType: domain.CommandTypeClaude,
			Status:      domain.QueueStatusPending,
			Priority:    10,
		}
		_ = repo.Enqueue(ctx, high)

		// Dequeue should get high priority first
		dequeued, err := repo.Dequeue(ctx, projectID)
		if err != nil {
			t.Fatalf("Dequeue() error = %v", err)
		}

		if dequeued.Command != "high priority" {
			t.Errorf("Command = %q, want %q", dequeued.Command, "high priority")
		}
	})
}

func TestCommandQueueRepository_UpdateStatus(t *testing.T) {
	db := testutil.TestDB(t)
	t.Cleanup(func() { cleanupTestQueue(t, db) })

	repo := NewCommandQueueRepository(db)
	ctx := context.Background()

	t.Run("updates status without result", func(t *testing.T) {
		cmd := &domain.QueuedCommand{
			ProjectID:   "test-proj-status-1",
			Command:     "test",
			CommandType: domain.CommandTypeClaude,
			Status:      domain.QueueStatusPending,
		}
		repo.Enqueue(ctx, cmd)

		err := repo.UpdateStatus(ctx, cmd.ID, domain.QueueStatusRunning, nil)
		if err != nil {
			t.Fatalf("UpdateStatus() error = %v", err)
		}

		retrieved, _ := repo.GetByID(ctx, cmd.ID)
		if retrieved.Status != domain.QueueStatusRunning {
			t.Errorf("Status = %q, want %q", retrieved.Status, domain.QueueStatusRunning)
		}
	})

	t.Run("updates status with result", func(t *testing.T) {
		cmd := &domain.QueuedCommand{
			ProjectID:   "test-proj-status-2",
			Command:     "test",
			CommandType: domain.CommandTypeShell,
			Status:      domain.QueueStatusRunning,
		}
		repo.Enqueue(ctx, cmd)

		result := &domain.QueuedCommandResult{
			ExitCode: 0,
			Output:   "success output",
			Error:    "",
		}

		err := repo.UpdateStatus(ctx, cmd.ID, domain.QueueStatusCompleted, result)
		if err != nil {
			t.Fatalf("UpdateStatus() error = %v", err)
		}

		retrieved, _ := repo.GetByID(ctx, cmd.ID)
		if retrieved.Status != domain.QueueStatusCompleted {
			t.Errorf("Status = %q, want %q", retrieved.Status, domain.QueueStatusCompleted)
		}
		if retrieved.ExitCode == nil || *retrieved.ExitCode != 0 {
			t.Errorf("ExitCode = %v, want 0", retrieved.ExitCode)
		}
		if retrieved.Output != "success output" {
			t.Errorf("Output = %q, want %q", retrieved.Output, "success output")
		}
	})
}

func TestCommandQueueRepository_GetByID(t *testing.T) {
	db := testutil.TestDB(t)
	t.Cleanup(func() { cleanupTestQueue(t, db) })

	repo := NewCommandQueueRepository(db)
	ctx := context.Background()

	t.Run("gets existing command", func(t *testing.T) {
		cmd := &domain.QueuedCommand{
			ProjectID:   "test-proj-getbyid",
			Command:     "get by id test",
			CommandType: domain.CommandTypeClaude,
			WorkingDir:  "/test/dir",
			Status:      domain.QueueStatusPending,
			Priority:    5,
			APIKeyID:    "key-getbyid",
		}
		repo.Enqueue(ctx, cmd)

		retrieved, err := repo.GetByID(ctx, cmd.ID)
		if err != nil {
			t.Fatalf("GetByID() error = %v", err)
		}

		if retrieved.Command != "get by id test" {
			t.Errorf("Command = %q, want %q", retrieved.Command, "get by id test")
		}
		if retrieved.Priority != 5 {
			t.Errorf("Priority = %d, want 5", retrieved.Priority)
		}
		if retrieved.APIKeyID != "key-getbyid" {
			t.Errorf("APIKeyID = %q, want %q", retrieved.APIKeyID, "key-getbyid")
		}
	})

	t.Run("returns error for non-existent command", func(t *testing.T) {
		_, err := repo.GetByID(ctx, "00000000-0000-0000-0000-000000000000")
		if err != domain.ErrCommandNotFound {
			t.Errorf("GetByID() error = %v, want %v", err, domain.ErrCommandNotFound)
		}
	})
}

func TestCommandQueueRepository_List(t *testing.T) {
	db := testutil.TestDB(t)
	t.Cleanup(func() { cleanupTestQueue(t, db) })

	repo := NewCommandQueueRepository(db)
	ctx := context.Background()

	projectID := "test-proj-list"

	// Create test commands
	for i := 0; i < 5; i++ {
		status := domain.QueueStatusPending
		if i%2 == 0 {
			status = domain.QueueStatusCompleted
		}
		cmd := &domain.QueuedCommand{
			ProjectID:   projectID,
			Command:     "list test " + string(rune('a'+i)),
			CommandType: domain.CommandTypeClaude,
			Status:      status,
		}
		repo.Enqueue(ctx, cmd)
		time.Sleep(10 * time.Millisecond) // Ensure different timestamps
	}

	t.Run("lists all commands for project", func(t *testing.T) {
		commands, err := repo.List(ctx, projectID, nil)
		if err != nil {
			t.Fatalf("List() error = %v", err)
		}

		if len(commands) < 5 {
			t.Errorf("List() returned %d commands, want at least 5", len(commands))
		}
	})

	t.Run("filters by status", func(t *testing.T) {
		status := domain.QueueStatusPending
		commands, err := repo.List(ctx, projectID, &domain.QueueFilters{
			Status: &status,
			Limit:  100,
		})
		if err != nil {
			t.Fatalf("List() error = %v", err)
		}

		for _, cmd := range commands {
			if cmd.Status != domain.QueueStatusPending {
				t.Errorf("Command has Status = %q, want %q", cmd.Status, domain.QueueStatusPending)
			}
		}
	})

	t.Run("respects limit and offset", func(t *testing.T) {
		commands, err := repo.List(ctx, projectID, &domain.QueueFilters{
			Limit:  2,
			Offset: 0,
		})
		if err != nil {
			t.Fatalf("List() error = %v", err)
		}

		if len(commands) != 2 {
			t.Errorf("List() returned %d commands, want 2", len(commands))
		}
	})

	t.Run("respects sort order", func(t *testing.T) {
		commands, err := repo.List(ctx, projectID, &domain.QueueFilters{
			SortOrder: "asc",
			Limit:     100,
		})
		if err != nil {
			t.Fatalf("List() error = %v", err)
		}

		if len(commands) >= 2 {
			if commands[0].CreatedAt.After(commands[1].CreatedAt) {
				t.Error("List() with asc sort order should return oldest first")
			}
		}
	})
}

func TestCommandQueueRepository_Cancel(t *testing.T) {
	db := testutil.TestDB(t)
	t.Cleanup(func() { cleanupTestQueue(t, db) })

	repo := NewCommandQueueRepository(db)
	ctx := context.Background()

	t.Run("cancels pending command", func(t *testing.T) {
		cmd := &domain.QueuedCommand{
			ProjectID:   "test-proj-cancel",
			Command:     "cancel test",
			CommandType: domain.CommandTypeClaude,
			Status:      domain.QueueStatusPending,
		}
		repo.Enqueue(ctx, cmd)

		err := repo.Cancel(ctx, cmd.ID)
		if err != nil {
			t.Fatalf("Cancel() error = %v", err)
		}

		retrieved, _ := repo.GetByID(ctx, cmd.ID)
		if retrieved.Status != domain.QueueStatusCancelled {
			t.Errorf("Status = %q, want %q", retrieved.Status, domain.QueueStatusCancelled)
		}
		if retrieved.CompletedAt == nil {
			t.Error("CompletedAt should be set after cancel")
		}
	})

	t.Run("returns error for non-pending command", func(t *testing.T) {
		cmd := &domain.QueuedCommand{
			ProjectID:   "test-proj-cancel-running",
			Command:     "cancel running test",
			CommandType: domain.CommandTypeClaude,
			Status:      domain.QueueStatusPending,
		}
		_ = repo.Enqueue(ctx, cmd)

		// Make it running
		_ = repo.UpdateStatus(ctx, cmd.ID, domain.QueueStatusRunning, nil)

		err := repo.Cancel(ctx, cmd.ID)
		if err == nil {
			t.Error("Cancel() should return error for running command")
		}
	})

	t.Run("returns error for non-existent command", func(t *testing.T) {
		err := repo.Cancel(ctx, "00000000-0000-0000-0000-000000000000")
		if err != domain.ErrCommandNotFound {
			t.Errorf("Cancel() error = %v, want %v", err, domain.ErrCommandNotFound)
		}
	})
}

func TestCommandQueueRepository_GetStats(t *testing.T) {
	db := testutil.TestDB(t)
	t.Cleanup(func() { cleanupTestQueue(t, db) })

	repo := NewCommandQueueRepository(db)
	ctx := context.Background()

	projectID := "test-proj-stats"

	// Create commands with different statuses
	statuses := []domain.QueueStatus{
		domain.QueueStatusPending,
		domain.QueueStatusPending,
		domain.QueueStatusRunning,
		domain.QueueStatusCompleted,
		domain.QueueStatusFailed,
	}

	for i, status := range statuses {
		cmd := &domain.QueuedCommand{
			ProjectID:   projectID,
			Command:     "stats test " + string(rune('a'+i)),
			CommandType: domain.CommandTypeClaude,
			Status:      status,
		}
		repo.Enqueue(ctx, cmd)
	}

	t.Run("returns correct stats", func(t *testing.T) {
		stats, err := repo.GetStats(ctx, projectID)
		if err != nil {
			t.Fatalf("GetStats() error = %v", err)
		}

		if stats.TotalPending < 2 {
			t.Errorf("TotalPending = %d, want at least 2", stats.TotalPending)
		}
		// Note: Running status is set during enqueue but some may be dequeued
		if stats.TotalCompleted < 1 {
			t.Errorf("TotalCompleted = %d, want at least 1", stats.TotalCompleted)
		}
		if stats.TotalFailed < 1 {
			t.Errorf("TotalFailed = %d, want at least 1", stats.TotalFailed)
		}
	})
}

func TestCommandQueueRepository_CleanupOld(t *testing.T) {
	db := testutil.TestDB(t)
	t.Cleanup(func() { cleanupTestQueue(t, db) })

	repo := NewCommandQueueRepository(db)
	ctx := context.Background()

	// Create completed commands (CleanupOld only removes terminal states)
	for i := 0; i < 3; i++ {
		cmd := &domain.QueuedCommand{
			ProjectID:   "test-proj-cleanup",
			Command:     "cleanup test " + string(rune('a'+i)),
			CommandType: domain.CommandTypeClaude,
			Status:      domain.QueueStatusPending,
		}
		_ = repo.Enqueue(ctx, cmd)

		// Complete them
		result := &domain.QueuedCommandResult{ExitCode: 0}
		_ = repo.UpdateStatus(ctx, cmd.ID, domain.QueueStatusCompleted, result)
	}

	t.Run("cleanup runs without error", func(t *testing.T) {
		// This won't delete newly created entries (they're not old enough)
		// but we verify the function executes without error
		deleted, err := repo.CleanupOld(ctx, 30)
		if err != nil {
			t.Fatalf("CleanupOld() error = %v", err)
		}

		// Newly created commands shouldn't be deleted
		if deleted != 0 {
			t.Logf("CleanupOld() deleted %d commands (expected 0 for new commands)", deleted)
		}
	})
}
