package worker

import (
	"context"
	"fmt"
	"testing"
	"time"

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

func TestWorkExecutor_StartAndStop(t *testing.T) {
	deps := newTestDeps()

	executor := NewWorkExecutor(deps.workerSvc, deps.workSvc, deps.buildExec, nil, nil, &WorkExecutorConfig{
		WorkerID:        "test-worker-1",
		PollPeriod:      100 * time.Millisecond,
		HeartbeatPeriod: 100 * time.Millisecond,
	})

	if err := executor.Start(); err != nil {
		t.Fatalf("Start() error = %v", err)
	}

	// Verify worker was registered
	deps.registry.mu.Lock()
	w, exists := deps.registry.workers["test-worker-1"]
	deps.registry.mu.Unlock()
	if !exists {
		t.Fatal("expected worker to be registered")
	}
	if w.Status != domain.WorkerStatusIdle {
		t.Errorf("got status %q, want %q", w.Status, domain.WorkerStatusIdle)
	}

	// Verify double-start returns error
	if err := executor.Start(); err == nil {
		t.Error("expected error on double-start")
	}

	executor.Stop()

	// Verify worker was deregistered
	deps.registry.mu.Lock()
	_, exists = deps.registry.workers["test-worker-1"]
	deps.registry.mu.Unlock()
	if exists {
		t.Error("expected worker to be deregistered after stop")
	}
}

func TestWorkExecutor_ClaimsAndExecutesTask(t *testing.T) {
	deps := newTestDeps()

	// Enqueue a build task
	deps.queue.mu.Lock()
	deps.queue.tasks["task-1"] = &domain.WorkTask{
		ID:         "task-1",
		ProjectID:  "project-1",
		Type:       domain.WorkTaskTypeBuild,
		Status:     domain.WorkTaskStatusPending,
		Spec:       map[string]any{"prompt": "Build a landing page"},
		MaxRetries: 3,
		CreatedAt:  time.Now(),
	}
	deps.queue.mu.Unlock()

	executor := NewWorkExecutor(deps.workerSvc, deps.workSvc, deps.buildExec, nil, nil, &WorkExecutorConfig{
		WorkerID:        "test-worker-2",
		PollPeriod:      50 * time.Millisecond,
		HeartbeatPeriod: 5 * time.Second,
	})

	// Register the worker (normally done by Start) then call tryClaimAndExecute directly
	if err := executor.Start(); err != nil {
		t.Fatalf("Start() error = %v", err)
	}

	// Call tryClaimAndExecute directly to avoid timing dependency
	executor.tryClaimAndExecute()
	executor.Stop()

	// Verify task was completed
	deps.queue.mu.Lock()
	task := deps.queue.tasks["task-1"]
	deps.queue.mu.Unlock()

	if task.Status != domain.WorkTaskStatusCompleted {
		t.Errorf("got task status %q, want %q", task.Status, domain.WorkTaskStatusCompleted)
	}
}

func TestWorkExecutor_FailsTaskOnAgentError(t *testing.T) {
	deps := newTestDeps()
	deps.agent.err = fmt.Errorf("agent crashed")

	// Enqueue a build task
	deps.queue.mu.Lock()
	deps.queue.tasks["task-1"] = &domain.WorkTask{
		ID:         "task-1",
		ProjectID:  "project-1",
		Type:       domain.WorkTaskTypeBuild,
		Status:     domain.WorkTaskStatusPending,
		Spec:       map[string]any{"prompt": "Build something"},
		MaxRetries: 3,
		CreatedAt:  time.Now(),
	}
	deps.queue.mu.Unlock()

	executor := NewWorkExecutor(deps.workerSvc, deps.workSvc, deps.buildExec, nil, nil, &WorkExecutorConfig{
		WorkerID:        "test-worker-3",
		PollPeriod:      50 * time.Millisecond,
		HeartbeatPeriod: 5 * time.Second,
	})

	if err := executor.Start(); err != nil {
		t.Fatalf("Start() error = %v", err)
	}

	// Call tryClaimAndExecute directly for each retry to avoid timing dependency
	for i := 0; i < 3; i++ {
		executor.tryClaimAndExecute()
	}
	executor.Stop()

	// Task should be permanently failed after all retries.
	deps.queue.mu.Lock()
	task := deps.queue.tasks["task-1"]
	deps.queue.mu.Unlock()

	if task.Status != domain.WorkTaskStatusFailed {
		t.Errorf("got task status %q, want %q (should be permanently failed after retries)", task.Status, domain.WorkTaskStatusFailed)
	}
	if task.RetryCount < 3 {
		t.Errorf("expected retry_count >= 3, got %d", task.RetryCount)
	}
}

func TestWorkExecutor_UnsupportedTaskType(t *testing.T) {
	deps := newTestDeps()

	// Enqueue a custom task (not build)
	deps.queue.mu.Lock()
	deps.queue.tasks["task-1"] = &domain.WorkTask{
		ID:         "task-1",
		ProjectID:  "project-1",
		Type:       domain.WorkTaskTypeCustom,
		Status:     domain.WorkTaskStatusPending,
		Spec:       map[string]any{"prompt": "Do something custom"},
		MaxRetries: 1,
		CreatedAt:  time.Now(),
	}
	deps.queue.mu.Unlock()

	executor := NewWorkExecutor(deps.workerSvc, deps.workSvc, deps.buildExec, nil, nil, &WorkExecutorConfig{
		WorkerID:        "test-worker-4",
		PollPeriod:      50 * time.Millisecond,
		HeartbeatPeriod: 5 * time.Second,
	})

	if err := executor.Start(); err != nil {
		t.Fatalf("Start() error = %v", err)
	}

	// Call tryClaimAndExecute directly to avoid timing dependency
	executor.tryClaimAndExecute()
	executor.Stop()

	// Should fail because custom tasks are unsupported
	deps.queue.mu.Lock()
	task := deps.queue.tasks["task-1"]
	deps.queue.mu.Unlock()

	// With maxRetries=1 and retryCount=1, it should be permanently failed
	if task.Status != domain.WorkTaskStatusFailed {
		t.Errorf("got task status %q, want %q", task.Status, domain.WorkTaskStatusFailed)
	}
}

// =============================================================================
// BuildExecutor Tests
// =============================================================================

func TestBuildExecutor_Execute(t *testing.T) {
	t.Run("successful build", func(t *testing.T) {
		agent := &mockCodeAgent{
			result: &domain.AgentResult{ExitCode: 0, DurationMs: 500},
		}
		registry := &mockCodeAgentRegistry{agent: agent}
		exec := NewBuildExecutor(registry, nil, nil, nil)

		task := &domain.WorkTask{
			ID:        "task-1",
			ProjectID: "project-1",
			Type:      domain.WorkTaskTypeBuild,
			Spec:      map[string]any{"prompt": "Build a landing page"},
		}

		result := exec.Execute(context.Background(), task)
		if !result.Success {
			t.Errorf("expected success, got error: %s", result.Error)
		}
		if result.DurationMs < 0 {
			t.Errorf("expected non-negative duration, got %d", result.DurationMs)
		}
	})

	t.Run("missing prompt", func(t *testing.T) {
		registry := &mockCodeAgentRegistry{agent: &mockCodeAgent{}}
		exec := NewBuildExecutor(registry, nil, nil, nil)

		task := &domain.WorkTask{
			ID:   "task-1",
			Type: domain.WorkTaskTypeBuild,
			Spec: map[string]any{},
		}

		result := exec.Execute(context.Background(), task)
		if result.Success {
			t.Error("expected failure for missing prompt")
		}
	})

	t.Run("no agent available", func(t *testing.T) {
		registry := &mockCodeAgentRegistry{agent: nil}
		exec := NewBuildExecutor(registry, nil, nil, nil)

		task := &domain.WorkTask{
			ID:   "task-1",
			Type: domain.WorkTaskTypeBuild,
			Spec: map[string]any{"prompt": "Build something"},
		}

		result := exec.Execute(context.Background(), task)
		if result.Success {
			t.Error("expected failure when no agent available")
		}
	})

	t.Run("agent execution error", func(t *testing.T) {
		agent := &mockCodeAgent{err: fmt.Errorf("connection refused")}
		registry := &mockCodeAgentRegistry{agent: agent}
		exec := NewBuildExecutor(registry, nil, nil, nil)

		task := &domain.WorkTask{
			ID:   "task-1",
			Type: domain.WorkTaskTypeBuild,
			Spec: map[string]any{"prompt": "Build something"},
		}

		result := exec.Execute(context.Background(), task)
		if result.Success {
			t.Error("expected failure on agent error")
		}
		if result.Error == "" {
			t.Error("expected error message")
		}
	})

	t.Run("agent non-zero exit code", func(t *testing.T) {
		agent := &mockCodeAgent{
			result: &domain.AgentResult{ExitCode: 1, DurationMs: 500},
		}
		registry := &mockCodeAgentRegistry{agent: agent}
		exec := NewBuildExecutor(registry, nil, nil, nil)

		task := &domain.WorkTask{
			ID:   "task-1",
			Type: domain.WorkTaskTypeBuild,
			Spec: map[string]any{"prompt": "Build something"},
		}

		result := exec.Execute(context.Background(), task)
		if result.Success {
			t.Error("expected failure on non-zero exit code")
		}
	})
}

func TestBuildExecutor_ParseSpec(t *testing.T) {
	exec := NewBuildExecutor(nil, nil, nil, nil)

	t.Run("valid spec", func(t *testing.T) {
		spec, err := exec.parseSpec(map[string]any{
			"prompt":      "Build a page",
			"template":    "astro-landing",
			"auto_commit": true,
			"auto_push":   true,
		})
		if err != nil {
			t.Fatalf("parseSpec() error = %v", err)
		}
		if spec.Prompt != "Build a page" {
			t.Errorf("got prompt %q", spec.Prompt)
		}
		if !spec.AutoCommit {
			t.Error("expected auto_commit = true")
		}
		if !spec.AutoPush {
			t.Error("expected auto_push = true")
		}
	})

	t.Run("missing prompt", func(t *testing.T) {
		_, err := exec.parseSpec(map[string]any{
			"template": "astro-landing",
		})
		if err == nil {
			t.Error("expected error for missing prompt")
		}
	})
}

func TestTruncate(t *testing.T) {
	tests := []struct {
		input  string
		maxLen int
		want   string
	}{
		{"short", 10, "short"},
		{"exactly ten", 11, "exactly ten"},
		{"this is a long string", 10, "this is..."},
		{"abc", 3, "abc"},
		{"abcd", 3, "abc"},
	}

	for _, tt := range tests {
		got := truncate(tt.input, tt.maxLen)
		if got != tt.want {
			t.Errorf("truncate(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want)
		}
	}
}
