package batching

import (
	"testing"
	"time"

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

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

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

	op := &GitOp{
		OpType:  "commit",
		Branch:  "main",
		Message: "Add feature",
		Files:   []string{"file1.go"},
	}

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

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

	for i := 0; i < 5; i++ {
		op := &GitOp{
			OpType:  "commit",
			Branch:  "main",
			Message: "Commit",
		}
		batcher.Enqueue(op)
	}

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

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

	for i := 0; i < 5; i++ {
		op := &GitOp{
			OpType:  "commit",
			Branch:  "main",
			Message: "Commit",
		}
		batcher.Enqueue(op)
	}

	// After 5 ops, should auto-flush
	assert.Equal(t, 0, batcher.QueueSize())
	assert.Equal(t, 1, batcher.PendingBatchCount())
}

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

	op := &GitOp{
		OpType:  "commit",
		Branch:  "main",
		Message: "Commit",
	}
	batcher.Enqueue(op)
	assert.Equal(t, 1, batcher.QueueSize())

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

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

	op := &GitOp{
		OpType:  "commit",
		Branch:  "main",
		Message: "Commit",
	}
	batcher.Enqueue(op)
	batcher.Flush()

	batch := batcher.GetPendingBatch()
	assert.NotNil(t, batch)
	assert.Equal(t, 1, len(batch.Operations))
}

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

	op := &GitOp{OpType: "commit"}
	batcher.Enqueue(op)
	batcher.Flush()

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

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

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

	op := &GitOp{OpType: "commit"}
	batcher.Enqueue(op)
	batcher.Flush()

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

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

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

	op := &GitOp{OpType: "commit"}
	batcher.Enqueue(op)
	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 TestGetStats(t *testing.T) {
	batcher := NewGitBatcher(5, 5*time.Second)

	// Add 10 ops (will create 2 batches of 5 each)
	for i := 0; i < 10; i++ {
		op := &GitOp{OpType: "commit"}
		batcher.Enqueue(op)
	}

	stats := batcher.GetStats()
	assert.Equal(t, 10, stats.TotalOps)
	assert.Equal(t, 2, stats.TotalBatches)
	assert.Equal(t, 5.0, stats.AvgOpsPerBatch)
	// 10 ops in 2 batches saves 8 round trips (5-1 + 5-1)
	assert.Equal(t, 8, stats.NetworkSavings)
}

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

	op := &GitOp{OpType: "commit"}
	batcher.Enqueue(op)

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

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

	op := &GitOp{OpType: "commit"}
	batcher.Enqueue(op)
	batcher.Flush()

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

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

	// Create and execute batches
	for i := 0; i < 2; i++ {
		op := &GitOp{OpType: "commit"}
		batcher.Enqueue(op)
		batcher.Flush()

		batch := batcher.GetPendingBatch()
		batcher.MarkBatchCompleted(batch.ID)
	}

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

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

	op := &GitOp{OpType: "commit"}
	batcher.Enqueue(op)
	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 TestShouldFlush(t *testing.T) {
	batcher := NewGitBatcher(100, 100*time.Millisecond)

	// Empty queue should not flush
	assert.False(t, batcher.ShouldFlush())

	// Enqueue but not old enough
	op := &GitOp{OpType: "commit"}
	batcher.Enqueue(op)
	assert.False(t, batcher.ShouldFlush())

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

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

	op := &GitOp{OpType: "commit"}
	batcher.Enqueue(op)
	batcher.Flush()

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

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

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

	ops := []*GitOp{
		{OpType: "commit", Message: "Commit 1"},
		{OpType: "commit", Message: "Commit 2"},
		{OpType: "commit", Message: "Commit 3"},
	}

	for _, op := range ops {
		batcher.Enqueue(op)
	}

	queued := batcher.GetQueuedOps()
	assert.Equal(t, 3, len(queued))
	assert.Equal(t, "Commit 1", queued[0].Message)
	assert.Equal(t, "Commit 3", queued[2].Message)
}

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

	// Add 6 ops (will create 2 batches of 3 each)
	for i := 0; i < 6; i++ {
		op := &GitOp{OpType: "commit"}
		batcher.Enqueue(op)
	}

	// Mark both batches as completed
	for i := 0; i < 2; i++ {
		batch := batcher.GetPendingBatch()
		if batch != nil {
			batcher.MarkBatchCompleted(batch.ID)
		}
	}

	savings := batcher.CalculateNetworkSavings()
	// 2 batches of 3 each saves 4 round trips (3-1 + 3-1)
	assert.Equal(t, 4, savings)
}

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

	op := &GitOp{OpType: "commit"}
	batcher.Enqueue(op)
	batcher.Flush()

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

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

func TestGetBatchInfo(t *testing.T) {
	batch := &GitBatch{
		ID:        "test-batch",
		Status:    "completed",
		CreatedAt: time.Now(),
		ExecutedAt: time.Now().Add(1 * time.Second),
	}

	info := batch.GetInfo()
	assert.Equal(t, "test-batch", info["id"])
	assert.Equal(t, "completed", info["status"])
}

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

	// Create 3 batches
	for batch := 0; batch < 3; batch++ {
		for i := 0; i < 3; i++ {
			op := &GitOp{
				OpType: "commit",
				Branch: "main",
			}
			batcher.Enqueue(op)
		}
	}

	// All 3 batches should be pending
	assert.Equal(t, 3, batcher.PendingBatchCount())
	assert.Equal(t, 0, batcher.QueueSize())
}

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

	// Enqueueing nil should not fail
	batcher.Enqueue(nil)
	assert.Equal(t, 0, batcher.QueueSize())
}

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

	for i := 0; i < b.N; i++ {
		op := &GitOp{
			OpType:  "commit",
			Branch:  "main",
			Message: "Commit",
		}
		batcher.Enqueue(op)
	}
}

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

	for i := 0; i < b.N; i++ {
		op := &GitOp{OpType: "commit"}
		batcher.Enqueue(op)

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