package batching

import (
	"testing"
	"time"

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

func TestLLMNewBatcher(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)
	assert.NotNil(t, batcher)
	assert.Equal(t, 0, batcher.QueueSize())
}

func TestLLMEnqueueRequest(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{
		ID:     "req-1",
		Type:   "implementer",
		Model:  "claude-opus",
		Prompt: "Generate code",
	}

	batcher.Enqueue(req)
	assert.Equal(t, 1, batcher.QueueSize())
}

func TestLLMEnqueueMultipleRequests(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	for i := 0; i < 5; i++ {
		req := &LLMRequest{
			Type:   "implementer",
			Model:  "claude-opus",
			Prompt: "Prompt",
		}
		batcher.Enqueue(req)
	}

	assert.Equal(t, 5, batcher.QueueSize())
}

func TestLLMAutoFlushOnMaxBatchSize(t *testing.T) {
	batcher := NewLLMBatcher(5, 10*time.Second)

	for i := 0; i < 5; i++ {
		req := &LLMRequest{
			Type:   "implementer",
			Model:  "claude-opus",
			Prompt: "Prompt",
		}
		batcher.Enqueue(req)
	}

	assert.Equal(t, 0, batcher.QueueSize())
	assert.Equal(t, 1, batcher.PendingBatchCount())
}

func TestLLMManualFlush(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{
		Type:   "implementer",
		Model:  "claude-opus",
		Prompt: "Prompt",
	}
	batcher.Enqueue(req)

	batcher.Flush()
	assert.Equal(t, 0, batcher.QueueSize())
	assert.Equal(t, 1, batcher.PendingBatchCount())
}

func TestLLMGetPendingBatch(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{
		Type:   "implementer",
		Model:  "claude-opus",
		Prompt: "Prompt",
	}
	batcher.Enqueue(req)
	batcher.Flush()

	batch := batcher.GetPendingBatch()
	assert.NotNil(t, batch)
	assert.Equal(t, 1, len(batch.Requests))
	assert.Equal(t, "implementer", batch.Type)
}

func TestLLMMarkBatchExecuting(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{Type: "implementer", Model: "claude-opus", Prompt: "test"}
	batcher.Enqueue(req)
	batcher.Flush()

	batch := batcher.GetPendingBatch()
	batcher.MarkBatchExecuting(batch.ID)

	updated := batcher.GetBatchByID(batch.ID)
	assert.Equal(t, "executing", updated.Status)
}

func TestLLMMarkBatchCompleted(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{
		ID:     "req-1",
		Type:   "implementer",
		Model:  "claude-opus",
		Prompt: "test",
	}
	batcher.Enqueue(req)
	batcher.Flush()

	batch := batcher.GetPendingBatch()

	results := map[string]*LLMResult{
		"req-1": {
			RequestID:  "req-1",
			Response:   "Generated code",
			TokenCount: 100,
		},
	}

	batcher.MarkBatchCompleted(batch.ID, results)

	executed := batcher.GetExecutedBatches()
	assert.Equal(t, 1, len(executed))
	assert.Equal(t, "completed", executed[0].Status)
}

func TestLLMMarkBatchFailed(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{
		ID:    "req-1",
		Type:  "implementer",
		Model: "claude-opus",
	}
	batcher.Enqueue(req)
	batcher.Flush()

	batch := batcher.GetPendingBatch()
	testErr := assert.AnError
	batcher.MarkBatchFailed(batch.ID, testErr)

	failed := batcher.GetBatchByID(batch.ID)
	assert.Equal(t, "failed", failed.Status)
	assert.Error(t, failed.Error)
}

func TestLLMGroupByTypeAndModel(t *testing.T) {
	batcher := NewLLMBatcher(100, 5*time.Second)

	// Add requests of different types
	for i := 0; i < 3; i++ {
		req := &LLMRequest{
			Type:   "implementer",
			Model:  "claude-opus",
			Prompt: "test",
		}
		batcher.Enqueue(req)
	}

	for i := 0; i < 2; i++ {
		req := &LLMRequest{
			Type:   "judge",
			Model:  "claude-opus",
			Prompt: "test",
		}
		batcher.Enqueue(req)
	}

	batcher.Flush()

	// Should create 2 batches (one for implementer, one for judge)
	assert.Equal(t, 2, batcher.PendingBatchCount())
}

func TestLLMGetStats(t *testing.T) {
	batcher := NewLLMBatcher(5, 5*time.Second)

	// Add 10 requests (will create 2 batches)
	for i := 0; i < 10; i++ {
		req := &LLMRequest{
			Type:   "implementer",
			Model:  "claude-opus",
			Prompt: "test",
		}
		batcher.Enqueue(req)
	}

	stats := batcher.GetStats()
	assert.Equal(t, 10, stats.TotalRequests)
	assert.Equal(t, 2, stats.TotalBatches)
	assert.Equal(t, 5.0, stats.AvgRequestsPerBatch)
	// 10 requests in 2 batches saves 8 API calls
	assert.Equal(t, 8, stats.APICallsSaved)
}

func TestLLMResultDelivery(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{
		ID:        "req-1",
		Type:      "implementer",
		Model:     "claude-opus",
		Prompt:    "test",
		ResultCh:  make(chan *LLMResult, 1),
	}

	batcher.Enqueue(req)
	batcher.Flush()

	batch := batcher.GetPendingBatch()

	results := map[string]*LLMResult{
		"req-1": {
			RequestID:  "req-1",
			Response:   "Response",
			TokenCount: 50,
		},
	}

	batcher.MarkBatchCompleted(batch.ID, results)

	// Check if result was delivered to channel
	select {
	case result := <-req.ResultCh:
		assert.NotNil(t, result)
		assert.Equal(t, "Response", result.Response)
	case <-time.After(1 * time.Second):
		t.Fatal("Result not delivered")
	}
}

