package push

import (
	"sync"
	"testing"
	"time"

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

func TestCircuitBreaker_StartsInClosedState(t *testing.T) {
	cb := NewCircuitBreaker("test")
	assert.Equal(t, "closed", cb.State())
	assert.True(t, cb.Allow())
}

func TestCircuitBreaker_OpensAfterThresholdFailures(t *testing.T) {
	cb := NewCircuitBreaker("test", WithFailureThreshold(3))

	// First two failures should keep it closed
	cb.RecordFailure()
	assert.Equal(t, "closed", cb.State())
	assert.True(t, cb.Allow())

	cb.RecordFailure()
	assert.Equal(t, "closed", cb.State())
	assert.True(t, cb.Allow())

	// Third failure should open it
	cb.RecordFailure()
	assert.Equal(t, "open", cb.State())
	assert.False(t, cb.Allow())
}

func TestCircuitBreaker_DefaultThresholdIsFive(t *testing.T) {
	cb := NewCircuitBreaker("test")

	for i := 0; i < 4; i++ {
		cb.RecordFailure()
		assert.Equal(t, "closed", cb.State())
	}

	cb.RecordFailure() // 5th failure
	assert.Equal(t, "open", cb.State())
}

func TestCircuitBreaker_RejectsRequestsWhenOpen(t *testing.T) {
	cb := NewCircuitBreaker("test", WithFailureThreshold(1))

	cb.RecordFailure()
	assert.Equal(t, "open", cb.State())

	// Multiple calls should all be rejected
	for i := 0; i < 10; i++ {
		assert.False(t, cb.Allow())
	}
}

func TestCircuitBreaker_TransitionsToHalfOpenAfterRecoveryTimeout(t *testing.T) {
	cb := NewCircuitBreaker("test",
		WithFailureThreshold(1),
		WithRecoveryTimeout(50*time.Millisecond),
	)

	cb.RecordFailure()
	assert.Equal(t, "open", cb.State())
	assert.False(t, cb.Allow())

	// Wait for recovery timeout
	time.Sleep(60 * time.Millisecond)

	// Should now allow one request (half-open)
	assert.True(t, cb.Allow())
	assert.Equal(t, "half-open", cb.State())
}

func TestCircuitBreaker_HalfOpenRejectsSecondRequest(t *testing.T) {
	cb := NewCircuitBreaker("test",
		WithFailureThreshold(1),
		WithRecoveryTimeout(50*time.Millisecond),
	)

	cb.RecordFailure()
	time.Sleep(60 * time.Millisecond)

	// First request allowed (probe)
	assert.True(t, cb.Allow())
	assert.Equal(t, "half-open", cb.State())

	// Second request rejected while probe is in flight
	assert.False(t, cb.Allow())
}

func TestCircuitBreaker_HalfOpenSuccess_ResetsToClosed(t *testing.T) {
	cb := NewCircuitBreaker("test",
		WithFailureThreshold(1),
		WithRecoveryTimeout(50*time.Millisecond),
	)

	cb.RecordFailure()
	time.Sleep(60 * time.Millisecond)

	// Probe request
	assert.True(t, cb.Allow())

	// Probe succeeds
	cb.RecordSuccess()
	assert.Equal(t, "closed", cb.State())
	assert.Equal(t, 0, cb.Counts())

	// Normal operation resumes
	assert.True(t, cb.Allow())
	assert.True(t, cb.Allow())
}

func TestCircuitBreaker_HalfOpenFailure_ReturnsToOpen(t *testing.T) {
	cb := NewCircuitBreaker("test",
		WithFailureThreshold(2),
		WithRecoveryTimeout(50*time.Millisecond),
	)

	// Open the circuit
	cb.RecordFailure()
	cb.RecordFailure()
	assert.Equal(t, "open", cb.State())

	time.Sleep(60 * time.Millisecond)

	// Probe request
	assert.True(t, cb.Allow())
	assert.Equal(t, "half-open", cb.State())

	// Probe fails - the failure count is now 3 which is >= threshold of 2
	cb.RecordFailure()
	assert.Equal(t, "open", cb.State())
	assert.False(t, cb.Allow())
}

func TestCircuitBreaker_SuccessResetsFailureCount(t *testing.T) {
	cb := NewCircuitBreaker("test", WithFailureThreshold(3))

	cb.RecordFailure()
	cb.RecordFailure()
	assert.Equal(t, 2, cb.Counts())

	// A success should reset the counter
	cb.RecordSuccess()
	assert.Equal(t, 0, cb.Counts())
	assert.Equal(t, "closed", cb.State())

	// Now it should take 3 more failures to open
	cb.RecordFailure()
	cb.RecordFailure()
	assert.Equal(t, "closed", cb.State())

	cb.RecordFailure()
	assert.Equal(t, "open", cb.State())
}

func TestCircuitBreaker_Reset(t *testing.T) {
	cb := NewCircuitBreaker("test", WithFailureThreshold(1))

	cb.RecordFailure()
	assert.Equal(t, "open", cb.State())

	cb.Reset()
	assert.Equal(t, "closed", cb.State())
	assert.Equal(t, 0, cb.Counts())
	assert.True(t, cb.Allow())
}

func TestCircuitBreaker_Name(t *testing.T) {
	cb := NewCircuitBreaker("apns-breaker")
	assert.Equal(t, "apns-breaker", cb.Name())
}

func TestCircuitBreaker_CustomOptions(t *testing.T) {
	cb := NewCircuitBreaker("test",
		WithFailureThreshold(10),
		WithRecoveryTimeout(5*time.Minute),
	)

	// Should take 10 failures to open
	for i := 0; i < 9; i++ {
		cb.RecordFailure()
		assert.Equal(t, "closed", cb.State())
	}
	cb.RecordFailure()
	assert.Equal(t, "open", cb.State())
}

func TestCircuitBreaker_InvalidOptionsIgnored(t *testing.T) {
	cb := NewCircuitBreaker("test",
		WithFailureThreshold(0),  // Should be ignored (keeps default)
		WithRecoveryTimeout(-1),  // Should be ignored (keeps default)
	)

	// Default threshold of 5 should still apply
	for i := 0; i < 4; i++ {
		cb.RecordFailure()
		assert.Equal(t, "closed", cb.State())
	}
	cb.RecordFailure()
	assert.Equal(t, "open", cb.State())
}

func TestCircuitBreaker_ThreadSafety(t *testing.T) {
	cb := NewCircuitBreaker("test",
		WithFailureThreshold(100),
		WithRecoveryTimeout(10*time.Millisecond),
	)

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

	// Hammer it from many goroutines
	for i := 0; i < goroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < iterations; j++ {
				cb.Allow()
				if j%2 == 0 {
					cb.RecordFailure()
				} else {
					cb.RecordSuccess()
				}
				_ = cb.State()
				_ = cb.Counts()
			}
		}(i)
	}

	wg.Wait()

	// Should not panic or deadlock. State should be valid.
	state := cb.State()
	require.Contains(t, []string{"closed", "open", "half-open"}, state)
}

func TestCircuitBreaker_FullLifecycle(t *testing.T) {
	cb := NewCircuitBreaker("lifecycle-test",
		WithFailureThreshold(3),
		WithRecoveryTimeout(50*time.Millisecond),
	)

	// 1. Closed: requests flow normally
	assert.True(t, cb.Allow())
	cb.RecordSuccess()
	assert.Equal(t, "closed", cb.State())

	// 2. Accumulate failures
	cb.RecordFailure()
	cb.RecordFailure()
	assert.Equal(t, "closed", cb.State())

	// 3. Third failure opens the circuit
	cb.RecordFailure()
	assert.Equal(t, "open", cb.State())
	assert.False(t, cb.Allow())

	// 4. Wait for recovery
	time.Sleep(60 * time.Millisecond)

	// 5. Half-open: probe request allowed
	assert.True(t, cb.Allow())
	assert.Equal(t, "half-open", cb.State())

	// 6. Probe succeeds, back to closed
	cb.RecordSuccess()
	assert.Equal(t, "closed", cb.State())
	assert.True(t, cb.Allow())
}