func TestLLMMultipleBatches(t *testing.T) {
	batcher := NewLLMBatcher(3, 5*time.Second)

	// Create 3 batches (3 requests each)
	for batch := 0; batch < 3; batch++ {
		for i := 0; i < 3; i++ {
			req := &LLMRequest{
				Type:   "implementer",
				Model:  "claude-opus",
				Prompt: "test",
			}
			batcher.Enqueue(req)
		}
	}

	assert.Equal(t, 3, batcher.PendingBatchCount())
}

func TestLLMQueueSize(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{Type: "implementer", Model: "claude-opus"}
	batcher.Enqueue(req)

	assert.Equal(t, 1, batcher.QueueSize())
}

func TestLLMPendingBatchCount(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{Type: "implementer", Model: "claude-opus"}
	batcher.Enqueue(req)
	batcher.Flush()

	assert.Equal(t, 1, batcher.PendingBatchCount())
}

func TestLLMGetExecutedBatches(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	for i := 0; i < 2; i++ {
		req := &LLMRequest{Type: "implementer", Model: "claude-opus"}
		batcher.Enqueue(req)
		batcher.Flush()

		batch := batcher.GetPendingBatch()
		batcher.MarkBatchCompleted(batch.ID, make(map[string]*LLMResult))
	}

	executed := batcher.GetExecutedBatches()
	assert.Equal(t, 2, len(executed))
}

func TestLLMTimeSinceLastFlush(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{Type: "implementer", Model: "claude-opus"}
	batcher.Enqueue(req)
	batcher.Flush()

	time.Sleep(100 * time.Millisecond)
	elapsed := batcher.TimeSinceLastFlush()

	assert.Greater(t, elapsed, 50*time.Millisecond)
	assert.Less(t, elapsed, 200*time.Millisecond)
}

func TestLLMShouldFlush(t *testing.T) {
	batcher := NewLLMBatcher(100, 100*time.Millisecond)

	req := &LLMRequest{Type: "implementer", Model: "claude-opus"}
	batcher.Enqueue(req)

	// Should not flush yet
	assert.False(t, batcher.ShouldFlush())

	// Wait for age to exceed
	time.Sleep(150 * time.Millisecond)
	assert.True(t, batcher.ShouldFlush())
}

func TestLLMClear(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{Type: "implementer", Model: "claude-opus"}
	batcher.Enqueue(req)
	batcher.Flush()

	batcher.Clear()
	assert.Equal(t, 0, batcher.QueueSize())
	assert.Equal(t, 0, batcher.PendingBatchCount())
}

func TestLLMGetBatchByID(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{Type: "implementer", Model: "claude-opus"}
	batcher.Enqueue(req)
	batcher.Flush()

	batch := batcher.GetPendingBatch()
	retrieved := batcher.GetBatchByID(batch.ID)

	assert.NotNil(t, retrieved)
	assert.Equal(t, batch.ID, retrieved.ID)
}

func TestLLMGetBatchInfo(t *testing.T) {
	batch := &LLMBatch{
		ID:        "batch-1",
		Type:      "implementer",
		Model:     "claude-opus",
		Status:    "completed",
		CreatedAt: time.Now(),
	}

	info := batch.GetInfo()
	assert.Equal(t, "batch-1", info["id"])
	assert.Equal(t, "implementer", info["type"])
	assert.Equal(t, "claude-opus", info["model"])
	assert.Equal(t, "completed", info["status"])
}

func TestLLMEnqueueNil(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	batcher.Enqueue(nil)
	assert.Equal(t, 0, batcher.QueueSize())
}

func TestLLMAutoIDGeneration(t *testing.T) {
	req := &LLMRequest{
		Type:   "implementer",
		Model:  "claude-opus",
		Prompt: "test",
	}

	batcher := NewLLMBatcher(10, 5*time.Second)
	batcher.Enqueue(req)

	assert.NotEmpty(t, req.ID)
}

func TestLLMTokenCounting(t *testing.T) {
	batcher := NewLLMBatcher(10, 5*time.Second)

	req := &LLMRequest{
		ID:     "req-1",
		Type:   "implementer",
		Model:  "claude-opus",
		Prompt: "test",
	}
	batcher.Enqueue(req)
	batcher.Flush()

	batch := batcher.GetPendingBatch()

	results := map[string]*LLMResult{
		"req-1": {
			RequestID:  "req-1",
			Response:   "Response",
			TokenCount: 500,
		},
	}

	batcher.MarkBatchCompleted(batch.ID, results)

	stats := batcher.GetStats()
	assert.Equal(t, 500, stats.TotalTokens)
}

func BenchmarkLLMEnqueue(b *testing.B) {
	batcher := NewLLMBatcher(1000, 10*time.Second)

	for i := 0; i < b.N; i++ {
		req := &LLMRequest{
			Type:   "implementer",
			Model:  "claude-opus",
			Prompt: "test",
		}
		batcher.Enqueue(req)
	}
}

func BenchmarkLLMFlush(b *testing.B) {
	batcher := NewLLMBatcher(1000, 10*time.Second)

	for i := 0; i < b.N; i++ {
		req := &LLMRequest{Type: "implementer", Model: "claude-opus"}
		batcher.Enqueue(req)

		if (i + 1) % 100 == 0 {
			batcher.Flush()
		}
	}
}
